Subject: 31.0.50; [PATCH] DocView: Opening ODF file fails when it has more than one file extension, or when using unoconv

From: Daniel Lopez <[email-masked]>
Date: Fri, 6 Jun 2025 05:12:12 +0100

Hi,

This is actually 2 seperate problems, but the code to fix them (patch 
below) is closely related so I hope it's okay to write it up like this.

1)
If I try and open a LibreOffice document with a name like 
"hello.odt.~123~" (example attached), the DocView conversion buffer 
appears and a second later the following error occurs:

File is missing: "Renaming", "No such file or directory", 
"/tmp/docview1000/hello.odt.~123~-76506e223cfc94b3909bc6acc92e1426/hello.pdf", 
"/tmp/docview1000/hello.odt.~123~-76506e223cfc94b3909bc6acc92e1426/doc.pdf"

(Practically speaking, this bit me when I tried to open a particular 
revision of a document from a Git repo, using `vc-revision-other-window` 
- as Emacs saves the file with a commit hash on the end of the name like 
so: "hello.odt.~1a1b409~").

The trouble is that when `doc-view-odf->pdf-converter-soffice` runs the 
external program 'soffice' to convert the document to a PDF, soffice can 
only be given an output directory, not a file name; soffice names it 
after the input file with the file extension changed to '.pdf'. But 
soffice considers the file extension to only be the last part ".~123~", 
so creates "hello.odt.pdf", whereas Emacs (via `file-name-base`) cuts 
off ".odt.~123~" and ends up expecting the file "hello.pdf".

2)
Through the customization option `doc-view-odf->pdf-converter-program`, 
Emacs can alternatively use the program 'unoconv' to do the conversion. 
But with this selected I couldn't open any LibreOffice documents, 
regardless of file name. Turns out that when Emacs passes a output 
directory to unoconv it always results in an output file simply called 
".pdf". But as it happens, unoconv does allow you to specify an output 
filename in full.

The patch:
I changed `doc-view-odf->pdf-converter-soffice`, 
`doc-view-odf->pdf-converter-unoconv` and the calling function 
`doc-view-convert-current-doc` to work around or fix both these issues. 
Any renaming is now done within the former two functions to keep the 
quirks of those programs contained.

Daniel

In GNU Emacs 31.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
3.24.49, cairo version 1.18.4) of 2025-04-01 built on ENO
Repository revision: bdee2481728f57053690f0dd76c943ddd5f343d9
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12101016
System Description: Arch Linux

Configured using:
'configure --prefix=/usr --sysconfdir=/etc --libexecdir=/usr/lib
--localstatedir=/var --mandir=/usr/share/man --with-gameuser=:games
--with-modules --without-libotf --without-m17n-flt --without-gconf
--with-native-compilation=yes --with-xinput2 --with-x-toolkit=gtk3
--without-xaw3d --with-sound=alsa --with-tree-sitter --without-gpm
--without-compress-install
'--program-transform-name=s/\([ec]tags\)/\1.emacs/'
'CFLAGS=-march=x86-64 -mtune=generic -O2 -pipe -fno-plt -fexceptions
-Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security
-fstack-clash-protection -fcf-protection -fno-omit-frame-pointer
-mno-omit-leaf-frame-pointer -g
-ffile-prefix-map=/mnt/gear/download/archbuilds/text/emacs-git/src=/usr/src/debug/emacs-git
-flto=auto' 'LDFLAGS=-Wl,-O1 -Wl,--sort-common -Wl,--as-needed
-Wl,-z,relro -Wl,-z,now -Wl,-z,pack-relative-relocs -flto=auto''

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GSETTINGS HARFBUZZ JPEG
LCMS2 LIBSYSTEMD LIBXML2 MODULES NATIVE_COMP NOTIFY INOTIFY PDUMPER PNG
RSVG SECCOMP SOUND SQLITE3 THREADS TIFF TOOLKIT_SCROLL_BARS TREE_SITTER
WEBP X11 XDBE XIM XINERAMA XINPUT2 XPM XRANDR GTK3 ZLIB

Important settings:
value of $LANG: en_GB.UTF-8
locale-coding-system: utf-8

Major mode: Text

Minor modes in effect:
global-company-mode: t
company-mode: t
server-mode: t
ergokeys-minor-mode: t
eval-sexp-fu-flash-mode: t
volatile-highlights-mode: t
ioutline-minor-mode: t
global-undo-tree-mode: t
undo-tree-mode: t
xclip-mode: t
delete-selection-mode: t
windmove-mode: t
tooltip-mode: t
global-eldoc-mode: t
eldoc-mode: t
show-paren-mode: t
electric-indent-mode: t
mouse-wheel-mode: t
menu-bar-mode: t
file-name-shadow-mode: t
global-font-lock-mode: t
font-lock-mode: t
blink-cursor-mode: t
minibuffer-regexp-mode: t
column-number-mode: t
line-number-mode: t
visual-line-mode: t
transient-mark-mode: t
auto-composition-mode: t
auto-encryption-mode: t
auto-compression-mode: t

Load-path shadows:
/home/daniel/.emacs.d/externals/expand-region.el/expand-region hides 
/home/daniel/.emacs.d/externals/expand-region
/home/daniel/.emacs.d/externals/multiple-cursors.el/multiple-cursors 
hides /home/daniel/.emacs.d/externals/multiple-cursors
/home/daniel/.emacs.d/externals/cmake-mode hides 
/home/daniel/.emacs.d/elpa/cmake-mode-20241219.1518/cmake-mode
/home/daniel/.emacs.d/externals/multiple-cursors.el/mc-mark-pop hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/mc-mark-pop
/home/daniel/.emacs.d/externals/multiple-cursors.el/mc-hide-unmatched-lines-mode 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/mc-hide-unmatched-lines-mode
/home/daniel/.emacs.d/externals/multiple-cursors.el/mc-cycle-cursors 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/mc-cycle-cursors
/home/daniel/.emacs.d/externals/multiple-cursors.el/mc-separate-operations 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/mc-separate-operations
/home/daniel/.emacs.d/externals/multiple-cursors.el/rectangular-region-mode 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/rectangular-region-mode
/home/daniel/.emacs.d/externals/multiple-cursors.el/multiple-cursors-core 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/multiple-cursors-core
/home/daniel/.emacs.d/externals/multiple-cursors.el/multiple-cursors 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/multiple-cursors
/home/daniel/.emacs.d/externals/multiple-cursors.el/mc-mark-more hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/mc-mark-more
/home/daniel/.emacs.d/externals/multiple-cursors.el/multiple-cursors-pkg 
hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/multiple-cursors-pkg
/home/daniel/.emacs.d/externals/multiple-cursors.el/mc-edit-lines hides 
/home/daniel/.emacs.d/elpa/multiple-cursors-20180913.1237/mc-edit-lines
/home/daniel/.emacs.d/externals/pkg-info hides 
/home/daniel/.emacs.d/elpa/pkg-info-20150517.1143/pkg-info
/home/daniel/.emacs.d/externals/epl hides 
/home/daniel/.emacs.d/elpa/epl-20180205.2049/epl
/home/daniel/.emacs.d/externals/highlight hides 
/home/daniel/.emacs.d/elpa/highlight-20161103.1410/highlight
/home/daniel/.emacs.d/elpa/lispy-20230314.1738/elpa hides 
/home/daniel/.emacs.d/elpa/worf-20220102.835/elpa
/home/daniel/.emacs.d/elpa/lv-20200507.1518/lv hides 
/home/daniel/.emacs.d/elpa/hydra-20181030.1925/lv
/home/daniel/.emacs.d/elpa/lispy-20230314.1738/elpa hides 
/home/daniel/.emacs.d/elpa/ivy-20230714.751/elpa
/home/daniel/.emacs.d/externals/cmake-mode hides 
/usr/share/emacs/site-lisp/cmake-mode
/home/daniel/.emacs.d/elpa/transient-20250418.2149/transient hides 
/usr/share/emacs/31.0.50/lisp/transient
/home/daniel/.emacs.d/dan/asm-mode hides 
/usr/share/emacs/31.0.50/lisp/progmodes/asm-mode
/home/daniel/.emacs.d/elpa/csharp-mode-20211124.1105/csharp-mode hides 
/usr/share/emacs/31.0.50/lisp/progmodes/csharp-mode
/home/daniel/.emacs.d/externals/peg hides 
/usr/share/emacs/31.0.50/lisp/progmodes/peg
/home/daniel/.emacs.d/dan/re-builder hides 
/usr/share/emacs/31.0.50/lisp/emacs-lisp/re-builder

Features:
(pcmpl-unix sh-script executable shell pcomplete find-dired grep
mhtml-mode css-mode-expansions css-mode smie eww vtable url-queue shr
pixel-fill kinsoku url-file svg xml mm-url gnus nnheader range
js-mode-expansions js c-ts-common cc-mode-expansions cc-mode cc-fonts
cc-guess cc-menus cc-cmds cc-styles cc-align cc-engine cc-vars cc-defs
html-mode-expansions sgml-mode facemenu dom tabify compare-w shadow sort
mail-extr emacsbug lisp-mnt message yank-media rfc822 mml mml-sec epa
epg rfc6068 epg-config gnus-util mm-decode mm-bodies mm-encode
mailabbrev gmm-utils mailheader sendmail face-remap markdown-mode
let-alist noutline outline doc-view filenotify jka-compr image-mode exif
scroll-lock pulse python-el-fgallina-expansions python compat treesit
time-date compile thai-util thai-word cl-print edebug debug backtrace
find-func mail-utils gnutls network-stream url-http mail-parse rfc2231
rfc2047 rfc2045 mm-util ietf-drums mail-prsvr url-gw nsm puny url-cache
url-auth vc-svn dabbrev repeat shortdoc help-fns radix-tree misearch
multi-isearch vc-git diff-mode track-changes files-x vc-dispatcher
ibuf-ext dired-aux dan-encl imenu company-oddmuse company-keywords
company-etags company-gtags company-dabbrev-code company-dabbrev
company-files company-capf company-cmake company-xcode company-clang
company-semantic company-eclim company-template company-bbdb company
server cus-edit pp cus-load polymode poly-lock polymode-base
polymode-weave polymode-export polymode-compat polymode-methods
polymode-core format-spec polymode-classes eieio-custom wid-edit
eieio-base color dsqlite text-property-search mule-util ddrill
outline-ex emacs-process expand-region text-mode-expansions
er-basic-expansions expand-region-core expand-region-custom
ace-jump-mode paredit edmacro kmacro shadertoy-mode sporth-mode
soundtoy-mode sc-mode sclang-language snd-mode parse-lisp wat-mode
wat-mode-macro wat-mode-font-lock butterchurn-mode electron-browser-mode
kite-mini-mode parse-js dash websocket bindat dpython-mode dpdb dgdb
IEEE-754 gud comint ansi-osc direwindow windowLight dan-comint python-ex
python-queries parse-python re-search-ex dan-test ffap asm-mode derived
gud-short-key-mode gud-ex comint-ex eval-sexp-fu highlight etags
fileloop generator xref project volatile-highlights dan-comment
outline-db-mode ioutline undo-tree diff desktop frameset ansi-color
pcase number-scrubbing stringmath dired-x dired dired-loaddefs
dired-details xclip kill-ring-search browse-kill-ring delsel
multiple-cursors mc-hide-unmatched-lines-mode mc-separate-operations
rectangular-region-mode mc-mark-pop mc-mark-more thingatpt
mc-cycle-cursors mc-edit-lines multiple-cursors-core rect advice
ibuf-macs ibuffer ibuffer-loaddefs window-ex dan-windresize-mode
easy-mmode transpose-frame windresize ring windmove finder-inf comp
comp-cstr cl-extra help-mode warnings comp-run comp-common rx tex-site
basic-mode-autoloads bui-autoloads cmake-mode-autoloads
csv-mode-autoloads dape-autoloads dictcc-autoloads
dired-duplicates-autoloads dumb-jump-autoloads eat-autoloads
heap-autoloads cl llama-autoloads lsp-docker-autoloads
lsp-treemacs-autoloads lsp-mode-autoloads lua-mode-autoloads
lv-autoloads minimap-autoloads org-drill-autoloads pamparam-autoloads
ivy-posframe-autoloads lispy-autoloads iedit-autoloads
pdf-tools-autoloads powershell-autoloads realgud-recursive-autoloads
reverso-autoloads slime-autoloads sqlite-mode-extras-autoloads
transient-autoloads transpose-frame-autoloads treemacs-autoloads
cfrs-autoloads pfuture-autoloads ace-window-autoloads vterm-autoloads
wat-ts-mode-autoloads worf-autoloads ace-link-autoloads avy-autoloads
swiper-autoloads info ivy-autoloads xclip-autoloads yaml-autoloads
zones-autoloads zoutline-autoloads package browse-url xdg url url-proxy
url-privacy url-expand url-methods url-history url-cookie
generate-lisp-file url-domsuf url-util mailcap url-handlers url-parse
auth-source cl-seq eieio eieio-core cl-macs icons password-cache json
subr-x map byte-opt gv bytecomp byte-compile url-vars cl-loaddefs cl-lib
rmc iso-transl tooltip cconv eldoc paren electric uniquify ediff-hook
vc-hooks lisp-float-type elisp-mode mwheel term/x-win x-win
term/common-win x-dnd touch-screen tool-bar dnd fontset image regexp-opt
fringe tabulated-list replace newcomment text-mode lisp-mode prog-mode
register page tab-bar menu-bar rfn-eshadow isearch easymenu timer select
scroll-bar mouse jit-lock font-lock syntax font-core term/tty-colors
frame minibuffer nadvice seq simple cl-generic indonesian philippine
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite emoji-zwj charscript
charprop case-table epa-hook jka-cmpr-hook help abbrev obarray oclosure
cl-preloaded button loaddefs theme-loaddefs faces cus-face macroexp
files window text-properties overlay sha1 md5 base64 format env
code-pages mule custom widget keymap hashtable-print-readable backquote
threads dbusbind inotify lcms2 dynamic-setting system-font-setting
font-render-setting cairo gtk x-toolkit xinput2 x multi-tty move-toolbar
make-network-process tty-child-frames native-compile emacs)

Memory information:
((conses 16 1146743 300078) (symbols 48 63070 77)
(strings 32 326478 27387) (string-bytes 1 13536281) (vectors 16 90479)
(vector-slots 8 1878066 144783) (floats 8 460 2152)
(intervals 56 61868 3838) (buffers 992 83))

--- /usr/share/emacs/31.0.50/lisp/doc-view.el	2025-04-01 04:54:28.000000000 +0100
+++ doc-view.el	2025-06-06 04:55:37.000000000 +0100
@@ -1323,18 +1323,14 @@ The test is performed using `doc-view-pd
        ,@(if page `(,(format "%d" page))))
      callback)))
 
-(defun doc-view-odf->pdf-converter-unoconv (odf callback)
-  "Convert ODF to PDF asynchronously and call CALLBACK when finished.
-The converted PDF is put into the current cache directory, and it
-is named like ODF with the extension turned to pdf."
+(defun doc-view-odf->pdf-converter-unoconv (odf pdf callback)
+  "Convert ODF to PDF asynchronously and call CALLBACK when finished."
   (doc-view-start-process "odf->pdf" doc-view-odf->pdf-converter-program
-			  (list "-f" "pdf" "-o" (doc-view--current-cache-dir) odf)
+			  (list "-f" "pdf" "-o" pdf odf)
 			  callback))
 
-(defun doc-view-odf->pdf-converter-soffice (odf callback)
-  "Convert ODF to PDF asynchronously and call CALLBACK when finished.
-The converted PDF is put into the current cache directory, and it
-is named like ODF with the extension turned to pdf."
+(defun doc-view-odf->pdf-converter-soffice (odf pdf callback)
+  "Convert ODF to PDF asynchronously and call CALLBACK when finished."
   ;; FIXME: soffice doesn't work when there's another running
   ;; LibreOffice instance, in which case it returns success without
   ;; actually doing anything.  See LibreOffice bug
@@ -1353,6 +1349,17 @@ is named like ODF with the extension tur
 			     "--headless" "--convert-to" "pdf"
 			     "--outdir" (doc-view--current-cache-dir) odf)
 			    (lambda ()
+			      ;; FIXME: soffice allows you to choose the directory for the output file,
+			      ;; but always names it after the input file with its extension changed.
+			      ;; Do the same transformation ourselves (note soffice only replaces the
+			      ;; very last extension, if there is more than one) and rename the file
+			      ;; to the given 'pdf' argument.
+			      (let ((name-from-soffice (expand-file-name
+							(concat (replace-regexp-in-string "\\.[^.]*$" "" (file-name-nondirectory doc-view--buffer-file-name)) ".pdf")
+							doc-view--current-cache-dir)))
+				(when (not (string= name-from-soffice pdf))
+				  (rename-file name-from-soffice pdf)))
+			      ;;
 			      (delete-directory tmp-user-install-dir t)
 			      (funcall callback)))))
 
@@ -1496,19 +1503,9 @@ Those files are saved in the directory g
       ('odf
        ;; ODF files have to be converted to PDF before Ghostscript can
        ;; process it.
-       (let ((pdf (doc-view-current-cache-doc-pdf))
-             (opdf (expand-file-name
-                    (concat (file-name-base doc-view--buffer-file-name)
-                            ".pdf")
-                    doc-view--current-cache-dir))
-             (png-file png-file))
-	 ;; The unoconv tool only supports an output directory, but no
-	 ;; file name.  It's named like the input file with the
-	 ;; extension replaced by pdf.
-         (funcall doc-view-odf->pdf-converter-function doc-view--buffer-file-name
+       (let ((pdf (doc-view-current-cache-doc-pdf)))
+         (funcall doc-view-odf->pdf-converter-function doc-view--buffer-file-name pdf
                   (lambda ()
-		    ;; Rename to doc.pdf
-		    (rename-file opdf pdf)
 		    (doc-view-pdf/ps->png pdf png-file)))))
       ;; The doc-view-mode-p check ensures that epub, cbz, fb2 and
       ;; (o)xps are handled with mutool