X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/0e8356fe05318a591743e7457e223968eba89f1b..f67b40b3d890918f1e856a5052f86c3c724f0658:/lisp/doc-view.el diff --git a/lisp/doc-view.el b/lisp/doc-view.el index f2bfb1c70f..d464216df8 100644 --- a/lisp/doc-view.el +++ b/lisp/doc-view.el @@ -8,10 +8,10 @@ ;; 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 @@ -19,15 +19,14 @@ ;; 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 . ;;; 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: @@ -99,11 +98,12 @@ ;;; 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). @@ -132,10 +132,10 @@ ;;; Code: +(eval-when-compile (require 'cl)) (require 'dired) (require 'image-mode) (require 'jka-compr) -(require 'tramp) ;; would be better to make tramp-tramp-file-p autoloaded ;;;; Customization Options @@ -171,7 +171,21 @@ Higher values result in larger images." "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) @@ -196,12 +210,10 @@ Needed for searching." :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 @@ -212,42 +224,54 @@ has finished." ;;;; 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 @@ -285,12 +309,16 @@ has finished." ;; Scrolling (define-key map [remap forward-char] 'image-forward-hscroll) (define-key map [remap backward-char] 'image-backward-hscroll) + (define-key map [remap move-end-of-line] 'image-eol) + (define-key map [remap move-beginning-of-line] 'image-bol) (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) @@ -318,54 +346,74 @@ has finished." ;;;; 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))) (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) + (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." @@ -381,28 +429,28 @@ has finished." "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 ((cur-page (doc-view-current-page))) (doc-view-next-page) - (when (/= cur-page doc-view-current-page) - (set-window-vscroll nil 0))))) + (when (/= cur-page (doc-view-current-page)) + (image-scroll-down nil))))) (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 ((cur-page (doc-view-current-page))) (doc-view-previous-page) - (when (/= cur-page doc-view-current-page) + (when (/= cur-page (doc-view-current-page)) (image-scroll-up nil))))) ;;;; 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)) @@ -451,12 +499,13 @@ It's a subdirectory of `doc-view-cache-directory'." (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) @@ -475,8 +524,10 @@ Image types are symbols like `dvi', `postscript' or `pdf'." (cond ((eq type 'dvi) (and (doc-view-mode-p 'pdf) - doc-view-dvipdfm-program - (executable-find doc-view-dvipdfm-program))) + (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 @@ -510,118 +561,153 @@ Should be invoked when the cached images aren't up-to-date." (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) 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)) + +(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." + (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." + (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 @@ -632,15 +718,23 @@ Those files are saved in the directory given by the function (setq doc-view-pending-cache-flush t) (let ((png-file (expand-file-name "page-%d.png" (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) + (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 @@ -653,15 +747,15 @@ 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'." (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. @@ -686,9 +780,9 @@ dragging it to its bottom-right corner. See also "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 @@ -698,13 +792,39 @@ ARGS is a list of image descriptors." (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. @@ -713,26 +833,34 @@ Predicate for sorting `doc-view-current-files'." (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" " @@ -748,10 +876,21 @@ For now these keys are useful: (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) @@ -760,7 +899,8 @@ For now these keys are useful: (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 @@ -775,6 +915,7 @@ For now these keys are useful: ;;;; 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 @@ -790,10 +931,10 @@ the pagenumber and CONTEXTS are all lines of text containing a match." (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 @@ -846,32 +987,15 @@ If BACKWARD is non-nil, jump to the previous match." (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 @@ -885,7 +1009,7 @@ If BACKWARD is non-nil, jump to the previous match." "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 @@ -899,16 +1023,21 @@ If BACKWARD is non-nil, jump to the previous match." ;; (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)) + (doc-view-display (current-buffer) 'force)) (doc-view-convert-current-doc)) (message "%s" @@ -919,34 +1048,38 @@ If BACKWARD is non-nil, jump to the previous match." "%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) + (file-name-extension doc-view-buffer-file-name)" files is missing. " + "Type \\[doc-view-toggle-display] to switch to an editing mode or " + "\\[doc-view-open-text] to open a buffer showing the doc as text."))))) + +(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. You can use \\\\[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) - ;; Handle compressed files, TRAMP files, files inside archives - (cond - (jka-compr-really-do-compress - (let ((file (expand-file-name - (file-name-nondirectory - (file-name-sans-extension buffer-file-name)) - doc-view-cache-directory))) - (write-region nil nil file) - (setq buffer-file-name file))) - ((or - (not (file-exists-p buffer-file-name)) - (tramp-tramp-file-p buffer-file-name)) - (let ((file (expand-file-name - (file-name-nondirectory buffer-file-name) - doc-view-cache-directory))) - (write-region nil nil file) - (setq buffer-file-name file)))) (let* ((prev-major-mode (if (eq major-mode 'doc-view-mode) doc-view-previous-major-mode @@ -954,28 +1087,77 @@ toggle between displaying the document or editing it as text." (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 '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) @@ -999,8 +1181,7 @@ See the command `doc-view-mode' for more information on this 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'." @@ -1010,36 +1191,28 @@ See the command `doc-view-mode' for more information on this mode." ;;;; 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-buffer-file-name "bookmark" ()) +(declare-function bookmark-prop-get "bookmark" (bookmark prop)) +(defun doc-view-bookmark-make-record () + `((filename . ,(bookmark-buffer-file-name)) + (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. + (let ((filename (bookmark-prop-get bmk 'filename)) + (page (bookmark-prop-get bmk 'page))) + (with-current-buffer (find-file-noselect filename) (when (not (eq major-mode 'doc-view-mode)) (doc-view-toggle-display)) - (doc-view-goto-page page) + (with-selected-window + (or (get-buffer-window (current-buffer) 0) + (selected-window)) + (doc-view-goto-page page)) `((buffer ,(current-buffer)) (position ,1)))))