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

From: Elias Pipping <[email-masked]>
Date: Sun, 02 Sep 2012 13:27:14 +0200

Package: emacs
Severity: wishlist

[resent from
http://lists.gnu.org/archive/html/emacs-devel/2012-09/msg00029.html ]

Hello,

I sent a patch to this list last year that made it possible to use
mupdf's "mudraw" instead of ghostscript's "gs" to convert pdf documents
into png files within doc-view-mode.

I've since been testing that patch and never looked back since the
switch sped up the pdf->png conversion by a factor of two and brought
anti-aliasing with it. 

In a similar fashion, I've now added support for DjVu->Tiff conversion
to doc-view-mode through djvulibre. While djvulibre's "ddjvu" has always
allowed for such conversion, I only yesterday requested[1] the necessary
feature of creating one tiff file per page, which was immediately
implemented (Mr. Bottou, if you're reading this, thanks again!).

Consequently, to have the two patches I'm attaching work as expected,
one needs the latest version of djvulibre from

  git://djvu.git.sourceforge.net/gitroot/djvu/djvulibre

Best regards,

Elias Pipping

[1] http://sourceforge.net/tracker/?func=detail&atid=406586&aid=3564062&group_id=32953

PS: I've configured emacs with --without-imagemagick. I did not test
what happens if imagemagick is used.

PPS: If you need PDF and DjVu files to toy around with, I suggest the
ones at http://projecteuclid.org/euclid.pjm/1102986512 since they also
make a nice read.

PPPS: The first diff is against vanilla emacs 24.2.

>>From c46da5f8237c3c658e011e35faddb300ea553059 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 53e7811..05f7422 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -154,6 +154,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.
@@ -643,6 +663,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)
@@ -802,6 +823,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
@@ -812,12 +852,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
@@ -829,7 +871,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
@@ -840,17 +882,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.7.12

>>From 6397e600d7e259450bdacddaad02622bdda9151e 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 | 123 +++++++++++++++++++++++++++++++++++++++++++------------
 lisp/files.el    |   2 +-
 2 files changed, 98 insertions(+), 27 deletions(-)

diff --git a/lisp/doc-view.el b/lisp/doc-view.el
index 05f7422..a4c0f02 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -141,7 +141,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
@@ -167,6 +167,13 @@
                 function)
   :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"
@@ -325,6 +332,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
@@ -461,24 +479,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))))
 
@@ -670,6 +689,8 @@ OpenDocument format)."
 	 (and doc-view-unoconv-program
 	      (executable-find doc-view-unoconv-program)
 	      (doc-view-mode-p 'pdf)))
+	((eq type 'djvu)
+	 (executable-find "ddjvu"))
 	(t ;; unknown image type
 	 nil))))
 
@@ -835,6 +856,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 . "ddjvu")
+    (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)
@@ -855,6 +886,8 @@ is named like ODF with the extension turned to pdf."
   (let ((invocation (case doc-view-doc-type
 		      (pdf (funcall doc-view-pdf->png-converter-invocation
 				    (round doc-view-resolution) pdf-ps png))
+		      (djvu (funcall doc-view-djvu->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
@@ -889,9 +922,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 single-page-converter)
   "Convert a PDF file to PNG asynchronously.
 Start by converting PAGES, and then the rest."
   (if (null pages)
@@ -901,11 +944,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 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
@@ -913,8 +956,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)))))))
 
@@ -984,7 +1027,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)
     (case doc-view-doc-type
@@ -1015,7 +1058,12 @@ 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
+				 'doc-view-pdf->png-1)))
+      (djvu
+       (let ((pages (doc-view-active-pages)))
+	 (doc-view-document->png doc-view-buffer-file-name png-file pages
+				 'doc-view-djvu->png-1)))
       (t
        ;; Convert to PNG images.
        (doc-view-pdf/ps->png doc-view-buffer-file-name png-file)))))
@@ -1081,7 +1129,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))))
@@ -1131,12 +1179,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 (selected-window))))
 	(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))
@@ -1325,7 +1373,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 ()
@@ -1398,6 +1446,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)
@@ -1412,7 +1462,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)
@@ -1421,6 +1472,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)
+       (case doc-view-doc-type
+	     (djvu 'doc-view-djvu->png-1)
+	     (t '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)
+		       (case doc-view-doc-type
+			     (djvu (values 'tiff "tif"))
+			     (t (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.
@@ -1450,6 +1519,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 5f83639..eae5611 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -2375,7 +2375,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.7.12