;;; doc-view.el --- View PDF/PostScript/DVI files in Emacs
-;; Copyright (C) 2007, 2008 Free Software Foundation, Inc.
+;; Copyright (C) 2007, 2008, 2009 Free Software Foundation, Inc.
;;
;; Author: Tassilo Horn <tassilo@member.fsf.org>
;; Maintainer: Tassilo Horn <tassilo@member.fsf.org>
;; This file is part of GNU Emacs.
-;; GNU Emacs is free software; you can redistribute it and/or modify
+;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation; either version 3, or (at your option)
-;; any later version.
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
-;; along with GNU Emacs; see the file COPYING. If not, write to the
-;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
-;; Boston, MA 02110-1301, USA.
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Requirements:
;; doc-view.el requires GNU Emacs 22.1 or newer. You also need Ghostscript,
-;; `dvipdfm' which comes with teTeX and `pdftotext', which comes with xpdf
-;; (http://www.foolabs.com/xpdf/) or poppler (http://poppler.freedesktop.org/).
+;; `dvipdf' (comes with Ghostscript) or `dvipdfm' (comes with teTeX or TeXLive)
+;; and `pdftotext', which comes with xpdf (http://www.foolabs.com/xpdf/) or
+;; poppler (http://poppler.freedesktop.org/).
;;; Commentary:
;; 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 invokation you
+;; `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').
;; `C-r' you can do the same, but backwards. To search for a new regexp give a
;; prefix arg to one of the search functions, e.g. by typing `C-u C-s'. The
;; searching works by using a plain text representation of the document. If
-;; that doesn't already exist the first invokation of `doc-view-search' (or
+;; that doesn't already exist the first invocation of `doc-view-search' (or
;; `doc-view-search-backward') starts the conversion. When that finishes and
;; you're still viewing the document (i.e. you didn't switch to another buffer)
;; you're queried for the regexp then.
;;; Todo:
+;; - add print command.
+;; - share more code with image-mode.
;; - better menu.
-;; - don't use `find-file'.
;; - Bind slicing to a drag event.
-;; - doc-view-fit-doc-to-window and doc-view-fit-window-to-doc.
-;; - zoom a the region around the cursor (like xdvi).
+;; - 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).
;;; Code:
+(eval-when-compile (require 'cl))
(require 'dired)
(require 'image-mode)
(require 'jka-compr)
"Program to convert DVI files to PDF.
DVI file will be converted to PDF before the resulting PDF is
-converted to PNG."
+converted to PNG.
+
+If this and `doc-view-dvipdf-program' are set,
+`doc-view-dvipdf-program' will be preferred."
+ :type 'file
+ :group 'doc-view)
+
+(defcustom doc-view-dvipdf-program (executable-find "dvipdf")
+ "Program to convert DVI files to PDF.
+
+DVI file will be converted to PDF before the resulting PDF is
+converted to PNG.
+
+If this and `doc-view-dvipdfm-program' are set,
+`doc-view-dvipdf-program' will be preferred."
:type 'file
:group 'doc-view)
:type 'directory
:group 'doc-view)
-(defcustom doc-view-conversion-buffer "*doc-view conversion output*"
- "The buffer where messages from the converter programs go to."
- :type 'string
- :group 'doc-view)
+(defvar doc-view-conversion-buffer " *doc-view conversion output*"
+ "The buffer where messages from the converter programs go to.")
-(defcustom doc-view-conversion-refresh-interval 3
+(defcustom doc-view-conversion-refresh-interval 1
"Interval in seconds between refreshes of the DocView buffer while converting.
After such a refresh newly converted pages will be available for
viewing. If set to nil there won't be any refreshes and the
;;;; Internal Variables
-(defvar doc-view-current-files nil
- "Only used internally.")
+(defun doc-view-new-window-function (winprops)
+ (let ((ol (image-mode-window-get 'overlay winprops)))
+ (if ol
+ (setq ol (copy-overlay ol))
+ (assert (not (get-char-property (point-min) 'display)))
+ (setq ol (make-overlay (point-min) (point-max) nil t))
+ (overlay-put ol 'doc-view t))
+ (overlay-put ol 'window (car winprops))
+ (image-mode-window-put 'overlay ol winprops)))
-(defvar doc-view-current-page nil
+(defvar doc-view-current-files nil
"Only used internally.")
+(make-variable-buffer-local 'doc-view-current-files)
-(defvar doc-view-current-converter-process nil
+(defvar doc-view-current-converter-processes nil
"Only used internally.")
+(make-variable-buffer-local 'doc-view-current-converter-processes)
(defvar doc-view-current-timer nil
"Only used internally.")
-
-(defvar doc-view-current-slice nil
- "Only used internally.")
+(make-variable-buffer-local 'doc-view-current-timer)
(defvar doc-view-current-cache-dir nil
"Only used internally.")
+(make-variable-buffer-local 'doc-view-current-cache-dir)
(defvar doc-view-current-search-matches nil
"Only used internally.")
-
-(defvar doc-view-current-image nil
- "Only used internally.")
-
-(defvar doc-view-current-overlay nil
- "Only used internally.")
+(make-variable-buffer-local 'doc-view-current-search-matches)
(defvar doc-view-pending-cache-flush nil
"Only used internally.")
-(defvar doc-view-current-info nil
- "Only used internally.")
-
(defvar doc-view-previous-major-mode nil
"Only used internally.")
+(defvar doc-view-buffer-file-name nil
+ "Only used internally.
+The file name used for conversion. Normally it's the same as
+`buffer-file-name', but for remote files, compressed files and
+files inside an archive it is a temporary copy of
+the (uncompressed, extracted) file residing in
+`doc-view-cache-directory'.")
+
+(defvar doc-view-doc-type nil
+ "The type of document in the current buffer.
+Can be `dvi', `pdf', or `ps'.")
+
;;;; DocView Keymaps
(defvar doc-view-mode-map
(let ((map (make-sparse-keymap)))
- (suppress-keymap map)
+ (set-keymap-parent map image-mode-map)
;; Navigation in the document
(define-key map (kbd "n") 'doc-view-next-page)
(define-key map (kbd "p") 'doc-view-previous-page)
(define-key map (kbd "M-<") 'doc-view-first-page)
(define-key map (kbd "M->") 'doc-view-last-page)
(define-key map [remap goto-line] 'doc-view-goto-page)
- (define-key map [remap scroll-up] 'image-scroll-up)
- (define-key map [remap scroll-down] 'image-scroll-down)
+ (define-key map (kbd "RET") 'image-next-line)
;; Zoom in/out.
(define-key map "+" 'doc-view-enlarge)
(define-key map "-" 'doc-view-shrink)
- ;; Killing/burying the buffer (and the process)
- (define-key map (kbd "q") 'bury-buffer)
+ ;; Killing the buffer (and the process)
(define-key map (kbd "k") 'doc-view-kill-proc-and-buffer)
(define-key map (kbd "K") 'doc-view-kill-proc)
;; Slicing the image
(define-key map (kbd "C-s") 'doc-view-search)
(define-key map (kbd "<find>") 'doc-view-search)
(define-key map (kbd "C-r") 'doc-view-search-backward)
- ;; Scrolling
- (define-key map [remap forward-char] 'image-forward-hscroll)
- (define-key map [remap backward-char] 'image-backward-hscroll)
- (define-key map [remap next-line] 'image-next-line)
- (define-key map [remap previous-line] 'image-previous-line)
;; Show the tooltip
(define-key map (kbd "C-t") 'doc-view-show-tooltip)
;; Toggle between text and image display or editing
(define-key map (kbd "C-c C-c") 'doc-view-toggle-display)
+ ;; Open a new buffer with doc's text contents
+ (define-key map (kbd "C-c C-t") 'doc-view-open-text)
;; Reconvert the current document
(define-key map (kbd "g") 'revert-buffer)
(define-key map (kbd "r") 'revert-buffer)
;;;; Navigation Commands
+(defmacro doc-view-current-page (&optional win)
+ `(image-mode-window-get 'page ,win))
+(defmacro doc-view-current-info () `(image-mode-window-get 'info))
+(defmacro doc-view-current-overlay () `(image-mode-window-get 'overlay))
+(defmacro doc-view-current-image () `(image-mode-window-get 'image))
+(defmacro doc-view-current-slice () `(image-mode-window-get 'slice))
+
(defun doc-view-goto-page (page)
"View the page given by PAGE."
(interactive "nPage: ")
- (let ((len (length doc-view-current-files)))
+ (let ((len (length doc-view-current-files))
+ (hscroll (window-hscroll)))
(if (< page 1)
(setq page 1)
- (when (> page len)
+ (when (and (> page len)
+ ;; As long as the converter is running, we don't know
+ ;; how many pages will be available.
+ (null doc-view-current-converter-processes))
(setq page len)))
- (setq doc-view-current-page page
- doc-view-current-info
+ (setf (doc-view-current-page) page
+ (doc-view-current-info)
(concat
(propertize
- (format "Page %d of %d."
- doc-view-current-page
- len) 'face 'bold)
+ (format "Page %d of %d." page len) 'face 'bold)
;; Tell user if converting isn't finished yet
- (if doc-view-current-converter-process
+ (if doc-view-current-converter-processes
" (still converting...)\n"
"\n")
;; Display context infos if this page matches the last search
(when (and doc-view-current-search-matches
- (assq doc-view-current-page
- doc-view-current-search-matches))
+ (assq page doc-view-current-search-matches))
(concat (propertize "Search matches:\n" 'face 'bold)
(let ((contexts ""))
- (dolist (m (cdr (assq doc-view-current-page
+ (dolist (m (cdr (assq page
doc-view-current-search-matches)))
(setq contexts (concat contexts " - \"" m "\"\n")))
contexts)))))
;; Update the buffer
- (doc-view-insert-image (nth (1- page) doc-view-current-files)
- :pointer 'arrow)
- (overlay-put doc-view-current-overlay 'help-echo doc-view-current-info)
- (goto-char (point-min))
- ;; This seems to be needed for set-window-hscroll (in
- ;; image-forward-hscroll) to do something useful, I don't have time to
- ;; debug this now. :-( --Stef
- (forward-char)))
+ ;; 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))))
+ (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
+ (lexical-let ((page page)
+ (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)
+ (with-selected-window win
+ (doc-view-goto-page page))))))))
+ (overlay-put (doc-view-current-overlay)
+ 'help-echo (doc-view-current-info))))
(defun doc-view-next-page (&optional arg)
"Browse ARG pages forward."
(interactive "p")
- (doc-view-goto-page (+ doc-view-current-page (or arg 1))))
+ (doc-view-goto-page (+ (doc-view-current-page) (or arg 1))))
(defun doc-view-previous-page (&optional arg)
"Browse ARG pages backward."
(interactive "p")
- (doc-view-goto-page (- doc-view-current-page (or arg 1))))
+ (doc-view-goto-page (- (doc-view-current-page) (or arg 1))))
(defun doc-view-first-page ()
"View the first page."
(defun doc-view-scroll-up-or-next-page ()
"Scroll page up if possible, else goto next page."
(interactive)
- (when (= (window-vscroll) (image-scroll-up nil))
- (let ((cur-page doc-view-current-page))
+ (let ((hscroll (window-hscroll))
+ (cur-page (doc-view-current-page)))
+ (when (= (window-vscroll) (image-scroll-up nil))
(doc-view-next-page)
- (when (/= cur-page doc-view-current-page)
- (set-window-vscroll nil 0)))))
+ (when (/= cur-page (doc-view-current-page))
+ (image-bob)
+ (image-bol 1))
+ (set-window-hscroll (selected-window) hscroll))))
(defun doc-view-scroll-down-or-previous-page ()
"Scroll page down if possible, else goto previous page."
(interactive)
- (when (= (window-vscroll) (image-scroll-down nil))
- (let ((cur-page doc-view-current-page))
+ (let ((hscroll (window-hscroll))
+ (cur-page (doc-view-current-page)))
+ (when (= (window-vscroll) (image-scroll-down nil))
(doc-view-previous-page)
- (when (/= cur-page doc-view-current-page)
- (image-scroll-up nil)))))
+ (when (/= cur-page (doc-view-current-page))
+ (image-eob)
+ (image-bol 1))
+ (set-window-hscroll (selected-window) hscroll))))
;;;; Utility Functions
(defun doc-view-kill-proc ()
- "Kill the current converter process."
+ "Kill the current converter process(es)."
(interactive)
- (when doc-view-current-converter-process
- (kill-process doc-view-current-converter-process)
- (setq doc-view-current-converter-process nil))
+ (while doc-view-current-converter-processes
+ (ignore-errors ;; Maybe it's dead already?
+ (kill-process (pop doc-view-current-converter-processes))))
(when doc-view-current-timer
(cancel-timer doc-view-current-timer)
(setq doc-view-current-timer nil))
(setq doc-view-current-cache-dir
(file-name-as-directory
(expand-file-name
- (let ((doc buffer-file-name))
- (concat (file-name-nondirectory doc)
- "-"
- (with-temp-buffer
- (insert-file-contents-literally doc)
- (md5 (current-buffer)))))
+ (concat (file-name-nondirectory buffer-file-name)
+ "-"
+ (let ((file doc-view-buffer-file-name))
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally file)
+ (md5 (current-buffer)))))
doc-view-cache-directory)))))
(defun doc-view-remove-if (predicate list)
(cond
((eq type 'dvi)
(and (doc-view-mode-p 'pdf)
- doc-view-dvipdfm-program
- (executable-find doc-view-dvipdfm-program)))
- ((or (eq type 'postscript) (eq type 'ps)
+ (or (and doc-view-dvipdf-program
+ (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))
(and doc-view-ghostscript-program
(executable-find doc-view-ghostscript-program)))
(dired-delete-file (doc-view-current-cache-dir) 'always))
(doc-view-initiate-display))
-(defun doc-view-dvi->pdf-sentinel (proc event)
- "If DVI->PDF conversion was successful, convert the PDF to PNG now."
- (if (not (string-match "finished" event))
- (message "DocView: dvi->pdf process changed status to %s." event)
- (with-current-buffer (process-get proc 'buffer)
- (setq doc-view-current-converter-process nil
- mode-line-process nil)
- ;; Now go on converting this PDF to a set of PNG files.
- (let* ((pdf (process-get proc 'pdf-file))
- (png (expand-file-name "page-%d.png"
- (doc-view-current-cache-dir))))
- (doc-view-pdf/ps->png pdf png)))))
-
-(defun doc-view-dvi->pdf (dvi pdf)
- "Convert DVI to PDF asynchronously."
- (setq doc-view-current-converter-process
- (start-process "dvi->pdf" doc-view-conversion-buffer
- doc-view-dvipdfm-program
- "-o" pdf dvi)
- mode-line-process (list (format ":%s" doc-view-current-converter-process)))
- (set-process-sentinel doc-view-current-converter-process
- 'doc-view-dvi->pdf-sentinel)
- (process-put doc-view-current-converter-process 'buffer (current-buffer))
- (process-put doc-view-current-converter-process 'pdf-file pdf))
-
-(defun doc-view-pdf/ps->png-sentinel (proc event)
- "If PDF/PS->PNG conversion was successful, update the display."
+(defun doc-view-sentinel (proc event)
+ "Generic sentinel for doc-view conversion processes."
(if (not (string-match "finished" event))
- (message "DocView: converter process changed status to %s." event)
- (with-current-buffer (process-get proc 'buffer)
- (setq doc-view-current-converter-process nil
- mode-line-process nil)
- (when doc-view-current-timer
- (cancel-timer doc-view-current-timer)
- (setq doc-view-current-timer nil))
- ;; Yippie, finished. Update the display!
- (doc-view-display buffer-file-name 'force))))
+ (message "DocView: process %s changed status to %s."
+ (process-name proc)
+ (if (string-match "\\(.+\\)\n?\\'" event)
+ (match-string 1 event)
+ event))
+ (when (buffer-live-p (process-get proc 'buffer))
+ (with-current-buffer (process-get proc 'buffer)
+ (setq doc-view-current-converter-processes
+ (delq proc doc-view-current-converter-processes))
+ (setq mode-line-process
+ (if doc-view-current-converter-processes
+ (format ":%s" (car doc-view-current-converter-processes))))
+ (funcall (process-get proc 'callback))))))
+
+(defun doc-view-start-process (name program args callback)
+ ;; Make sure the process is started in an existing directory, (rather than
+ ;; some file-name-handler-managed dir, for example).
+ (let* ((default-directory (if (file-readable-p default-directory)
+ default-directory
+ (expand-file-name "~/")))
+ (proc (apply 'start-process name doc-view-conversion-buffer
+ program args)))
+ (push proc doc-view-current-converter-processes)
+ (setq mode-line-process (list (format ":%s" proc)))
+ (set-process-sentinel proc 'doc-view-sentinel)
+ (process-put proc 'buffer (current-buffer))
+ (process-put proc 'callback callback)))
+
+(defun doc-view-dvi->pdf (dvi pdf callback)
+ "Convert DVI to PDF asynchronously and call CALLBACK when finished."
+ ;; Prefer dvipdf over dvipdfm, because the latter has problems if the DVI
+ ;; references and includes other PS files.
+ (if (and doc-view-dvipdf-program
+ (executable-find doc-view-dvipdf-program))
+ (doc-view-start-process "dvi->pdf" doc-view-dvipdf-program
+ (list dvi pdf)
+ callback)
+ (doc-view-start-process "dvi->pdf" doc-view-dvipdfm-program
+ (list "-o" pdf dvi)
+ callback)))
+
(defun doc-view-pdf/ps->png (pdf-ps png)
"Convert PDF-PS to PNG asynchronously."
- (setq doc-view-current-converter-process
- (apply 'start-process
- (append (list "pdf/ps->png" doc-view-conversion-buffer
- doc-view-ghostscript-program)
- doc-view-ghostscript-options
- (list (format "-r%d" (round doc-view-resolution)))
- (list (concat "-sOutputFile=" png))
- (list pdf-ps)))
- mode-line-process (list (format ":%s" doc-view-current-converter-process)))
- (process-put doc-view-current-converter-process
- 'buffer (current-buffer))
- (set-process-sentinel doc-view-current-converter-process
- 'doc-view-pdf/ps->png-sentinel)
+ (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))
+ (lambda ()
+ (when doc-view-current-timer
+ (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
- (run-at-time "1 secs" doc-view-conversion-refresh-interval
- 'doc-view-display
- buffer-file-name))))
-
-(defun doc-view-pdf->txt-sentinel (proc event)
- (if (not (string-match "finished" event))
- (message "DocView: converter process changed status to %s." event)
- (let ((current-buffer (current-buffer))
- (proc-buffer (process-get proc 'buffer)))
- (with-current-buffer proc-buffer
- (setq doc-view-current-converter-process nil
- mode-line-process nil)
- ;; If the user looks at the DocView buffer where the conversion was
- ;; performed, search anew. This time it will be queried for a regexp.
- (when (eq current-buffer proc-buffer)
- (doc-view-search nil))))))
-
-(defun doc-view-pdf->txt (pdf txt)
- "Convert PDF to TXT asynchronously."
- (setq doc-view-current-converter-process
- (start-process "pdf->txt" doc-view-conversion-buffer
- doc-view-pdftotext-program "-raw"
- pdf txt)
- mode-line-process (list (format ":%s" doc-view-current-converter-process)))
- (set-process-sentinel doc-view-current-converter-process
- 'doc-view-pdf->txt-sentinel)
- (process-put doc-view-current-converter-process 'buffer (current-buffer)))
-
-(defun doc-view-ps->pdf-sentinel (proc event)
- (if (not (string-match "finished" event))
- (message "DocView: converter process changed status to %s." event)
- (with-current-buffer (process-get proc 'buffer)
- (setq doc-view-current-converter-process nil
- mode-line-process nil)
- ;; Now we can transform to plain text.
- (doc-view-pdf->txt (process-get proc 'pdf-file)
- (expand-file-name "doc.txt"
- (doc-view-current-cache-dir))))))
-
-(defun doc-view-ps->pdf (ps pdf)
- "Convert PS to PDF asynchronously."
- (setq doc-view-current-converter-process
- (start-process "ps->pdf" doc-view-conversion-buffer
- doc-view-ps2pdf-program
- ;; Avoid security problems when rendering files from
- ;; untrusted sources.
- "-dSAFER"
- ;; in-file and out-file
- ps pdf)
- mode-line-process (list (format ":%s" doc-view-current-converter-process)))
- (set-process-sentinel doc-view-current-converter-process
- 'doc-view-ps->pdf-sentinel)
- (process-put doc-view-current-converter-process 'buffer (current-buffer))
- (process-put doc-view-current-converter-process 'pdf-file pdf))
+ (run-at-time "1 secs" doc-view-conversion-refresh-interval
+ '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.
+Start by converting PAGES, and then the rest."
+ (if (null pages)
+ (doc-view-pdf/ps->png pdf png)
+ ;; We could render several `pages' with a single process if they're
+ ;; (almost) consecutive, but since in 99% of the cases, there'll be only
+ ;; a single page anyway, and of the remaining 1%, few cases will have
+ ;; consecutive pages, it's not worth the trouble.
+ (lexical-let ((pdf pdf) (png png) (rest (cdr pages)))
+ (doc-view-pdf->png-1
+ pdf (format png (car pages)) (car pages)
+ (lambda ()
+ (if rest
+ (doc-view-pdf->png pdf png rest)
+ ;; Yippie, the important pages are done, update the display.
+ (clear-image-cache)
+ ;; Convert the rest of the pages.
+ (doc-view-pdf/ps->png pdf png)))))))
+
+(defun doc-view-pdf->txt (pdf txt callback)
+ "Convert PDF to TXT asynchronously and call CALLBACK when finished."
+ (or doc-view-pdftotext-program
+ (error "You need the `pdftotext' program to convert a PDF to text"))
+ (doc-view-start-process "pdf->txt" doc-view-pdftotext-program
+ (list "-raw" pdf txt)
+ callback))
+
+(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
+ ;; Doc is a PDF, so convert it to TXT
+ (doc-view-pdf->txt doc-view-buffer-file-name txt callback))
+ (ps
+ ;; Doc is a PS, so convert it to PDF (which will be converted to
+ ;; TXT thereafter).
+ (lexical-let ((pdf (expand-file-name "doc.pdf"
+ (doc-view-current-cache-dir)))
+ (txt txt)
+ (callback callback))
+ (doc-view-ps->pdf doc-view-buffer-file-name pdf
+ (lambda () (doc-view-pdf->txt pdf txt callback)))))
+ (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))
+ (t (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."
+ (or doc-view-ps2pdf-program
+ (error "You need the `ps2pdf' program to convert PS to PDF"))
+ (doc-view-start-process "ps->pdf" doc-view-ps2pdf-program
+ (list
+ ;; Avoid security problems when rendering files from
+ ;; untrusted sources.
+ "-dSAFER"
+ ;; in-file and out-file
+ ps pdf)
+ callback))
+
+(defun doc-view-active-pages ()
+ (let ((pages ()))
+ (dolist (win (get-buffer-window-list (current-buffer) nil 'visible))
+ (let ((page (image-mode-window-get 'page win)))
+ (unless (memq page pages) (push page pages))))
+ pages))
(defun doc-view-convert-current-doc ()
- "Convert `buffer-file-name' to a set of png files, one file per page.
+ "Convert `doc-view-buffer-file-name' to a set of png files, one file per page.
Those files are saved in the directory given by the function
`doc-view-current-cache-dir'."
;; Let stale files still display while we recompute the new ones, so only
;; 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)))
+ (res-file (expand-file-name "resolution.el"
(doc-view-current-cache-dir))))
- (make-directory (doc-view-current-cache-dir))
- (if (not (string= (file-name-extension buffer-file-name) "dvi"))
- ;; Convert to PNG images.
- (doc-view-pdf/ps->png buffer-file-name png-file)
- ;; DVI files have to be converted to PDF before Ghostscript can process
- ;; it.
- (doc-view-dvi->pdf buffer-file-name
- (expand-file-name "doc.pdf"
- doc-view-current-cache-dir)))))
+ (make-directory (doc-view-current-cache-dir) t)
+ ;; Save the used resolution so that it can be restored when
+ ;; reading the cached files.
+ (let ((res doc-view-resolution))
+ (with-temp-buffer
+ (princ res (current-buffer))
+ ;; Don't use write-file, so as to avoid prompts for `require-newline',
+ ;; or for pre-existing buffers with the same name, ...
+ (write-region nil nil res-file nil 'silently)))
+ (case doc-view-doc-type
+ (dvi
+ ;; DVI 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))
+ (png-file png-file))
+ (doc-view-dvi->pdf doc-view-buffer-file-name pdf
+ (lambda () (doc-view-pdf/ps->png pdf png-file)))))
+ (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)))
+ (t
+ ;; Convert to PNG images.
+ (doc-view-pdf/ps->png doc-view-buffer-file-name png-file)))))
;;;; Slicing
+(declare-function image-size "image.c" (spec &optional pixels frame))
+
(defun doc-view-set-slice (x y width height)
"Set the slice of the images that should be displayed.
You can use this function to tell doc-view not to display the
See `doc-view-set-slice-using-mouse' for a more convenient way to
do that. To reset the slice use `doc-view-reset-slice'."
(interactive
- (let* ((size (image-size doc-view-current-image t))
+ (let* ((size (image-size (doc-view-current-image) t))
(a (read-number (format "Top-left X (0..%d): " (car size))))
(b (read-number (format "Top-left Y (0..%d): " (cdr size))))
(c (read-number (format "Width (0..%d): " (- (car size) a))))
(d (read-number (format "Height (0..%d): " (- (cdr size) b)))))
(list a b c d)))
- (setq doc-view-current-slice (list x y width height))
+ (setf (doc-view-current-slice) (list x y width height))
;; Redisplay
- (doc-view-goto-page doc-view-current-page))
+ (doc-view-goto-page (doc-view-current-page)))
(defun doc-view-set-slice-using-mouse ()
"Set the slice of the images that should be displayed.
"Reset the current slice.
After calling this function whole pages will be visible again."
(interactive)
- (setq doc-view-current-slice nil)
+ (setf (doc-view-current-slice) nil)
;; Redisplay
- (doc-view-goto-page doc-view-current-page))
+ (doc-view-goto-page (doc-view-current-page)))
;;;; Display
(when doc-view-pending-cache-flush
(clear-image-cache)
(setq doc-view-pending-cache-flush nil))
- (let ((image (apply 'create-image file 'png nil args)))
- (setq doc-view-current-image image)
- (move-overlay doc-view-current-overlay (point-min) (point-max))
- (overlay-put doc-view-current-overlay 'display
- (if doc-view-current-slice
- (list (cons 'slice doc-view-current-slice) image)
- image))))
+ (let ((ol (doc-view-current-overlay))
+ (image (if (and file (file-readable-p file))
+ (apply 'create-image file 'png nil args)))
+ (slice (doc-view-current-slice)))
+ (setf (doc-view-current-image) image)
+ (move-overlay ol (point-min) (point-max))
+ (overlay-put ol 'display
+ (cond
+ (image
+ (if slice
+ (list (cons 'slice slice) image)
+ image))
+ ;; We're trying to display a page that doesn't exist.
+ (doc-view-current-converter-processes
+ ;; Maybe the page doesn't exist *yet*.
+ "Cannot display this page (yet)!")
+ (t
+ ;; Typically happens if the conversion process somehow
+ ;; failed. Better not signal an error here because it
+ ;; could prevent a subsequent reconversion from fixing
+ ;; the problem.
+ (concat "Cannot display this page!\n"
+ "Maybe because of a conversion failure!"))))
+ (let ((win (overlay-get ol 'window)))
+ (if (stringp (overlay-get ol 'display))
+ (progn ;Make sure the text is not scrolled out of view.
+ (set-window-hscroll win 0)
+ (set-window-vscroll win 0))
+ (let ((hscroll (image-mode-window-get 'hscroll win))
+ (vscroll (image-mode-window-get 'vscroll win)))
+ ;; Reset scroll settings, in case they were changed.
+ (if hscroll (set-window-hscroll win hscroll))
+ (if vscroll (set-window-vscroll win vscroll)))))))
(defun doc-view-sort (a b)
"Return non-nil if A should be sorted before B.
(and (= (length a) (length b))
(string< a b))))
-(defun doc-view-display (doc &optional force)
- "Start viewing the document DOC.
+(defun doc-view-display (buffer &optional force)
+ "Start viewing the document in BUFFER.
If FORCE is non-nil, start viewing even if the document does not
have the page we want to view."
- (with-current-buffer (get-file-buffer doc)
- (setq doc-view-current-files
- (sort (directory-files (doc-view-current-cache-dir) t
- "page-[0-9]+\\.png" t)
- 'doc-view-sort))
- (when (or force
- (>= (length doc-view-current-files)
- (or doc-view-current-page 1)))
- (doc-view-goto-page doc-view-current-page))))
+ (with-current-buffer buffer
+ (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)
+ '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)
+ (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))))))))
(defun doc-view-buffer-message ()
;; Only show this message initially, not when refreshing the buffer (in which
;; case it's better to keep displaying the "stale" page while computing
;; the fresh new ones).
- (unless (overlay-get doc-view-current-overlay 'display)
- (overlay-put doc-view-current-overlay 'display
+ (unless (overlay-get (doc-view-current-overlay) 'display)
+ (overlay-put (doc-view-current-overlay) 'display
(concat (propertize "Welcome to DocView!" 'face 'bold)
"\n"
"
`k' : Kill the conversion process and this buffer.
`K' : Kill the conversion process.\n"))))
+(declare-function tooltip-show "tooltip" (text &optional use-echo-area))
+
(defun doc-view-show-tooltip ()
(interactive)
- (tooltip-show doc-view-current-info))
+ (tooltip-show (doc-view-current-info)))
+
+(defun doc-view-open-text ()
+ "Open a buffer with the current doc's contents as text."
+ (interactive)
+ (if doc-view-current-converter-processes
+ (message "DocView: please wait till conversion finished.")
+ (let ((txt (expand-file-name "doc.txt" (doc-view-current-cache-dir))))
+ (if (file-readable-p txt)
+ (find-file txt)
+ (doc-view-doc->txt txt 'doc-view-open-text)))))
;;;;; Toggle between editing and viewing
+
(defun doc-view-toggle-display ()
"Toggle between editing a document as text or viewing it."
(interactive)
(progn
(doc-view-kill-proc)
(setq buffer-read-only nil)
- (delete-overlay doc-view-current-overlay)
+ (remove-overlays (point-min) (point-max) 'doc-view t)
+ (set (make-local-variable 'image-mode-winprops-alist) t)
;; Switch to the previously used major mode or fall back to fundamental
;; mode.
(if doc-view-previous-major-mode
;;;; Searching
+
(defun doc-view-search-internal (regexp file)
"Return a list of FILE's pages that contain text matching REGEXP.
The value is an alist of the form (PAGE CONTEXTS) where PAGE is
(when (match-string 2)
(if (/= page lastpage)
(push (cons page
- (list (buffer-substring
- (line-beginning-position)
- (line-end-position))))
- matches)
+ (list (buffer-substring
+ (line-beginning-position)
+ (line-end-position))))
+ matches)
(setq matches (cons
(append
(or
(doc-view-search-no-of-matches
doc-view-current-search-matches)))
;; We must convert to TXT first!
- (if doc-view-current-converter-process
+ (if doc-view-current-converter-processes
(message "DocView: please wait till conversion finished.")
- (let ((ext (file-name-extension buffer-file-name)))
- (cond
- ((string= ext "pdf")
- ;; Doc is a PDF, so convert it to TXT
- (doc-view-pdf->txt buffer-file-name txt))
- ((string= ext "ps")
- ;; Doc is a PS, so convert it to PDF (which will be converted to
- ;; TXT thereafter).
- (doc-view-ps->pdf buffer-file-name
- (expand-file-name "doc.pdf"
- (doc-view-current-cache-dir))))
- ((string= ext "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))
- (t (error "DocView doesn't know what to do")))))))))
+ (doc-view-doc->txt txt (lambda () (doc-view-search nil))))))))
(defun doc-view-search-next-match (arg)
"Go to the ARGth next matching page."
(interactive "p")
(let* ((next-pages (doc-view-remove-if
- (lambda (i) (<= (car i) doc-view-current-page))
+ (lambda (i) (<= (car i) (doc-view-current-page)))
doc-view-current-search-matches))
(page (car (nth (1- arg) next-pages))))
(if page
"Go to the ARGth previous matching page."
(interactive "p")
(let* ((prev-pages (doc-view-remove-if
- (lambda (i) (>= (car i) doc-view-current-page))
+ (lambda (i) (>= (car i) (doc-view-current-page)))
doc-view-current-search-matches))
(page (car (nth (1- arg) (nreverse prev-pages)))))
(if page
;; (put 'doc-view-mode 'mode-class 'special)
+(defun doc-view-already-converted-p ()
+ "Return non-nil if the current doc was already converted."
+ (and (file-exists-p (doc-view-current-cache-dir))
+ (> (length (directory-files (doc-view-current-cache-dir) nil "\\.png$")) 0)))
+
(defun doc-view-initiate-display ()
;; Switch to image display if possible
- (if (doc-view-mode-p (intern (file-name-extension buffer-file-name)))
+ (if (doc-view-mode-p doc-view-doc-type)
(progn
(doc-view-buffer-message)
- (setq doc-view-current-page (or doc-view-current-page 1))
- (if (file-exists-p (doc-view-current-cache-dir))
+ (setf (doc-view-current-page) (or (doc-view-current-page) 1))
+ (if (doc-view-already-converted-p)
(progn
(message "DocView: using cached files!")
- (doc-view-display buffer-file-name 'force))
+ ;; Load the saved resolution
+ (let ((res-file (expand-file-name "resolution.el"
+ (doc-view-current-cache-dir)))
+ (res doc-view-resolution))
+ (with-temp-buffer
+ (when (file-exists-p res-file)
+ (insert-file-contents res-file)
+ (setq res (read (current-buffer)))))
+ (when (numberp res)
+ (set (make-local-variable 'doc-view-resolution) res)))
+ (doc-view-display (current-buffer) 'force))
(doc-view-convert-current-doc))
(message
"%s"
"editing or viewing the document."))))
(message
"%s"
- (substitute-command-keys
- (concat "No image (png) support available or some conversion utility for "
- (file-name-extension buffer-file-name)" files is missing. "
- "Type \\[doc-view-toggle-display] to switch to an editing mode.")))))
-
-(defvar bookmark-make-cell-function)
+ (concat "No PNG support is available, or some conversion utility for "
+ (file-name-extension doc-view-buffer-file-name)
+ " files is missing."))
+ (if (and (executable-find doc-view-pdftotext-program)
+ (y-or-n-p
+ "Unable to render file. View extracted text instead? "))
+ (doc-view-open-text)
+ (doc-view-toggle-display))))
+
+(defvar bookmark-make-record-function)
+
+(defun doc-view-clone-buffer-hook ()
+ ;; FIXME: There are several potential problems linked with reconversion
+ ;; and auto-revert when we have indirect buffers because they share their
+ ;; /tmp cache directory. This sharing is good (you'd rather not reconvert
+ ;; for each clone), but that means that clones need to collaborate a bit.
+ ;; I guess it mostly means: detect when a reconversion process is already
+ ;; running, and run the sentinel in all clones.
+ ;;
+ ;; Maybe the clones should really have a separate /tmp directory
+ ;; so they could have a different resolution and you could use clones
+ ;; for zooming.
+ (remove-overlays (point-min) (point-max) 'doc-view t)
+ (if (consp image-mode-winprops-alist) (setq image-mode-winprops-alist nil)))
+
+(defun doc-view-intersection (l1 l2)
+ (let ((l ()))
+ (dolist (x l1) (if (memq x l2) (push x l)))
+ l))
;;;###autoload
(defun doc-view-mode ()
"Major mode in DocView buffers.
+
+DocView Mode is an Emacs document viewer. It displays PDF, PS
+and DVI files (as PNG images) in Emacs buffers.
+
You can use \\<doc-view-mode-map>\\[doc-view-toggle-display] to
-toggle between displaying the document or editing it as text."
+toggle between displaying the document or editing it as text.
+\\{doc-view-mode-map}"
(interactive)
- (if jka-compr-really-do-compress
- ;; This is a compressed file uncompressed by auto-compression-mode.
- (when (y-or-n-p (concat "DocView: Cannot convert compressed file. "
- "Save it uncompressed first? "))
- (let ((file (read-file-name
- "File: "
- (file-name-directory buffer-file-name))))
- (write-region (point-min) (point-max) file)
- (kill-buffer nil)
- (find-file file)
- (doc-view-mode)))
+
+ (if (= (point-min) (point-max))
+ ;; The doc is empty or doesn't exist at all, so fallback to
+ ;; another mode. We used to also check file-exists-p, but this
+ ;; returns nil for tar members.
+ (let ((auto-mode-alist (remq (rassq 'doc-view-mode auto-mode-alist)
+ auto-mode-alist)))
+ (normal-mode))
+
(let* ((prev-major-mode (if (eq major-mode 'doc-view-mode)
doc-view-previous-major-mode
major-mode)))
(kill-all-local-variables)
(set (make-local-variable 'doc-view-previous-major-mode) prev-major-mode))
- (make-local-variable 'doc-view-current-files)
- (make-local-variable 'doc-view-current-image)
- (make-local-variable 'doc-view-current-page)
- (make-local-variable 'doc-view-current-converter-process)
- (make-local-variable 'doc-view-current-timer)
- (make-local-variable 'doc-view-current-slice)
- (make-local-variable 'doc-view-current-cache-dir)
- (make-local-variable 'doc-view-current-info)
- (make-local-variable 'doc-view-current-search-matches)
- (set (make-local-variable 'doc-view-current-overlay)
- (make-overlay (point-min) (point-max) nil t))
+
+ ;; Figure out the document type.
+ (let ((name-types
+ (when buffer-file-name
+ (cdr (assoc (file-name-extension buffer-file-name)
+ '(("dvi" dvi)
+ ("pdf" pdf)
+ ("epdf" pdf)
+ ("ps" ps)
+ ("eps" ps))))))
+ (content-types
+ (save-excursion
+ (goto-char (point-min))
+ (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")))))
+
+ (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)
+ (cond
+ (jka-compr-really-do-compress
+ (expand-file-name
+ (file-name-nondirectory
+ (file-name-sans-extension buffer-file-name))
+ doc-view-cache-directory))
+ ;; Is the file readable by local processes?
+ ;; We used to use `file-remote-p' but it's unclear what it's
+ ;; supposed to return nil for things like local files accessed via
+ ;; `su' or via file://...
+ ((let ((file-name-handler-alist nil))
+ (not (file-readable-p buffer-file-name)))
+ (expand-file-name
+ (file-name-nondirectory buffer-file-name)
+ doc-view-cache-directory))
+ (t buffer-file-name)))
+ (when (not (string= doc-view-buffer-file-name buffer-file-name))
+ (write-region nil nil doc-view-buffer-file-name))
+
(add-hook 'change-major-mode-hook
- (lambda () (delete-overlay doc-view-current-overlay))
+ (lambda ()
+ (doc-view-kill-proc)
+ (remove-overlays (point-min) (point-max) 'doc-view t))
nil t)
+ (add-hook 'clone-indirect-buffer-hook 'doc-view-clone-buffer-hook nil t)
+ (add-hook 'kill-buffer-hook 'doc-view-kill-proc nil t)
+
+ (remove-overlays (point-min) (point-max) 'doc-view t) ;Just in case.
+ ;; Keep track of display info ([vh]scroll, page number, overlay,
+ ;; ...) for each window in which this document is shown.
+ (add-hook 'image-mode-new-window-functions
+ '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))
+ '(" P" (:eval (number-to-string (doc-view-current-page)))
"/" (:eval (number-to-string (length doc-view-current-files)))))
+ ;; Don't scroll unless the user specifically asked for it.
+ (set (make-local-variable 'auto-hscroll-mode) nil)
(set (make-local-variable '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-cell-function)
- 'doc-view-bookmark-make-cell)
+ (set (make-local-variable 'bookmark-make-record-function)
+ 'doc-view-bookmark-make-record)
(setq mode-name "DocView"
buffer-read-only t
major-mode 'doc-view-mode)
(defun doc-view-clear-cache ()
"Delete the whole cache (`doc-view-cache-directory')."
(interactive)
- (dired-delete-file doc-view-cache-directory 'always)
- (make-directory doc-view-cache-directory))
+ (dired-delete-file doc-view-cache-directory 'always))
(defun doc-view-dired-cache ()
"Open `dired' in `doc-view-cache-directory'."
;;;; Bookmark integration
-(defun doc-view-bookmark-make-cell (annotation &rest args)
- (let ((the-record
- `((filename . ,(buffer-file-name))
- (page . ,doc-view-current-page)
- (handler . doc-view-bookmark-jump))))
-
- ;; Take no chances with text properties
- (set-text-properties 0 (length annotation) nil annotation)
-
- (when annotation
- (nconc the-record (list (cons 'annotation annotation))))
-
- ;; Finally, return the completed record.
- the-record))
+(declare-function bookmark-make-record-default "bookmark"
+ (&optional point-only))
+(declare-function bookmark-prop-get "bookmark" (bookmark prop))
+(declare-function bookmark-default-handler "bookmark" (bmk))
+(defun doc-view-bookmark-make-record ()
+ (nconc (bookmark-make-record-default)
+ `((page . ,(doc-view-current-page))
+ (handler . doc-view-bookmark-jump))))
-(declare-function bookmark-get-filename "bookmark" (bookmark))
-(declare-function bookmark-get-bookmark-record "bookmark" (bookmark))
;;;###autoload
(defun doc-view-bookmark-jump (bmk)
;; This implements the `handler' function interface for record type
- ;; returned by `bookmark-make-cell-function', which see.
- (save-window-excursion
- (let ((filename (bookmark-get-filename bmk))
- (page (cdr (assq 'page (bookmark-get-bookmark-record bmk)))))
- (find-file filename)
+ ;; returned by `doc-view-bookmark-make-record', which see.
+ (prog1 (bookmark-default-handler bmk)
+ (let ((page (bookmark-prop-get bmk 'page)))
(when (not (eq major-mode 'doc-view-mode))
- (doc-view-toggle-display))
- (doc-view-goto-page page)
- `((buffer ,(current-buffer)) (position ,1)))))
+ (doc-view-toggle-display))
+ (with-selected-window
+ (or (get-buffer-window (current-buffer) 0)
+ (selected-window))
+ (doc-view-goto-page page)))))
(provide 'doc-view)