]> code.delx.au - gnu-emacs/blobdiff - lisp/doc-view.el
Add dired-hide-details-mode. (Bug#6799)
[gnu-emacs] / lisp / doc-view.el
index d16d8c6f62da7b6ba1eceaa2792423695359bfd5..89d2edd3dbb429edaa9e00d71720c59e165f94a2 100644 (file)
@@ -1,10 +1,10 @@
 ;;; doc-view.el --- View PDF/PostScript/DVI files in Emacs -*- lexical-binding: t -*-
 
 
-;; Copyright (C) 2007-2011 Free Software Foundation, Inc.
+;; Copyright (C) 2007-2013 Free Software Foundation, Inc.
 ;;
-;; Author: Tassilo Horn <tassilo@member.fsf.org>
-;; Maintainer: Tassilo Horn <tassilo@member.fsf.org>
+;; Author: Tassilo Horn <tsdh@gnu.org>
+;; Maintainer: Tassilo Horn <tsdh@gnu.org>
 ;; Keywords: files, pdf, ps, dvi
 
 ;; This file is part of GNU Emacs.
 ;; pages won't be displayed before conversion of the document finished
 ;; completely.
 ;;
-;; DocView lets you select a slice of the displayed pages.  This slice will be
-;; remembered and applied to all pages of the current document.  This enables
-;; you to cut away the margins of a document to save some space.  To select a
-;; slice you can use `doc-view-set-slice' (bound to `s s') which will query you
-;; for the coordinates of the slice's top-left corner and its width and height.
-;; A much more convenient way to do the same is offered by the command
-;; `doc-view-set-slice-using-mouse' (bound to `s m').  After invocation you
-;; only have to press mouse-1 at the top-left corner and drag it to the
-;; bottom-right corner of the desired slice.  To reset the slice use
-;; `doc-view-reset-slice' (bound to `s r').
+;; DocView lets you select a slice of the displayed pages.  This slice
+;; will be remembered and applied to all pages of the current
+;; document.  This enables you to cut away the margins of a document
+;; to save some space.  To select a slice you can use
+;; `doc-view-set-slice' (bound to `s s') which will query you for the
+;; coordinates of the slice's top-left corner and its width and
+;; height.  A much more convenient way to do the same is offered by
+;; the command `doc-view-set-slice-using-mouse' (bound to `s m').
+;; After invocation you only have to press mouse-1 at the top-left
+;; corner and drag it to the bottom-right corner of the desired slice.
+;; Even more accurate and convenient is to use
+;; `doc-view-set-slice-from-bounding-box' (bound to `s b') which uses
+;; the BoundingBox information of the current page to set an optimal
+;; slice.  To reset the slice use `doc-view-reset-slice' (bound to `s
+;; r').
 ;;
 ;; You can also search within the document.  The command `doc-view-search'
 ;; (bound to `C-s') queries for a search regexp and initializes a list of all
 ;; - share more code with image-mode.
 ;; - better menu.
 ;; - Bind slicing to a drag event.
-;; - doc-view-fit-doc-to-window and doc-view-fit-window-to-doc?
 ;; - zoom the region around the cursor (like xdvi).
 ;; - get rid of the silly arrow in the fringe.
 ;; - improve anti-aliasing (pdf-utils gets it better).
 ;; (except the tooltip) if the next match is on the same page.
 
 ;; And it's much slower than the current search facility, because
-;; isearch really searches for each step forward or backward wheras
+;; isearch really searches for each step forward or backward whereas
 ;; the current approach searches once and then it knows to which
 ;; pages to jump.
 
 
 ;;; Code:
 
-(eval-when-compile (require 'cl))
+(eval-when-compile (require 'cl-lib))
 (require 'dired)
 (require 'image-mode)
 (require 'jka-compr)
 ;;;; 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
   :type 'file
   :group 'doc-view)
 
+(defcustom doc-view-pdfdraw-program
+  (cond
+   ((executable-find "pdfdraw") "pdfdraw")
+   (t "mudraw"))
+  "Name of MuPDF's program to convert PDF files to PNG."
+  :type 'file
+  :version "24.4")
+
+(defcustom doc-view-pdf->png-converter-function
+  (if (executable-find doc-view-pdfdraw-program)
+      #'doc-view-pdf->png-converter-mupdf
+    #'doc-view-pdf->png-converter-ghostscript)
+  "Function to call to convert a PDF file into a PNG file."
+  :type '(radio
+          (function-item doc-view-pdf->png-converter-ghostscript
+                         :doc "Use ghostscript")
+          (function-item doc-view-pdf->png-converter-mupdf
+                         :doc "Use mupdf")
+          function)
+  :version "24.4")
+
 (defcustom doc-view-ghostscript-options
   '("-dSAFER" ;; Avoid security problems when rendering files from untrusted
     ;; sources.
@@ -169,9 +194,18 @@ Higher values result in larger images."
   :type 'number
   :group 'doc-view)
 
+(defcustom doc-view-scale-internally t
+  "Whether we should try to rescale images ourselves.
+If nil, the document is re-rendered every time the scaling factor is modified.
+This only has an effect if the image libraries linked with Emacs support
+scaling."
+  :type 'boolean)
+
 (defcustom doc-view-image-width 850
   "Default image width.
-Has only an effect if imagemagick support is compiled into emacs."
+Has only an effect if `doc-view-scale-internally' is non-nil and support for
+scaling is compiled into emacs."
+  :version "24.1"
   :type 'number
   :group 'doc-view)
 
@@ -197,13 +231,37 @@ If this and `doc-view-dvipdfm-program' are set,
   :type 'file
   :group 'doc-view)
 
-(defcustom doc-view-unoconv-program "unoconv"
+(define-obsolete-variable-alias 'doc-view-unoconv-program
+                                'doc-view-odf->pdf-converter-program
+                                "24.4")
+
+(defcustom doc-view-odf->pdf-converter-program
+  (cond
+   ((executable-find "soffice") "soffice")
+   ((executable-find "unoconv") "unoconv")
+   (t "soffice"))
   "Program to convert any file type readable by OpenOffice.org to PDF.
 
 Needed for viewing OpenOffice.org (and MS Office) files."
+  :version "24.4"
   :type 'file
   :group 'doc-view)
 
+(defcustom doc-view-odf->pdf-converter-function
+  (cond
+   ((string-match "unoconv\\'" doc-view-odf->pdf-converter-program)
+    #'doc-view-odf->pdf-converter-unoconv)
+   ((string-match "soffice\\'" doc-view-odf->pdf-converter-program)
+    #'doc-view-odf->pdf-converter-soffice))
+  "Function to call to convert a ODF file into a PDF file."
+  :type '(radio
+          (function-item doc-view-odf->pdf-converter-unoconv
+                         :doc "Use unoconv")
+          (function-item doc-view-odf->pdf-converter-soffice
+                         :doc "Use LibreOffice")
+          function)
+  :version "24.4")
+
 (defcustom doc-view-ps2pdf-program "ps2pdf"
   "Program to convert PS files to PDF.
 
@@ -249,20 +307,23 @@ of the page moves to the previous page."
 ;;;; Internal Variables
 
 (defun doc-view-new-window-function (winprops)
+  ;; (message "New window %s for buf %s" (car winprops) (current-buffer))
+  (cl-assert (or (eq t (car winprops))
+                 (eq (window-buffer (car winprops)) (current-buffer))))
   (let ((ol (image-mode-window-get 'overlay winprops)))
-    (when (and ol (not (overlay-buffer ol)))
-      ;; I've seen `ol' be a dead overlay.  I do not yet know how this
-      ;; happened, so maybe the bug is elsewhere, but in the mean time,
-      ;; this seems like a safe approach.
-      (setq ol nil))
     (if ol
         (progn
-          (assert (eq (overlay-buffer ol) (current-buffer)))
-          (setq ol (copy-overlay ol)))
-      (assert (not (get-char-property (point-min) 'display)))
+          (setq ol (copy-overlay ol))
+          ;; `ol' might actually be dead.
+          (move-overlay ol (point-min) (point-max)))
       (setq ol (make-overlay (point-min) (point-max) nil t))
       (overlay-put ol 'doc-view t))
     (overlay-put ol 'window (car winprops))
+    (unless (windowp (car winprops))
+      ;; It's a pseudo entry.  Let's make sure it's not displayed (the
+      ;; `window' property is only effective if its value is a window).
+      (cl-assert (eq t (car winprops)))
+      (delete-overlay ol))
     (image-mode-window-put 'overlay ol winprops)))
 
 (defvar doc-view-current-files nil
@@ -303,6 +364,19 @@ 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-function nil
+  "Function to call to convert a single page of the document to a bitmap file.
+May operate on the source document or on some intermediate (typically PDF)
+conversion of it.")
+
+(defvar-local doc-view--image-type nil
+  "The type of image in the current buffer.
+Can be `png' or `tiff'.")
+
+(defvar-local doc-view--image-file-pattern nil
+  "The `format' pattern of image file names.
+Typically \"page-%s.png\".")
+
 ;;;; DocView Keymaps
 
 (defvar doc-view-mode-map
@@ -316,6 +390,7 @@ Can be `dvi', `pdf', or `ps'.")
     (define-key map [remap forward-page]  'doc-view-next-page)
     (define-key map [remap backward-page] 'doc-view-previous-page)
     (define-key map (kbd "SPC")       'doc-view-scroll-up-or-next-page)
+    (define-key map (kbd "S-SPC")     'doc-view-scroll-down-or-previous-page)
     (define-key map (kbd "DEL")       'doc-view-scroll-down-or-previous-page)
     (define-key map (kbd "C-n")       'doc-view-next-line-or-next-page)
     (define-key map (kbd "<down>")    'doc-view-next-line-or-next-page)
@@ -338,6 +413,7 @@ Can be `dvi', `pdf', or `ps'.")
     ;; Slicing the image
     (define-key map (kbd "s s")       'doc-view-set-slice)
     (define-key map (kbd "s m")       'doc-view-set-slice-using-mouse)
+    (define-key map (kbd "s b")       'doc-view-set-slice-from-bounding-box)
     (define-key map (kbd "s r")       'doc-view-reset-slice)
     ;; Searching
     (define-key map (kbd "C-s")       'doc-view-search)
@@ -379,6 +455,7 @@ Can be `dvi', `pdf', or `ps'.")
      )
     "---"
     ["Set Slice"               doc-view-set-slice-using-mouse]
+    ["Set Slice (BoundingBox)"  doc-view-set-slice-from-bounding-box]
     ["Set Slice (manual)"      doc-view-set-slice]
     ["Reset Slice"             doc-view-reset-slice]
     "---"
@@ -439,24 +516,26 @@ 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)
-                                  (doc-view-current-cache-dir))))
+    (let ((file (expand-file-name
+                 (format doc-view--image-file-pattern page)
+                 (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-function
+                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))))
 
@@ -552,7 +631,8 @@ at the top edge of the page moves to the previous page."
   "Kill the current converter process(es)."
   (interactive)
   (while (consp doc-view-current-converter-processes)
-    (ignore-errors ;; Maybe it's dead already?
+    (ignore-errors ;; Some entries might not be processes, and maybe
+                  ;; some are dead already?
       (kill-process (pop doc-view-current-converter-processes))))
   (when doc-view-current-timer
     (cancel-timer doc-view-current-timer)
@@ -607,7 +687,8 @@ It's a subdirectory of `doc-view-cache-directory'."
     (setq doc-view-current-cache-dir
          (file-name-as-directory
           (expand-file-name
-           (concat (file-name-nondirectory doc-view-buffer-file-name)
+           (concat (subst-char-in-string ?% ?_ ;; bug#13679
+                     (file-name-nondirectory doc-view-buffer-file-name))
                    "-"
                    (let ((file doc-view-buffer-file-name))
                      (with-temp-buffer
@@ -639,14 +720,16 @@ OpenDocument format)."
                       (executable-find doc-view-dvipdf-program))
                  (and doc-view-dvipdfm-program
                       (executable-find doc-view-dvipdfm-program)))))
-       ((or (eq type 'postscript) (eq type 'ps) (eq type 'eps)
-            (eq type 'pdf))
+       ((memq type '(postscript ps eps pdf))
+        ;; FIXME: allow mupdf here
         (and doc-view-ghostscript-program
              (executable-find doc-view-ghostscript-program)))
        ((eq type 'odf)
-        (and doc-view-unoconv-program
-             (executable-find doc-view-unoconv-program)
+        (and doc-view-odf->pdf-converter-program
+             (executable-find doc-view-odf->pdf-converter-program)
              (doc-view-mode-p 'pdf)))
+       ((eq type 'djvu)
+        (executable-find "ddjvu"))
        (t ;; unknown image type
         nil))))
 
@@ -655,19 +738,22 @@ OpenDocument format)."
 (defvar doc-view-shrink-factor 1.125)
 
 (defun doc-view-enlarge (factor)
-  "Enlarge the document."
+  "Enlarge the document by FACTOR."
   (interactive (list doc-view-shrink-factor))
-  (if (eq (plist-get (cdr (doc-view-current-image)) :type)
-         'imagemagick)
-      ;; ImageMagick supports on-the-fly-rescaling
-      (progn
-       (set (make-local-variable 'doc-view-image-width)
-            (ceiling (* factor doc-view-image-width)))
-       (doc-view-insert-image (plist-get (cdr (doc-view-current-image)) :file)
-                              :width doc-view-image-width))
-    (set (make-local-variable 'doc-view-resolution)
-        (ceiling (* factor doc-view-resolution)))
-    (doc-view-reconvert-doc)))
+  (if (and doc-view-scale-internally
+           (eq (plist-get (cdr (doc-view-current-image)) :type)
+               'imagemagick))
+      ;; ImageMagick supports on-the-fly-rescaling.
+      (let ((new (ceiling (* factor doc-view-image-width))))
+        (unless (equal new doc-view-image-width)
+          (setq-local doc-view-image-width new)
+          (doc-view-insert-image
+           (plist-get (cdr (doc-view-current-image)) :file)
+           :width doc-view-image-width)))
+    (let ((new (ceiling (* factor doc-view-resolution))))
+      (unless (equal new doc-view-resolution)
+        (setq-local doc-view-resolution new)
+        (doc-view-reconvert-doc)))))
 
 (defun doc-view-shrink (factor)
   "Shrink the document."
@@ -735,12 +821,14 @@ min {(window-width / image-width), (window-height / image-height)} times."
               (img-height (cdr (image-display-size
                                 (image-get-display-property) t))))
           (doc-view-enlarge (min (/ (float win-width) (float img-width))
-                                 (/ (float (- win-height 1)) (float img-height)))))
+                                 (/ (float (- win-height 1))
+                                    (float img-height)))))
       ;; If slice is set
       (let* ((slice-width (nth 2 slice))
              (slice-height (nth 3 slice))
              (scale-factor (min (/ (float win-width) (float slice-width))
-                                (/ (float (- win-height 1)) (float slice-height))))
+                                (/ (float (- win-height 1))
+                                   (float slice-height))))
              (new-slice (mapcar (lambda (x) (ceiling (* scale-factor x))) slice)))
         (doc-view-enlarge scale-factor)
         (setf (doc-view-current-slice) new-slice)
@@ -754,6 +842,7 @@ Should be invoked when the cached images aren't up-to-date."
   ;; Clear the old cached files
   (when (file-exists-p (doc-view-current-cache-dir))
     (delete-directory (doc-view-current-cache-dir) 'recursive))
+  (kill-local-variable 'doc-view-last-page-number)
   (doc-view-initiate-display))
 
 (defun doc-view-sentinel (proc event)
@@ -800,22 +889,82 @@ Should be invoked when the cached images aren't up-to-date."
                            (list "-o" pdf dvi)
                            callback)))
 
-(defun doc-view-odf->pdf (odf callback)
+(defun doc-view-pdf->png-converter-ghostscript (pdf png page callback)
+  (doc-view-start-process
+   "pdf/ps->png" doc-view-ghostscript-program
+   `(,@doc-view-ghostscript-options
+     ,(format "-r%d" (round doc-view-resolution))
+     ,@(if page `(,(format "-dFirstPage=%d" page)))
+     ,@(if page `(,(format "-dLastPage=%d" page)))
+     ,(concat "-sOutputFile=" png)
+     ,pdf)
+   callback))
+
+(defalias 'doc-view-ps->png-converter-ghostscript
+  'doc-view-pdf->png-converter-ghostscript)
+
+(defun doc-view-djvu->tiff-converter-ddjvu (djvu tiff page callback)
+  "Convert PAGE of a DJVU file to bitmap(s) asynchronously.
+Call CALLBACK with no arguments when done.
+If PAGE is nil, convert the whole document."
+  (doc-view-start-process
+   "djvu->tiff" "ddjvu"
+   `("-format=tiff"
+     ;; ddjvu only accepts the range 1-999.
+     ,(format "-scale=%d" (round doc-view-resolution))
+     ;; -eachpage was only added after djvulibre-3.5.25.3!
+     ,@(unless page '("-eachpage"))
+     ,@(if page `(,(format "-page=%d" page)))
+     ,djvu
+     ,tiff)
+   callback))
+
+(defun doc-view-pdf->png-converter-mupdf (pdf png page callback)
+  (doc-view-start-process
+   "pdf->png" doc-view-pdfdraw-program
+   `(,(concat "-o" png)
+     ,(format "-r%d" (round doc-view-resolution))
+     ,pdf
+     ,@(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."
-  (doc-view-start-process "odf->pdf" doc-view-unoconv-program
+  (doc-view-start-process "odf->pdf" doc-view-odf->pdf-converter-program
                          (list "-f" "pdf" "-o" (doc-view-current-cache-dir) 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."
+  ;; 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
+  ;; https://bugs.freedesktop.org/show_bug.cgi?id=37531.  A workaround
+  ;; is to start soffice with a separate UserInstallation directory.
+  (let ((tmp-user-install-dir (make-temp-file "libreoffice-docview" t)))
+    (doc-view-start-process "odf->pdf" doc-view-odf->pdf-converter-program
+                           (list
+                            (concat "-env:UserInstallation=file://"
+                                    tmp-user-install-dir)
+                            "--headless" "--convert-to" "pdf"
+                            "--outdir" (doc-view-current-cache-dir) odf)
+                           (lambda ()
+                             (delete-directory tmp-user-install-dir t)
+                             (funcall callback)))))
+
 (defun doc-view-pdf/ps->png (pdf-ps png)
+  ;; FIXME: Fix name and docstring to account for djvu&tiff.
   "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))
+  (funcall
+   (pcase doc-view-doc-type
+     (`pdf doc-view-pdf->png-converter-function)
+     (`djvu #'doc-view-djvu->tiff-converter-ddjvu)
+     (_ #'doc-view-ps->png-converter-ghostscript))
+   pdf-ps png nil
    (let ((resolution doc-view-resolution))
      (lambda ()
        ;; Only create the resolution file when it's all done, so it also
@@ -828,6 +977,7 @@ is named like ODF with the extension turned to pdf."
          (cancel-timer doc-view-current-timer)
          (setq doc-view-current-timer nil))
        (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
@@ -835,25 +985,10 @@ is named like ODF with the extension turned to pdf."
                        'doc-view-display
                        (current-buffer)))))
 
-(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))
-
 (declare-function clear-image-cache "image.c" (&optional filter))
 
-(defun doc-view-pdf->png (pdf png pages)
-  "Convert a PDF file to PNG asynchronously.
+(defun doc-view-document->bitmap (pdf png pages)
+  "Convert a document file to bitmap images asynchronously.
 Start by converting PAGES, and then the rest."
   (if (null pages)
       (doc-view-pdf/ps->png pdf png)
@@ -862,11 +997,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-function
+              pdf (format png (car pages)) (car pages)
        (lambda ()
          (if rest
-             (doc-view-pdf->png pdf png rest)
+             (doc-view-document->bitmap 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
@@ -874,8 +1009,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)))))))
 
@@ -887,33 +1022,33 @@ Start by converting PAGES, and then the rest."
                           (list "-raw" pdf txt)
                           callback))
 
+(defun doc-view-current-cache-doc-pdf ()
+  "Return the name of the doc.pdf in the current cache dir.
+  This file exists only if the current document isn't a PDF or PS file already."
+  (expand-file-name "doc.pdf" (doc-view-current-cache-dir)))
+
 (defun doc-view-doc->txt (txt callback)
   "Convert the current document to text and call CALLBACK when done."
   (make-directory (doc-view-current-cache-dir) t)
-  (case doc-view-doc-type
-    (pdf
+  (pcase doc-view-doc-type
+    (`pdf
      ;; Doc is a PDF, so convert it to TXT
      (doc-view-pdf->txt doc-view-buffer-file-name txt callback))
-    (ps
+    (`ps
      ;; Doc is a PS, so convert it to PDF (which will be converted to
      ;; TXT thereafter).
-     (let ((pdf (expand-file-name "doc.pdf"
-                                 (doc-view-current-cache-dir))))
+     (let ((pdf (doc-view-current-cache-doc-pdf)))
        (doc-view-ps->pdf doc-view-buffer-file-name pdf
                          (lambda () (doc-view-pdf->txt pdf txt callback)))))
-    (dvi
+    (`dvi
      ;; Doc is a DVI.  This means that a doc.pdf already exists in its
      ;; cache subdirectory.
-     (doc-view-pdf->txt (expand-file-name "doc.pdf"
-                                          (doc-view-current-cache-dir))
-                        txt callback))
-    (odf
+     (doc-view-pdf->txt (doc-view-current-cache-doc-pdf) txt callback))
+    (`odf
      ;; Doc is some ODF (or MS Office) doc.  This means that a doc.pdf
      ;; already exists in its cache subdirectory.
-     (doc-view-pdf->txt (expand-file-name "doc.pdf"
-                                          (doc-view-current-cache-dir))
-                        txt callback))
-    (t (error "DocView doesn't know what to do"))))
+     (doc-view-pdf->txt (doc-view-current-cache-doc-pdf) txt callback))
+    (_ (error "DocView doesn't know what to do"))))
 
 (defun doc-view-ps->pdf (ps pdf callback)
   "Convert PS to PDF asynchronously and call CALLBACK when finished."
@@ -945,39 +1080,39 @@ 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"
-                                    (doc-view-current-cache-dir))))
+  (let ((png-file (expand-file-name
+                   (format doc-view--image-file-pattern "%d")
+                   (doc-view-current-cache-dir))))
     (make-directory (doc-view-current-cache-dir) t)
-    (case doc-view-doc-type
-      (dvi
+    (pcase doc-view-doc-type
+      (`dvi
        ;; DVI files have to be converted to PDF before Ghostscript can process
        ;; it.
-       (let ((pdf (expand-file-name "doc.pdf" doc-view-current-cache-dir)))
+       (let ((pdf (doc-view-current-cache-doc-pdf)))
          (doc-view-dvi->pdf doc-view-buffer-file-name pdf
                             (lambda () (doc-view-pdf/ps->png pdf png-file)))))
-      (odf
+      (`odf
        ;; ODF files have to be converted to PDF before Ghostscript can
        ;; process it.
-       (lexical-let
-           ((pdf (expand-file-name "doc.pdf" doc-view-current-cache-dir))
-           (opdf (expand-file-name (concat (file-name-sans-extension
-                                            (file-name-nondirectory doc-view-buffer-file-name))
-                                           ".pdf")
-                                   doc-view-current-cache-dir))
-            (png-file png-file))
-        ;; The unoconv tool only supports a output directory, but no
+       (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.
-         (doc-view-odf->pdf doc-view-buffer-file-name
+         (funcall doc-view-odf->pdf-converter-function doc-view-buffer-file-name
                             (lambda ()
                              ;; Rename to doc.pdf
                              (rename-file opdf pdf)
                              (doc-view-pdf/ps->png pdf png-file)))))
-      (pdf
+      ((or `pdf `djvu)
        (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)))
-      (t
+         ;; Convert doc to bitmap images starting with the active pages.
+         (doc-view-document->bitmap doc-view-buffer-file-name png-file pages)))
+      (_
        ;; Convert to PNG images.
        (doc-view-pdf/ps->png doc-view-buffer-file-name png-file)))))
 
@@ -991,8 +1126,9 @@ You can use this function to tell doc-view not to display the
 margins of the document.  It prompts for the top-left corner (X
 and Y) of the slice to display and its WIDTH and HEIGHT.
 
-See `doc-view-set-slice-using-mouse' for a more convenient way to
-do that.  To reset the slice use `doc-view-reset-slice'."
+See `doc-view-set-slice-using-mouse' and
+`doc-view-set-slice-from-bounding-box' for more convenient ways
+to do that.  To reset the slice use `doc-view-reset-slice'."
   (interactive
    (let* ((size (image-size (doc-view-current-image) t))
          (a (read-number (format "Top-left X (0..%d): " (car size))))
@@ -1023,6 +1159,83 @@ dragging it to its bottom-right corner.  See also
          (setq done t))))
     (doc-view-set-slice x y w h)))
 
+(defun doc-view-get-bounding-box ()
+  "Get the BoundingBox information of the current page."
+  (let* ((page (doc-view-current-page))
+        (doc (let ((cache-doc (doc-view-current-cache-doc-pdf)))
+               (if (file-exists-p cache-doc)
+                   cache-doc
+                 doc-view-buffer-file-name)))
+        (o (shell-command-to-string
+            (concat doc-view-ghostscript-program
+                    " -dSAFER -dBATCH -dNOPAUSE -q -sDEVICE=bbox "
+                    (format "-dFirstPage=%s -dLastPage=%s %s"
+                            page page doc)))))
+    (save-match-data
+      (when (string-match (concat "%%BoundingBox: "
+                                 "\\([[:digit:]]+\\) \\([[:digit:]]+\\) "
+                                 "\\([[:digit:]]+\\) \\([[:digit:]]+\\)") o)
+       (mapcar #'string-to-number
+               (list (match-string 1 o)
+                     (match-string 2 o)
+                     (match-string 3 o)
+                     (match-string 4 o)))))))
+
+(defvar doc-view-paper-sizes
+  '((a4 595 842)
+    (a4-landscape 842 595)
+    (letter 612 792)
+    (letter-landscape 792 612)
+    (legal 612 1008)
+    (legal-landscape 1008 612)
+    (a3 842 1191)
+    (a3-landscape 1191 842)
+    (tabloid 792 1224)
+    (ledger 1224 792))
+  "An alist from paper size names to dimensions.")
+
+(defun doc-view-guess-paper-size (iw ih)
+  "Guess the paper size according to the aspect ratio."
+  (cl-labels ((div (x y)
+                  (round (/ (* 100.0 x) y))))
+    (let ((ar (div iw ih))
+         (al (mapcar (lambda (l)
+                       (list (div (nth 1 l) (nth 2 l)) (car l)))
+                     doc-view-paper-sizes)))
+      (cadr (assoc ar al)))))
+
+(defun doc-view-scale-bounding-box (ps iw ih bb)
+  (list (/ (* (nth 0 bb) iw) (nth 1 (assoc ps doc-view-paper-sizes)))
+       (/ (* (nth 1 bb) ih) (nth 2 (assoc ps doc-view-paper-sizes)))
+       (/ (* (nth 2 bb) iw) (nth 1 (assoc ps doc-view-paper-sizes)))
+       (/ (* (nth 3 bb) ih) (nth 2 (assoc ps doc-view-paper-sizes)))))
+
+(defun doc-view-set-slice-from-bounding-box (&optional force-paper-size)
+  "Set the slice from the document's BoundingBox information.
+The result is that the margins are almost completely cropped,
+much more accurate than could be done manually using
+`doc-view-set-slice-using-mouse'."
+  (interactive "P")
+  (let ((bb (doc-view-get-bounding-box)))
+    (if (not bb)
+       (message "BoundingBox couldn't be determined")
+      (let* ((is (image-size (doc-view-current-image) t))
+            (iw (car is))
+            (ih (cdr is))
+            (ps (or (and (null force-paper-size)
+                          (doc-view-guess-paper-size iw ih))
+                    (intern (completing-read "Paper size: "
+                                              doc-view-paper-sizes
+                                             nil t))))
+            (bb (doc-view-scale-bounding-box ps iw ih bb))
+            (x1 (nth 0 bb))
+            (y1 (nth 1 bb))
+            (x2 (nth 2 bb))
+            (y2 (nth 3 bb)))
+       ;; We keep a 2 pixel margin.
+       (doc-view-set-slice (- x1 2) (- ih y2 2)
+                           (+ (- x2 x1) 4) (+ (- y2 y1) 4))))))
+
 (defun doc-view-reset-slice ()
   "Reset the current slice.
 After calling this function whole pages will be visible again."
@@ -1041,10 +1254,11 @@ ARGS is a list of image descriptors."
     (setq doc-view-pending-cache-flush nil))
   (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)
+                  (if (not (and doc-view-scale-internally
+                                 (fboundp 'imagemagick-types)))
+                      (apply 'create-image file doc-view--image-type nil args)
                     (unless (member :width args)
-                      (setq args (append args (list :width doc-view-image-width))))
+                      (setq args `(,@args :width ,doc-view-image-width)))
                     (apply 'create-image file 'imagemagick nil args))))
         (slice (doc-view-current-slice)))
     (setf (doc-view-current-image) image)
@@ -1092,19 +1306,26 @@ 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)
+                                   (format doc-view--image-file-pattern
+                                           "[0-9]+")
+                                   t)
                   'doc-view-sort))
+      (unless (eq (length prev-pages) (length doc-view-current-files))
+       (force-mode-line-update))
       (dolist (win (or (get-buffer-window-list buffer nil t)
-                      (list (selected-window))))
+                      (list t)))
        (let* ((page (doc-view-current-page win))
-              (pagefile (expand-file-name (format "page-%d.png" page)
-                                          (doc-view-current-cache-dir))))
+              (pagefile (expand-file-name
+                          (format doc-view--image-file-pattern page)
+                          (doc-view-current-cache-dir))))
          (when (or force
                    (and (not (member pagefile prev-pages))
                         (member pagefile doc-view-current-files)))
-           (with-selected-window win
-                                 (assert (eq (current-buffer) buffer))
-                                 (doc-view-goto-page page))))))))
+           (if (windowp win)
+               (with-selected-window win
+                 (cl-assert (eq (current-buffer) buffer) t)
+                 (doc-view-goto-page page))
+             (doc-view-goto-page page))))))))
 
 (defun doc-view-buffer-message ()
   ;; Only show this message initially, not when refreshing the buffer (in which
@@ -1148,6 +1369,10 @@ For now these keys are useful:
 
 ;;;;; Toggle between editing and viewing
 
+(defvar-local doc-view-saved-settings nil
+  "Doc-view settings saved while in some other mode.")
+(put 'doc-view-saved-settings 'permanent-local t)
+
 (defun doc-view-toggle-display ()
   "Toggle between editing a document as text or viewing it."
   (interactive)
@@ -1157,7 +1382,7 @@ For now these keys are useful:
        (doc-view-kill-proc)
        (setq buffer-read-only nil)
        (remove-overlays (point-min) (point-max) 'doc-view t)
-       (set (make-local-variable 'image-mode-winprops-alist) t)
+       (setq-local image-mode-winprops-alist t)
        ;; Switch to the previously used major mode or fall back to
        ;; normal mode.
        (doc-view-fallback-mode)
@@ -1285,12 +1510,13 @@ If BACKWARD is non-nil, jump to the previous match."
        ;; the conversion is incomplete.
        (file-readable-p (expand-file-name "resolution.el"
                                           (doc-view-current-cache-dir)))
-       (> (length (directory-files (doc-view-current-cache-dir)
-                                   nil "\\.png\\'"))
+       (> (length (directory-files
+                   (doc-view-current-cache-dir)
+                   nil (format doc-view--image-file-pattern "[0-9]+")))
           0)))
 
 (defun doc-view-initiate-display ()
-  ;; Switch to image display if possible
+  ;; Switch to image display if possible.
   (if (doc-view-mode-p doc-view-doc-type)
       (progn
        (doc-view-buffer-message)
@@ -1298,7 +1524,7 @@ If BACKWARD is non-nil, jump to the previous match."
        (if (doc-view-already-converted-p)
            (progn
              (message "DocView: using cached files!")
-             ;; Load the saved resolution
+             ;; Load the saved resolution.
              (let* ((res-file (expand-file-name "resolution.el"
                                                  (doc-view-current-cache-dir)))
                      (res
@@ -1307,7 +1533,7 @@ If BACKWARD is non-nil, jump to the previous match."
                           (insert-file-contents res-file)
                           (read (current-buffer))))))
                 (when (numberp res)
-                 (set (make-local-variable 'doc-view-resolution) res)))
+                 (setq-local doc-view-resolution res)))
              (doc-view-display (current-buffer) 'force))
          (doc-view-convert-current-doc))
        (message
@@ -1359,6 +1585,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)
@@ -1373,14 +1601,25 @@ If BACKWARD is non-nil, jump to the previous match."
           (cond
            ((looking-at "%!") '(ps))
            ((looking-at "%PDF") '(pdf))
-           ((looking-at "\367\002") '(dvi))))))
-    (set (make-local-variable 'doc-view-doc-type)
-        (car (or (doc-view-intersection name-types content-types)
-                 (when (and name-types content-types)
-                   (error "Conflicting types: name says %s but content says %s"
-                          name-types content-types))
-                 name-types content-types
-                 (error "Cannot determine the document type"))))))
+           ((looking-at "\367\002") '(dvi))
+           ((looking-at "AT&TFORM") '(djvu))))))
+    (setq-local doc-view-doc-type
+        (car (or (doc-view-intersection name-types content-types)
+                 (when (and name-types content-types)
+                   (error "Conflicting types: name says %s but content says %s"
+                          name-types content-types))
+                 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"
+  (pcase-let ((`(,conv-function ,type ,extension)
+               (pcase doc-view-doc-type
+                 (`djvu (list #'doc-view-djvu->tiff-converter-ddjvu 'tiff "tif"))
+                 (_     (list doc-view-pdf->png-converter-function  'png  "png")))))
+    (setq-local doc-view-single-page-converter-function conv-function)
+    (setq-local doc-view--image-type type)
+    (setq-local doc-view--image-file-pattern (concat "page-%s." extension))))
 
 ;;;###autoload
 (defun doc-view-mode ()
@@ -1400,21 +1639,24 @@ toggle between displaying the document or editing it as text.
       ;; returns nil for tar members.
       (doc-view-fallback-mode)
 
-    (let* ((prev-major-mode (if (eq major-mode 'doc-view-mode)
+    (let* ((prev-major-mode (if (derived-mode-p 'doc-view-mode)
                                doc-view-previous-major-mode
-                             (when (not (memq major-mode
-                                              '(doc-view-mode fundamental-mode)))
+                             (unless (eq major-mode 'fundamental-mode)
                                major-mode))))
       (kill-all-local-variables)
-      (set (make-local-variable 'doc-view-previous-major-mode) prev-major-mode))
+      (setq-local doc-view-previous-major-mode prev-major-mode))
+
+    (dolist (var doc-view-saved-settings)
+      (set (make-local-variable (car var)) (cdr var)))
 
     ;; Figure out the document type.
     (unless doc-view-doc-type
       (doc-view-set-doc-type))
+    (doc-view-set-up-single-converter)
 
     (doc-view-make-safe-dir doc-view-cache-directory)
     ;; Handle compressed files, remote files, files inside archives
-    (set (make-local-variable 'doc-view-buffer-file-name)
+    (setq-local doc-view-buffer-file-name
         (cond
          (jka-compr-really-do-compress
            ;; FIXME: there's a risk of name conflicts here.
@@ -1453,20 +1695,19 @@ toggle between displaying the document or editing it as text.
              'doc-view-new-window-function nil t)
     (image-mode-setup-winprops)
 
-    (set (make-local-variable 'mode-line-position)
-        '(" P" (:eval (number-to-string (doc-view-current-page)))
-          "/" (:eval (number-to-string (doc-view-last-page-number)))))
+    (setq-local mode-line-position
+                '(" P" (:eval (number-to-string (doc-view-current-page)))
+                  "/" (:eval (number-to-string (doc-view-last-page-number)))))
     ;; Don't scroll unless the user specifically asked for it.
-    (set (make-local-variable 'auto-hscroll-mode) nil)
-    (set (make-local-variable 'mwheel-scroll-up-function)
-        'doc-view-scroll-up-or-next-page)
-    (set (make-local-variable 'mwheel-scroll-down-function)
-        'doc-view-scroll-down-or-previous-page)
-    (set (make-local-variable 'cursor-type) nil)
+    (setq-local auto-hscroll-mode nil)
+    (setq-local mwheel-scroll-up-function #'doc-view-scroll-up-or-next-page)
+    (setq-local mwheel-scroll-down-function
+                #'doc-view-scroll-down-or-previous-page)
+    (setq-local cursor-type nil)
     (use-local-map doc-view-mode-map)
-    (set (make-local-variable 'after-revert-hook) 'doc-view-reconvert-doc)
-    (set (make-local-variable 'bookmark-make-record-function)
-        'doc-view-bookmark-make-record)
+    (add-hook 'after-revert-hook 'doc-view-reconvert-doc nil t)
+    (setq-local bookmark-make-record-function
+                #'doc-view-bookmark-make-record)
     (setq mode-name "DocView"
          buffer-read-only t
          major-mode 'doc-view-mode)
@@ -1475,18 +1716,25 @@ toggle between displaying the document or editing it as text.
     ;; canonical view mode for PDF/PS/DVI files.  This could be
     ;; switched on automatically depending on the value of
     ;; `view-read-only'.
-    (set (make-local-variable 'view-read-only) nil)
+    (setq-local view-read-only nil)
     (run-mode-hooks 'doc-view-mode-hook)))
 
 (defun doc-view-fallback-mode ()
   "Fallback to the previous or next best major mode."
-  (if doc-view-previous-major-mode
-      (funcall doc-view-previous-major-mode)
-    (let ((auto-mode-alist (rassq-delete-all
-                           'doc-view-mode-maybe
-                           (rassq-delete-all 'doc-view-mode
-                                             (copy-alist auto-mode-alist)))))
-      (normal-mode))))
+  (let ((vars (if (derived-mode-p 'doc-view-mode)
+                  (mapcar (lambda (var) (cons var (symbol-value var)))
+                          '(doc-view-resolution
+                            image-mode-winprops-alist)))))
+    (if doc-view-previous-major-mode
+        (funcall doc-view-previous-major-mode)
+      (let ((auto-mode-alist
+             (rassq-delete-all
+              'doc-view-mode-maybe
+              (rassq-delete-all 'doc-view-mode
+                                (copy-alist auto-mode-alist)))))
+        (normal-mode)))
+    (when vars
+      (setq-local doc-view-saved-settings vars))))
 
 ;;;###autoload
 (defun doc-view-mode-maybe ()