Subject: bug#13164: [PATCH] View DjVu documents through doc-view-mode

From: Elias Pipping <[email-masked]>
Date: Tue, 22 Jan 2013 00:10:32 +0100

Tassilo Horn <[email-masked]> writes:

> Stefan Monnier <[email-masked]> writes:
>
> Hi Elias & Stefan,
>
>>> I've attached three more tiny patches.
>>
>> Thanks, installed,
>
> I really like the new mupdf support.  It's much faster than ghostscript.
>
> There seems to be some minor issue, though: the last-page counter in the
> mode-line doesn't seem to be updated properly during conversion.  I've
> just tested with some 700 pages PDF. Initially, it displayed "1/4" but
> didn't increase anymore.  To make it update again, I needed to do
> something, e.g., switch to another page or just scroll a bit.

Indeed, I can reproduce your problem with a recent version of emacs,
namely

  commit c5a8149837c5ed53655d4383dea3b8f29374b266
  Author: Glenn Morris <[email-masked]>
  Date:   Sat Jan 19 18:40:49 2013 -0800

      * lisp-mode.el (emacs-lisp-mode-map): Add native profiler menu entries.

I do not have it with emacs 24.2.92 and my original two patches (I'm
attaching them in case you want to verify that; please note that you
need to have

  (setq doc-view-pdf->png-converter-invocation
        'doc-view-pdf->png-converter-invocation-mupdf)

in your init.el with that version for mupdf to be used). So it must've
snuck in sometime after.

Best regards,

Elias Pipping

>From f6684796d3a9cb0016b50251d9b36a4c618855c1 Mon Sep 17 00:00:00 2001
From: Elias Pipping <[email-masked]>
Date: Sun, 26 Jun 2011 20:02:29 +0200
Subject: [PATCH 1/2] Make doc-view-pdf->png-1 customizable

This allows mupdf's mudraw to be used as a pdf->png converter
Not only is it faster than ghostscript, it also does anti-aliasing
---
 lisp/doc-view.el | 73 ++++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 55 insertions(+), 18 deletions(-)

diff --git a/lisp/doc-view.el b/lisp/doc-view.el
index f8975a5..8e1b309 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -158,6 +158,26 @@
   :type 'file
   :group 'doc-view)
 
+(defcustom doc-view-pdfdraw-program "mudraw"
+  "Program to convert PDF files to PNG."
+  :type 'file
+  :group 'doc-view)
+
+(defcustom doc-view-pdf->png-converter-invocation
+  'doc-view-pdf->png-converter-invocation-ghostscript
+  "Called to convert a PDF file into a PNG file"
+  :type '(radio (function-item doc-view-pdf->png-converter-invocation-ghostscript :doc "Use ghostscript")
+		(function-item doc-view-pdf->png-converter-invocation-mupdf :doc "Use mupdf")
+                function)
+  :group 'doc-view)
+
+(defcustom doc-view-ps->png-converter-invocation
+  'doc-view-ps->png-converter-invocation-ghostscript
+  "Called to convert a PS file into a PNG file"
+  :type '(radio (function-item doc-view-ps->png-converter-invocation-ghostscript :doc "Use ghostscript")
+                function)
+  :group 'doc-view)
+
 (defcustom doc-view-ghostscript-options
   '("-dSAFER" ;; Avoid security problems when rendering files from untrusted
     ;; sources.
@@ -653,6 +673,7 @@ OpenDocument format)."
 		       (executable-find doc-view-dvipdfm-program)))))
 	((or (eq type 'postscript) (eq type 'ps) (eq type 'eps)
 	     (eq type 'pdf))
+	 ;; FIXME: allow mupdf here
 	 (and doc-view-ghostscript-program
 	      (executable-find doc-view-ghostscript-program)))
 	((eq type 'odf)
@@ -817,6 +838,25 @@ Should be invoked when the cached images aren't up-to-date."
 			    (list "-o" pdf dvi)
 			    callback)))
 
+(defun doc-view-pdf->png-converter-invocation-ghostscript (resolution pdf png &optional page)
+  `((command . ,doc-view-ghostscript-program)
+    (arguments . (,@doc-view-ghostscript-options
+		  ,(format "-r%d" resolution)
+		  ,@(if page `(,(format "-dFirstPage=%d" page)))
+		  ,@(if page `(,(format "-dLastPage=%d" page)))
+		  ,(concat "-sOutputFile=" png)
+		  ,pdf))))
+
+(defalias 'doc-view-ps->png-converter-invocation-ghostscript
+  'doc-view-pdf->png-converter-invocation-ghostscript)
+
+(defun doc-view-pdf->png-converter-invocation-mupdf (resolution pdf png &optional page)
+  `((command . ,doc-view-pdfdraw-program)
+    (arguments . (,(concat "-o" png)
+		  ,(format "-r%d" resolution)
+		  ,pdf
+		  ,@(if page `(,(format "%d" page)))))))
+
 (defun doc-view-odf->pdf (odf callback)
   "Convert ODF to PDF asynchronously and call CALLBACK when finished.
 The converted PDF is put into the current cache directory, and it
@@ -827,12 +867,14 @@ is named like ODF with the extension turned to pdf."
 
 (defun doc-view-pdf/ps->png (pdf-ps png)
   "Convert PDF-PS to PNG asynchronously."
-  (doc-view-start-process
-   "pdf/ps->png" doc-view-ghostscript-program
-   (append doc-view-ghostscript-options
-           (list (format "-r%d" (round doc-view-resolution))
-                 (concat "-sOutputFile=" png)
-                 pdf-ps))
+  (let ((invocation (case doc-view-doc-type
+		      (pdf (funcall doc-view-pdf->png-converter-invocation
+				    (round doc-view-resolution) pdf-ps png))
+		      (t   (funcall doc-view-ps->png-converter-invocation
+				    (round doc-view-resolution) pdf-ps png)))))
+    (doc-view-start-process
+     "pdf/ps->png" (cdr (assoc 'command invocation))
+     (cdr (assoc 'arguments invocation))
    (let ((resolution doc-view-resolution))
      (lambda ()
        ;; Only create the resolution file when it's all done, so it also
@@ -844,7 +886,7 @@ is named like ODF with the extension turned to pdf."
        (when doc-view-current-timer
          (cancel-timer doc-view-current-timer)
          (setq doc-view-current-timer nil))
-       (doc-view-display (current-buffer) 'force))))
+       (doc-view-display (current-buffer) 'force)))))
   ;; Update the displayed pages as soon as they're done generating.
   (when doc-view-conversion-refresh-interval
     (setq doc-view-current-timer
@@ -855,17 +897,12 @@ is named like ODF with the extension turned to pdf."
 (defun doc-view-pdf->png-1 (pdf png page callback)
   "Convert a PAGE of a PDF file to PNG asynchronously.
 Call CALLBACK with no arguments when done."
-  (doc-view-start-process
-   "pdf->png-1" doc-view-ghostscript-program
-   (append doc-view-ghostscript-options
-           (list (format "-r%d" (round doc-view-resolution))
-                 ;; Sadly, `gs' only supports the page-range
-                 ;; for PDF files.
-                 (format "-dFirstPage=%d" page)
-                 (format "-dLastPage=%d" page)
-                 (concat "-sOutputFile=" png)
-                 pdf))
-   callback))
+  (let ((invocation (funcall doc-view-pdf->png-converter-invocation
+			     (round doc-view-resolution) pdf png page)))
+    (doc-view-start-process
+     "pdf/ps->png" (cdr (assoc 'command invocation))
+     (cdr (assoc 'arguments invocation))
+     callback)))
 
 (declare-function clear-image-cache "image.c" (&optional filter))
 
-- 
1.8.0.2

>From c02ac7ce9db90f1772658beae4dd0c51debf0bdd Mon Sep 17 00:00:00 2001
From: Elias Pipping <[email-masked]>
Date: Sat, 1 Sep 2012 18:15:11 +0200
Subject: [PATCH 2/2] Add DjVu support

---
 lisp/doc-view.el | 135 ++++++++++++++++++++++++++++++++++++++++++-------------
 lisp/files.el    |   2 +-
 2 files changed, 106 insertions(+), 31 deletions(-)

diff --git a/lisp/doc-view.el b/lisp/doc-view.el
index 8e1b309..ea2023e 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -138,6 +138,7 @@
 ;;; Code:
 
 (eval-when-compile (require 'cl-lib))
+(require 'cl)
 (require 'dired)
 (require 'image-mode)
 (require 'jka-compr)
@@ -145,7 +146,7 @@
 ;;;; Customization Options
 
 (defgroup doc-view nil
-  "In-buffer viewer for PDF, PostScript and DVI files."
+  "In-buffer viewer for PDF, PostScript, DVI, and DJVU files."
   :link '(function-link doc-view)
   :version "22.2"
   :group 'applications
@@ -171,6 +172,18 @@
                 function)
   :group 'doc-view)
 
+(defcustom doc-view-ddjvu-program "ddjvu"
+  "Program to convert DjVu files to PNG."
+  :type 'file
+  :group 'doc-view)
+
+(defcustom doc-view-djvu->png-converter-invocation
+  'doc-view-djvu->png-converter-invocation-ddjvu
+  "Called to convert a DJVU file into a PNG file"
+  :type '(radio (function-item doc-view-djvu->png-converter-invocation-ddjvu :doc "Use ddjvu")
+                function)
+  :group 'doc-view)
+
 (defcustom doc-view-ps->png-converter-invocation
   'doc-view-ps->png-converter-invocation-ghostscript
   "Called to convert a PS file into a PNG file"
@@ -332,6 +345,17 @@ the (uncompressed, extracted) file residing in
   "The type of document in the current buffer.
 Can be `dvi', `pdf', or `ps'.")
 
+(defvar doc-view-single-page-converter nil
+  "Called to convert a single page of the document to a PNG file.")
+
+(defvar doc-view-image-type nil
+  "The type of image in the current buffer.
+Can be `png' or `tiff'.")
+
+(defvar doc-view-image-file-extension nil
+  "The file extension of the image type in the current buffer.
+Can be `png' or `tif'.")
+
 ;;;; DocView Keymaps
 
 (defvar doc-view-mode-map
@@ -470,24 +494,25 @@ Can be `dvi', `pdf', or `ps'.")
     ;; We used to find the file name from doc-view-current-files but
     ;; that's not right if the pages are not generated sequentially
     ;; or if the page isn't in doc-view-current-files yet.
-    (let ((file (expand-file-name (format "page-%d.png" page)
+    (let ((file (expand-file-name (format "page-%d.%s" page doc-view-image-file-extension)
                                   (doc-view-current-cache-dir))))
       (doc-view-insert-image file :pointer 'arrow)
       (set-window-hscroll (selected-window) hscroll)
       (when (and (not (file-exists-p file))
                  doc-view-current-converter-processes)
         ;; The PNG file hasn't been generated yet.
-        (doc-view-pdf->png-1 doc-view-buffer-file-name file page
-                             (let ((win (selected-window)))
-                               (lambda ()
-                                 (and (eq (current-buffer) (window-buffer win))
-                                      ;; If we changed page in the mean
-                                      ;; time, don't mess things up.
-                                      (eq (doc-view-current-page win) page)
-                                      ;; Make sure we don't infloop.
-                                      (file-readable-p file)
-                                      (with-selected-window win
-							    (doc-view-goto-page page))))))))
+        (funcall doc-view-single-page-converter
+		 doc-view-buffer-file-name file page
+		 (let ((win (selected-window)))
+		   (lambda ()
+		     (and (eq (current-buffer) (window-buffer win))
+			  ;; If we changed page in the mean
+			  ;; time, don't mess things up.
+			  (eq (doc-view-current-page win) page)
+			  ;; Make sure we don't infloop.
+			  (file-readable-p file)
+			  (with-selected-window win
+			    (doc-view-goto-page page))))))))
     (overlay-put (doc-view-current-overlay)
                  'help-echo (doc-view-current-info))))
 
@@ -663,7 +688,7 @@ Document types are symbols like `dvi', `ps', `pdf', or `odf' (any
 OpenDocument format)."
   (and (display-graphic-p)
        (or (image-type-available-p 'imagemagick)
-	   (image-type-available-p 'png))
+	   (image-type-available-p 'png)) ; FIXME: do not check for PNG with djvu
        (cond
 	((eq type 'dvi)
 	 (and (doc-view-mode-p 'pdf)
@@ -680,6 +705,8 @@ OpenDocument format)."
 	 (and doc-view-unoconv-program
 	      (executable-find doc-view-unoconv-program)
 	      (doc-view-mode-p 'pdf)))
+	((eq type 'djvu)
+	 (executable-find doc-view-ddjvu-program)) ; FIXME: check for TIFF with djvu
 	(t ;; unknown image type
 	 nil))))
 
@@ -850,6 +877,16 @@ Should be invoked when the cached images aren't up-to-date."
 (defalias 'doc-view-ps->png-converter-invocation-ghostscript
   'doc-view-pdf->png-converter-invocation-ghostscript)
 
+(defun doc-view-djvu->png-converter-invocation-ddjvu (resolution djvu png &optional page)
+  `((command . ,doc-view-ddjvu-program)
+    (arguments . ("-format=tiff"
+		  ,(format "-scale=%d" resolution) ; ddjvu only accepts the range 1-999
+		  ;; -eachpage was only added after djvulibre-3.5.25.3!
+		  ,@(unless page '("-eachpage"))
+		  ,@(if page `(,(format "-page=%d" page)))
+		  ,djvu
+		  ,png))))
+
 (defun doc-view-pdf->png-converter-invocation-mupdf (resolution pdf png &optional page)
   `((command . ,doc-view-pdfdraw-program)
     (arguments . (,(concat "-o" png)
@@ -867,10 +904,12 @@ is named like ODF with the extension turned to pdf."
 
 (defun doc-view-pdf/ps->png (pdf-ps png)
   "Convert PDF-PS to PNG asynchronously."
-  (let ((invocation (case doc-view-doc-type
-		      (pdf (funcall doc-view-pdf->png-converter-invocation
+  (let ((invocation (pcase doc-view-doc-type
+		      (`pdf (funcall doc-view-pdf->png-converter-invocation
 				    (round doc-view-resolution) pdf-ps png))
-		      (t   (funcall doc-view-ps->png-converter-invocation
+		      (`djvu (funcall doc-view-djvu->png-converter-invocation
+				     (round doc-view-resolution) pdf-ps png))
+		      (_   (funcall doc-view-ps->png-converter-invocation
 				    (round doc-view-resolution) pdf-ps png)))))
     (doc-view-start-process
      "pdf/ps->png" (cdr (assoc 'command invocation))
@@ -904,9 +943,19 @@ Call CALLBACK with no arguments when done."
      (cdr (assoc 'arguments invocation))
      callback)))
 
+(defun doc-view-djvu->png-1 (djvu png page callback)
+  "Convert a PAGE of a DJVU file to PNG asynchronously.
+Call CALLBACK with no arguments when done."
+  (let ((invocation (funcall doc-view-djvu->png-converter-invocation
+			     (round doc-view-resolution) djvu png page)))
+    (doc-view-start-process
+     "djvu->png" (cdr (assoc 'command invocation))
+     (cdr (assoc 'arguments invocation))
+     callback)))
+
 (declare-function clear-image-cache "image.c" (&optional filter))
 
-(defun doc-view-pdf->png (pdf png pages)
+(defun doc-view-document->png (pdf png pages)
   "Convert a PDF file to PNG asynchronously.
 Start by converting PAGES, and then the rest."
   (if (null pages)
@@ -916,11 +965,11 @@ Start by converting PAGES, and then the rest."
     ;; a single page anyway, and of the remaining 1%, few cases will have
     ;; consecutive pages, it's not worth the trouble.
     (let ((rest (cdr pages)))
-      (doc-view-pdf->png-1
-       pdf (format png (car pages)) (car pages)
+      (funcall doc-view-single-page-converter
+	       pdf (format png (car pages)) (car pages)
        (lambda ()
          (if rest
-             (doc-view-pdf->png pdf png rest)
+             (doc-view-document->png pdf png rest)
            ;; Yippie, the important pages are done, update the display.
            (clear-image-cache)
            ;; For the windows that have a message (like "Welcome to
@@ -928,8 +977,8 @@ Start by converting PAGES, and then the rest."
            ;; not sufficient.
            (dolist (win (get-buffer-window-list (current-buffer) nil 'visible))
              (with-selected-window win
-				   (when (stringp (get-char-property (point-min) 'display))
-				     (doc-view-goto-page (doc-view-current-page)))))
+	       (when (stringp (get-char-property (point-min) 'display))
+		 (doc-view-goto-page (doc-view-current-page)))))
            ;; Convert the rest of the pages.
            (doc-view-pdf/ps->png pdf png)))))))
 
@@ -999,7 +1048,7 @@ Those files are saved in the directory given by the function
   ;; preserves the horizontal/vertical scroll settings (which are otherwise
   ;; resets during the redisplay).
   (setq doc-view-pending-cache-flush t)
-  (let ((png-file (expand-file-name "page-%d.png"
+  (let ((png-file (expand-file-name (concat "page-%d." doc-view-image-file-extension)
                                     (doc-view-current-cache-dir))))
     (make-directory (doc-view-current-cache-dir) t)
     (pcase doc-view-doc-type
@@ -1028,7 +1077,10 @@ Those files are saved in the directory given by the function
       (`pdf
        (let ((pages (doc-view-active-pages)))
          ;; Convert PDF to PNG images starting with the active pages.
-         (doc-view-pdf->png doc-view-buffer-file-name png-file pages)))
+         (doc-view-document->png doc-view-buffer-file-name png-file pages)))
+      (`djvu
+       (let ((pages (doc-view-active-pages)))
+	 (doc-view-document->png doc-view-buffer-file-name png-file pages)))
       (_
        ;; Convert to PNG images.
        (doc-view-pdf/ps->png doc-view-buffer-file-name png-file)))))
@@ -1171,7 +1223,7 @@ ARGS is a list of image descriptors."
   (let ((ol (doc-view-current-overlay))
         (image (if (and file (file-readable-p file))
 		   (if (not (fboundp 'imagemagick-types))
-		       (apply 'create-image file 'png nil args)
+		       (apply 'create-image file doc-view-image-type nil args)
 		     (unless (member :width args)
 		       (setq args (append args (list :width doc-view-image-width))))
 		     (apply 'create-image file 'imagemagick nil args))))
@@ -1221,12 +1273,12 @@ have the page we want to view."
     (let ((prev-pages doc-view-current-files))
       (setq doc-view-current-files
             (sort (directory-files (doc-view-current-cache-dir) t
-                                   "page-[0-9]+\\.png" t)
+                                   (concat "page-[0-9]+\\." doc-view-image-file-extension) t)
                   'doc-view-sort))
       (dolist (win (or (get-buffer-window-list buffer nil t)
 		       (list t)))
 	(let* ((page (doc-view-current-page win))
-	       (pagefile (expand-file-name (format "page-%d.png" page)
+	       (pagefile (expand-file-name (format "page-%d.%s" page doc-view-image-file-extension)
 					   (doc-view-current-cache-dir))))
 	  (when (or force
 		    (and (not (member pagefile prev-pages))
@@ -1421,7 +1473,7 @@ If BACKWARD is non-nil, jump to the previous match."
        (file-readable-p (expand-file-name "resolution.el"
                                           (doc-view-current-cache-dir)))
        (> (length (directory-files (doc-view-current-cache-dir)
-                                   nil "\\.png\\'"))
+                                   nil (concat "\\." doc-view-image-file-extension "\\'")))
           0)))
 
 (defun doc-view-initiate-display ()
@@ -1494,6 +1546,8 @@ If BACKWARD is non-nil, jump to the previous match."
 			 ("pdf" pdf) ("epdf" pdf)
 			 ;; PostScript
 			 ("ps" ps) ("eps" ps)
+			 ;; DjVu
+			 ("djvu" djvu)
 			 ;; OpenDocument formats
 			 ("odt" odf) ("ods" odf) ("odp" odf) ("odg" odf)
 			 ("odc" odf) ("odi" odf) ("odm" odf) ("ott" odf)
@@ -1508,7 +1562,8 @@ If BACKWARD is non-nil, jump to the previous match."
 	   (cond
 	    ((looking-at "%!") '(ps))
 	    ((looking-at "%PDF") '(pdf))
-	    ((looking-at "\367\002") '(dvi))))))
+	    ((looking-at "\367\002") '(dvi))
+	    ((looking-at "AT&TFORM") '(djvu))))))
     (set (make-local-variable 'doc-view-doc-type)
 	 (car (or (doc-view-intersection name-types content-types)
 		  (when (and name-types content-types)
@@ -1517,6 +1572,24 @@ If BACKWARD is non-nil, jump to the previous match."
 		  name-types content-types
 		  (error "Cannot determine the document type"))))))
 
+(defun doc-view-set-up-single-converter ()
+  "Find the right single-page converter for the current document type"
+  (set (make-local-variable 'doc-view-single-page-converter)
+       (pcase doc-view-doc-type
+	     (`djvu 'doc-view-djvu->png-1)
+	     (_ 'doc-view-pdf->png-1))))
+
+(defun doc-view-prepare-for-images ()
+  "Figure out the appropriate image type for the current document type"
+  (multiple-value-bind (type extension)
+		       (pcase doc-view-doc-type
+			     (`djvu (values 'tiff "tif"))
+			     (_ (values 'png "png")))
+		       (set (make-local-variable 'doc-view-image-type)
+			    type)
+		       (set (make-local-variable 'doc-view-image-file-extension)
+			    extension)))
+
 ;;;###autoload
 (defun doc-view-mode ()
   "Major mode in DocView buffers.
@@ -1549,6 +1622,8 @@ toggle between displaying the document or editing it as text.
     ;; Figure out the document type.
     (unless doc-view-doc-type
       (doc-view-set-doc-type))
+    (doc-view-set-up-single-converter)
+    (doc-view-prepare-for-images)
 
     (doc-view-make-safe-dir doc-view-cache-directory)
     ;; Handle compressed files, remote files, files inside archives
diff --git a/lisp/files.el b/lisp/files.el
index 62ad96c..c52ef28 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -2355,7 +2355,7 @@ ARC\\|ZIP\\|LZH\\|LHA\\|ZOO\\|[JEW]AR\\|XPI\\|RAR\\|7Z\\)\\'" . archive-mode)
      ("\\.\\(diffs?\\|patch\\|rej\\)\\'" . diff-mode)
      ("\\.\\(dif\\|pat\\)\\'" . diff-mode) ; for MSDOG
      ("\\.[eE]?[pP][sS]\\'" . ps-mode)
-     ("\\.\\(?:PDF\\|DVI\\|OD[FGPST]\\|DOCX?\\|XLSX?\\|PPTX?\\|pdf\\|dvi\\|od[fgpst]\\|docx?\\|xlsx?\\|pptx?\\)\\'" . doc-view-mode-maybe)
+     ("\\.\\(?:PDF\\|DVI\\|OD[FGPST]\\|DOCX?\\|XLSX?\\|PPTX?\\|pdf\\|dvi\\|djvu\\|od[fgpst]\\|docx?\\|xlsx?\\|pptx?\\)\\'" . doc-view-mode-maybe)
      ("configure\\.\\(ac\\|in\\)\\'" . autoconf-mode)
      ("\\.s\\(v\\|iv\\|ieve\\)\\'" . sieve-mode)
      ("BROWSE\\'" . ebrowse-tree-mode)
-- 
1.8.0.2