|
|
- ;;; pdf-virtual.el --- Virtual PDF documents -*- lexical-binding: t; -*-
-
- ;; Copyright (C) 2015 Andreas Politz
-
- ;; Author: Andreas Politz <politza@hochschule-trier.de>
- ;; Keywords: multimedia, files
-
- ;; This program 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 of the License, or
- ;; (at your option) any later version.
-
- ;; This program is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
-
- ;; You should have received a copy of the GNU General Public License
- ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- ;;; Commentary:
-
- ;; A virtual PDF is a collection of pages, or parts thereof, of
- ;; arbitrary documents in one particular order. This library acts as
- ;; an intermediate between pdf-info.el and all other packages, in
- ;; order to transparently make this collection appear as one single
- ;; document.
- ;;
- ;; The trickiest part is to make theses intermediate functions behave
- ;; like the pdf-info-* equivalents in both the synchronous and
- ;; asynchronous case.
-
- ;;; Code:
- (eval-when-compile
- (unless (or (> emacs-major-version 24)
- (and (= emacs-major-version 24)
- (>= emacs-minor-version 4)))
- (error "pdf-virtual.el only works with Emacs >= 24.4")))
-
- (require 'let-alist)
- (require 'pdf-info)
- (require 'pdf-util)
-
- ;; * ================================================================== *
- ;; * Variables
- ;; * ================================================================== *
-
- (defconst pdf-virtual-magic-mode-regexp "^ *;+ *%VPDF\\_>"
- "A regexp matching the first line in a vpdf file.")
-
- (defvar-local pdf-virtual-document nil
- "A list representing the virtual document.")
-
- (put 'pdf-virtual-document 'permanent-local t)
-
- (defvar pdf-virtual-adapter-alist nil
- "Alist of server functions.
-
- Each element looks like \(PDF-VIRTUAL-FN . PDF-INFO-FN\). This
- list is filled by the macro `pdf-virtual-define-adapter' and used
- to enable/disable the corresponding advices.")
-
- ;; * ================================================================== *
- ;; * VPDF datastructure
- ;; * ================================================================== *
-
- (defun pdf-virtual-pagespec-normalize (page-spec &optional filename)
- "Normalize PAGE-SPEC using FILENAME.
-
- PAGE-SPEC should be as described in
- `pdf-virtual-document-create'. FILENAME is used to determine the
- last page number, if needed. The `current-buffer', if it is nil.
-
- Returns a list \(\(FIRST . LAST\) . REGION\)\)."
-
- (let ((page-spec (cond
- ((natnump page-spec)
- (list (cons page-spec page-spec)))
- ((null (car page-spec))
- (let ((npages (pdf-info-number-of-pages filename)))
- (cons (cons 1 npages)
- (cdr page-spec))))
- ((natnump (car page-spec))
- (cond
- ((natnump (cdr page-spec))
- (list page-spec))
- (t
- (cons (cons (car page-spec)
- (car page-spec))
- (cdr page-spec)))))
- (t page-spec))))
- (when (equal (cdr page-spec)
- '(0 0 1 1))
- (setq page-spec `((,(caar page-spec) . ,(cdar page-spec)))))
- page-spec))
-
- (cl-defstruct pdf-virtual-range
- ;; The PDF's filename.
- filename
- ;; First page in this range.
- first
- ;; Last page.
- last
- ;; The edges selected for these pages.
- region
- ;; The page-index corresponding to the first page in this range.
- index-start)
-
- (cl-defstruct pdf-virtual-document
- ;; Array of shared pdf-virtual-range structs, one element for each
- ;; page.
- page-array
- ;; An alist mapping filenames to a list of pages.
- file-map)
-
- (defun pdf-virtual-range-length (page)
- "Return the number of pages in PAGE."
- (1+ (- (pdf-virtual-range-last page)
- (pdf-virtual-range-first page))))
-
- (defun pdf-virtual-document-create (list &optional directory
- file-error-handler)
- "Create a virtual PDF from LIST using DIRECTORY.
-
- LIST should be a list of elements \(FILENAME . PAGE-SPECS\),
- where FILENAME is a PDF document and PAGE-SPECS is a list of
- PAGE-RANGE and/or \(PAGE-RANGE . EDGES\). In the later case,
- EDGES should be a list of relative coordinates \(LEFT TOP RIGHT
- BOT\) selecting a region of the page(s) in PAGE-RANGE. Giving no
- PAGE-SPECs at all is equivalent to all pages of FILENAME.
-
- See `pdf-info-normalize-page-range' for the valid formats of
- PAGE-RANGE.
- "
-
- (unless (cl-every 'consp list)
- (error "Every element should be a cons: %s" list))
- (unless (cl-every 'stringp (mapcar 'car list))
- (error "The car of every element should be a filename."))
- (unless (cl-every (lambda (elt)
- (cl-every (lambda (page)
- (or (pdf-info-valid-page-spec-p page)
- (and (consp page)
- (pdf-info-valid-page-spec-p (car page))
- (pdf-util-edges-p (cdr page) 'relative))))
- elt))
- (mapcar 'cdr list))
- (error
- "The cdr of every element should be a list of page-specs"))
- (let* ((doc (pdf-virtual-document--normalize
- list (or directory default-directory)
- file-error-handler))
- (npages 0)
- document file-map)
- (while doc
- (let* ((elt (pop doc))
- (filename (car elt))
- (mapelt (assoc filename file-map))
- (page-specs (cdr elt)))
- (if mapelt
- (setcdr mapelt (cons (1+ npages) (cdr mapelt)))
- (push (list filename (1+ npages)) file-map))
- (while page-specs
- (let* ((ps (pop page-specs))
- (first (caar ps))
- (last (cdar ps))
- (region (cdr ps))
- (clx (make-pdf-virtual-range
- :filename filename
- :first first
- :last last
- :region region
- :index-start npages)))
- (cl-incf npages (1+ (- last first)))
- (push (make-vector (1+ (- last first)) clx)
- document)))))
- (make-pdf-virtual-document
- :page-array (apply 'vconcat (nreverse document))
- :file-map (nreverse
- (mapcar (lambda (f)
- (setcdr f (nreverse (cdr f)))
- f)
- file-map)))))
-
- (defun pdf-virtual-document--normalize (list &optional directory
- file-error-handler)
- (unless file-error-handler
- (setq file-error-handler
- (lambda (filename err)
- (signal (car err)
- (append (cdr err) (list filename))))))
- (let ((default-directory
- (or directory default-directory)))
- (setq list (cl-remove-if-not
- (lambda (filename)
- (condition-case err
- (progn
- (unless (file-readable-p filename)
- (signal 'file-error
- (list "File not readable: " filename)))
- (pdf-info-open filename)
- t)
- (error
- (funcall file-error-handler filename err)
- nil)))
- list
- :key 'car))
- (let* ((file-attributes (make-hash-table :test 'equal))
- (file-equal-p (lambda (f1 f2)
- (let ((a1 (gethash f1 file-attributes))
- (a2 (gethash f2 file-attributes)))
- (if (and a1 a2)
- (equal a1 a2)
- (file-equal-p f1 f2)))))
- files normalized)
- ;; Optimize file-equal-p by caching file-attributes, which is slow
- ;; and would be called quadratic times otherwise. (We don't want
- ;; the same file under different names.)
- (dolist (f (mapcar 'car list))
- (unless (find-file-name-handler f 'file-equal-p)
- (puthash f (file-attributes f) file-attributes)))
- (dolist (elt list)
- (let ((file (cl-find (car elt) files :test file-equal-p)))
- (unless file
- (push (car elt) files)
- (setq file (car elt)))
- (let ((pages (mapcar (lambda (p)
- (pdf-virtual-pagespec-normalize p file))
- (or (cdr elt) '(nil))))
- newpages)
- (while pages
- (let* ((spec (pop pages))
- (first (caar spec))
- (last (cdar spec))
- (region (cdr spec)))
- (while (and pages
- (eq (1+ last)
- (caar (car pages)))
- (equal region (cdr (car pages))))
- (setq last (cdar (pop pages))))
- (push `((,first . ,last) . ,region) newpages)))
- (push (cons file (nreverse newpages))
- normalized))))
- (nreverse normalized))))
-
- (defmacro pdf-virtual-document-defun (name args &optional documentation &rest body)
- "Define a PDF Document function.
-
- Args are just like for `defun'. This macro will ensure, that the
- DOCUMENT argument, which should be last, is setup properly in
- case it is nil, i.e. check that the buffer passes
- `pdf-virtual-buffer-assert-p' and use the variable
- `pdf-virtual-document'."
-
- (declare (doc-string 3) (indent defun)
- (debug (&define name lambda-list
- [&optional stringp]
- def-body)))
- (unless (stringp documentation)
- (push documentation body)
- (setq documentation nil))
- (unless (memq '&optional args)
- (setq args (append (butlast args)
- (list '&optional)
- (last args))))
- (when (memq '&rest args)
- (error "&rest argument not supported"))
- (let ((doc-arg (car (last args)))
- (fn (intern (format "pdf-virtual-document-%s" name))))
- `(progn
- (put ',fn 'definition-name ',name)
- (defun ,fn
- ,args ,documentation
- (setq ,doc-arg
- (or ,doc-arg
- (progn (pdf-virtual-buffer-assert-p)
- pdf-virtual-document)))
- (cl-check-type ,doc-arg pdf-virtual-document)
- ,@body))))
-
- (pdf-virtual-document-defun filenames (doc)
- "Return the list of filenames in DOC."
- (mapcar 'car (pdf-virtual-document-file-map doc)))
-
- (pdf-virtual-document-defun normalize-pages (pages doc)
- "Normalize PAGES using DOC.
-
- Like `pdf-info-normalize-page-range', except 0 is replaced by
- DOC's last page."
-
- (setq pages (pdf-info-normalize-page-range pages))
- (if (eq 0 (cdr pages))
- `(,(car pages) . ,(pdf-virtual-document-number-of-pages doc))
- pages))
-
- (pdf-virtual-document-defun page (page doc)
- "Get PAGE of DOC.
-
- Returns a list \(FILENAME FILE-PAGE REGION\)."
- (let ((page (car (pdf-virtual-document-pages (cons page page) doc))))
- (when page
- (cl-destructuring-bind (filename first-last region)
- page
- (list filename (car first-last) region)))))
-
- (pdf-virtual-document-defun pages (pages doc)
- "Get PAGES of DOC.
-
- PAGES should be a cons \(FIRST . LAST\). Return a list of
- ranges corresponding to PAGES. Each element has the form
-
- \(FILENAME \(FILE-FIRT-PAGE . FILE-LAST-PAGE\) REGION\)
- .
- "
-
- (let ((begin (car pages))
- (end (cdr pages)))
- (unless (<= begin end)
- (error "begin should not exceed end: %s" (cons begin end)))
- (let ((arr (pdf-virtual-document-page-array doc))
- result)
- (when (or (< begin 1)
- (> end (length arr)))
- (signal 'args-out-of-range (list 'pages pages)))
- (while (<= begin end)
- (let* ((page (aref arr (1- begin)))
- (filename (pdf-virtual-range-filename page))
- (offset (- (1- begin)
- (pdf-virtual-range-index-start page)))
- (first (+ (pdf-virtual-range-first page)
- offset))
- (last (min (+ first (- end begin))
- (pdf-virtual-range-last page)))
- (region (pdf-virtual-range-region page)))
- (push `(,filename (,first . ,last) ,region) result)
- (cl-incf begin (1+ (- last first)))))
- (nreverse result))))
-
- (pdf-virtual-document-defun number-of-pages (doc)
- "Return the number of pages in DOC."
- (length (pdf-virtual-document-page-array doc)))
-
- (pdf-virtual-document-defun page-of (filename &optional file-page limit doc)
- "Return a page number displaying FILENAME's page FILE-PAGE in DOC.
-
- If FILE-PAGE is nil, return the first page displaying FILENAME.
- If LIMIT is non-nil, it should be a range \(FIRST . LAST\) in
- which the returned page should fall. This is useful if there are
- more than one page displaying FILE-PAGE. LIMIT is ignored, if
- FILE-PAGE is nil.
-
- Return nil if there is no matching page."
-
- (if (null file-page)
- (cadr (assoc filename (pdf-virtual-document-file-map doc)))
- (let ((pages (pdf-virtual-document-page-array doc)))
- (catch 'found
- (mapc
- (lambda (pn)
- (while (and (<= pn (length pages))
- (equal (pdf-virtual-range-filename (aref pages (1- pn)))
- filename))
- (let* ((page (aref pages (1- pn)))
- (first (pdf-virtual-range-first page))
- (last (pdf-virtual-range-last page)))
- (when (and (>= file-page first)
- (<= file-page last))
- (let ((r (+ (pdf-virtual-range-index-start page)
- (- file-page (pdf-virtual-range-first page))
- 1)))
- (when (or (null limit)
- (and (>= r (car limit))
- (<= r (cdr limit))))
- (throw 'found r))))
- (cl-incf pn (1+ (- last first))))))
- (cdr (assoc filename (pdf-virtual-document-file-map doc))))
- nil))))
-
- (pdf-virtual-document-defun find-matching-page (page predicate
- &optional
- backward-p doc)
- (unless (and (>= page 1)
- (<= page (length (pdf-virtual-document-page-array doc))))
- (signal 'args-out-of-range (list 'page page)))
- (let* ((pages (pdf-virtual-document-page-array doc))
- (i (1- page))
- (this (aref pages i))
- other)
- (while (and (< i (length pages))
- (>= i 0)
- (null other))
- (setq i
- (if backward-p
- (1- (pdf-virtual-range-index-start this))
- (+ (pdf-virtual-range-length this)
- (pdf-virtual-range-index-start this))))
- (when (and (< i (length pages))
- (>= i 0))
- (setq other (aref pages i))
- (unless (funcall predicate this other)
- (setq other nil))))
- other))
-
- (pdf-virtual-document-defun next-matching-page (page predicate doc)
- (pdf-virtual-document-find-matching-page page predicate nil doc))
-
- (pdf-virtual-document-defun previous-matching-page (page predicate doc)
- (declare (indent 1))
- (pdf-virtual-document-find-matching-page page predicate t doc))
-
- (pdf-virtual-document-defun next-file (page doc)
- "Return the next page displaying a different file than PAGE.
-
- PAGE should be a page-number."
- (let ((page (pdf-virtual-document-next-matching-page
- page
- (lambda (this other)
- (not (equal (pdf-virtual-range-filename this)
- (pdf-virtual-range-filename other)))))))
- (when page
- (1+ (pdf-virtual-range-index-start page)))))
-
- (pdf-virtual-document-defun previous-file (page doc)
- "Return the previous page displaying a different file than PAGE.
-
- PAGE should be a page-number."
- (let ((page (pdf-virtual-document-previous-matching-page
- page
- (lambda (this other)
- (not (equal (pdf-virtual-range-filename this)
- (pdf-virtual-range-filename other)))))))
- (when page
- (1+ (pdf-virtual-range-index-start page)))))
-
- ;; * ================================================================== *
- ;; * Modes
- ;; * ================================================================== *
-
- (defvar pdf-virtual-edit-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map emacs-lisp-mode-map)
- (define-key map (kbd "C-c C-c") 'pdf-virtual-view-mode)
- map))
-
-
- ;;;###autoload
- (define-derived-mode pdf-virtual-edit-mode emacs-lisp-mode "VPDF-Edit"
- "Major mode when editing a virtual PDF buffer."
- (buffer-enable-undo)
- (setq-local buffer-read-only nil)
- (unless noninteractive
- (message (substitute-command-keys "Press \\[pdf-virtual-view-mode] to view."))))
-
- ;; FIXME: Provide filename/region from-windows-gathering functions.
- (defvar pdf-virtual-view-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map pdf-view-mode-map)
- (define-key map (kbd "C-c C-c") 'pdf-virtual-edit-mode)
- (define-key map [remap backward-paragraph] 'pdf-virtual-buffer-backward-file)
- (define-key map [remap forward-paragraph] 'pdf-virtual-buffer-forward-file)
- (define-key map (kbd "C-c C-c") 'pdf-virtual-edit-mode)
- map))
-
- ;;;###autoload
- (define-derived-mode pdf-virtual-view-mode pdf-view-mode "VPDF-View"
- "Major mode in virtual PDF buffers."
- (setq-local write-contents-functions nil)
- (remove-hook 'kill-buffer-hook 'pdf-view-close-document t)
- (setq-local header-line-format
- `(:eval (pdf-virtual-buffer-current-file)))
- (unless noninteractive
- (message (substitute-command-keys "Press \\[pdf-virtual-edit-mode] to edit."))))
-
- ;;;###autoload
- (define-minor-mode pdf-virtual-global-minor-mode
- "Enable recognition and handling of VPDF files."
- nil nil nil
- :global t
- (let ((elt `(,pdf-virtual-magic-mode-regexp . pdf-virtual-view-mode)))
- (cond
- (pdf-virtual-global-minor-mode
- (add-to-list 'magic-mode-alist elt))
- (t
- (setq magic-mode-alist
- (remove elt magic-mode-alist))))
- (dolist (elt pdf-virtual-adapter-alist)
- (let ((fn (car elt))
- (orig (cdr elt)))
- (advice-remove orig fn)
- (when pdf-virtual-global-minor-mode
- (advice-add orig :around fn))))))
-
- (advice-add 'pdf-virtual-view-mode
- :around 'pdf-virtual-view-mode-prepare)
-
- ;; This needs to run before pdf-view-mode does its thing.
- (defun pdf-virtual-view-mode-prepare (fn)
- (let (list unreadable)
- (save-excursion
- (goto-char 1)
- (unless (looking-at pdf-virtual-magic-mode-regexp)
- (pdf-virtual-buffer-assert-p))
- (setq list (read (current-buffer))))
- (setq pdf-virtual-document
- (pdf-virtual-document-create
- list
- nil
- (lambda (filename _error)
- (push filename unreadable))))
- (when unreadable
- (display-warning
- 'pdf-virtual
- (format "Some documents could not be opened:\n%s"
- (mapconcat (lambda (f)
- (concat " " f))
- unreadable "\n"))))
- (if (= (pdf-virtual-document-number-of-pages) 0)
- (error "Docüment is empty.")
- (unless pdf-virtual-global-minor-mode
- (pdf-virtual-global-minor-mode 1))
- (funcall fn))))
-
- ;; * ================================================================== *
- ;; * Buffer handling
- ;; * ================================================================== *
-
- ;;;###autoload
- (defun pdf-virtual-buffer-create (&optional filenames buffer-name display-p)
- (interactive
- (list (directory-files default-directory nil "\\.pdf\\'")
- (read-string
- "Buffer name (default: all.vpdf): " nil nil "all.vpdf") t))
- (with-current-buffer (generate-new-buffer buffer-name)
- (insert ";; %VPDF 1.0\n\n")
- (insert ";; File Format
- ;;
- ;; FORMAT ::= ( FILES* )
- ;; FILES ::= ( FILE . PAGE-SPEC* )
- ;; PAGE-SPEC ::= PAGE | ( PAGE . REGION )
- ;; PAGE ::= NUMBER | ( FIRST . LAST )
- ;; REGION ::= ( LEFT TOP RIGHT BOT )
- ;;
- ;; 0 <= X <= 1, forall X in REGION .
-
- ")
- (if (null filenames)
- (insert "nil\n")
- (insert "(")
- (dolist (f filenames)
- (insert (format "(%S)\n " f)))
- (delete-char -2)
- (insert ")\n"))
- (pdf-virtual-edit-mode)
- (when display-p
- (pop-to-buffer (current-buffer)))
- (current-buffer)))
-
- (defun pdf-virtual-buffer-p (&optional buffer)
- (save-current-buffer
- (when buffer (set-buffer buffer))
- (or (derived-mode-p 'pdf-virtual-view-mode 'pdf-virtual-edit-mode)
- pdf-virtual-document)))
-
- (defun pdf-virtual-view-window-p (&optional window)
- (save-selected-window
- (when window (select-window window))
- (derived-mode-p 'pdf-virtual-view-mode)))
-
- (defun pdf-virtual-filename-p (filename)
- (and (stringp filename)
- (file-exists-p filename)
- (with-temp-buffer
- (save-excursion (insert-file-contents filename nil 0 128))
- (looking-at pdf-virtual-magic-mode-regexp))))
-
- (defun pdf-virtual-buffer-assert-p (&optional buffer)
- (unless (pdf-virtual-buffer-p buffer)
- (error "Buffer is not a virtual PDF buffer")))
-
- (defun pdf-virtual-view-window-assert-p (&optional window)
- (unless (pdf-virtual-view-window-p window)
- (error "Window's buffer is not in `pdf-virtual-view-mode'.")))
-
- (defun pdf-virtual-buffer-current-file (&optional window)
- (pdf-virtual-view-window-assert-p window)
- (pdf-virtual-range-filename
- (aref (pdf-virtual-document-page-array
- pdf-virtual-document)
- (1- (pdf-view-current-page window)))))
-
- (defun pdf-virtual-buffer-forward-file (&optional n interactive-p)
- (interactive "p\np")
- (pdf-virtual-view-window-assert-p)
- (let* ((pn (pdf-view-current-page))
- (pages (pdf-virtual-document-page-array
- pdf-virtual-document))
- (page (aref pages (1- pn)))
- (first-filepage (1+ (pdf-virtual-range-index-start page))))
-
- (when (and (< n 0)
- (not (= first-filepage pn)))
- (cl-incf n))
- (setq pn first-filepage)
-
- (let (next)
- (while (and (> n 0)
- (setq next (pdf-virtual-document-next-file pn)))
- (setq pn next)
- (cl-decf n)))
- (let (previous)
- (while (and (< n 0)
- (setq previous (pdf-virtual-document-previous-file pn)))
- (setq pn previous)
- (cl-incf n)))
- (when interactive-p
- (when (< n 0)
- (message "First file."))
- (when (> n 0)
- (message "Last file.")))
- (pdf-view-goto-page pn)
- n))
-
- (defun pdf-virtual-buffer-backward-file (&optional n interactive-p)
- (interactive "p\np")
- (pdf-virtual-buffer-forward-file (- (or n 1)) interactive-p))
-
- ;; * ================================================================== *
- ;; * Helper functions
- ;; * ================================================================== *
-
-
- (defmacro pdf-virtual-dopages (bindings pages &rest body)
- (declare (indent 2) (debug (sexp form &rest form)))
- (let ((page (make-symbol "page")))
- `(dolist (,page ,pages)
- (cl-destructuring-bind ,bindings
- ,page
- ,@body))))
-
- (defun pdf-virtual--perform-search (string pages &optional regexp-p no-error)
- (let* ((pages (pdf-virtual-document-normalize-pages pages))
- (file-pages (pdf-virtual-document-pages pages)))
- (pdf-info-compose-queries
- ((responses
- (pdf-virtual-dopages (filename pages _region)
- file-pages
- (if regexp-p
- (pdf-info-search-string string pages filename)
- ;; FIXME: no-error won't work with synchronous calls.
- (pdf-info-search-regexp string pages no-error filename)))))
- (let (result)
- (pdf-virtual-dopages (filename _ region)
- file-pages
- (let ((matches (pop responses)))
- (when region
- (setq matches
- (mapcar
- (lambda (m)
- (let-alist m
- `((edges . ,(pdf-util-edges-transform region .edges t))
- ,@m)))
- (pdf-virtual--filter-edges
- region matches
- (apply-partially 'alist-get 'edges)))))
- (dolist (m matches)
- (push `((page . ,(pdf-virtual-document-page-of
- filename (alist-get 'page m)
- pages))
- ,@m)
- result))))
- (nreverse result)))))
-
- (defun pdf-virtual--filter-edges (region elts &optional edges-key-fn)
- (if (null region)
- elts
- (cl-remove-if-not
- (lambda (edges)
- (or (null edges)
- (if (consp (car edges))
- (cl-some (apply-partially 'pdf-util-edges-intersection region) edges)
- (pdf-util-edges-intersection region edges))))
- elts
- :key edges-key-fn)))
-
- (defun pdf-virtual--transform-goto-dest (link filename region)
- (let-alist link
- (let ((local-page (pdf-virtual-document-page-of
- filename .page)))
- (if local-page
- `((type . ,'goto-dest)
- (title . , .title)
- (page . ,local-page)
- (top . ,(car (pdf-util-edges-transform
- region (cons .top .top) t))))
- `((type . ,'goto-remote)
- (title . , .title)
- (filename . ,filename)
- (page . , .page)
- (top . , .top))))))
-
- ;; * ================================================================== *
- ;; * Server adapter
- ;; * ================================================================== *
-
- (defmacro pdf-virtual-define-adapter (name arglist &optional doc &rest body)
- ;; FIXME: Handle &optional + &rest argument.
- (declare (doc-string 3) (indent 2)
- (debug (&define name lambda-list
- [&optional stringp]
- def-body)))
- (unless (stringp doc)
- (push doc body)
- (setq doc nil))
- (let ((fn (intern (format "pdf-virtual-%s" name)))
- (base-fn (intern (format "pdf-info-%s" name)))
- (base-fn-arg (make-symbol "fn"))
- (true-file-or-buffer (make-symbol "true-file-or-buffer"))
- (args (cl-remove-if (lambda (elt)
- (memq elt '(&optional &rest)))
- arglist)))
- (unless (fboundp base-fn)
- (error "Base function is undefined: %s" base-fn))
- (unless (memq 'file-or-buffer arglist)
- (error "Argument list is missing a `file-or-buffer' argument: %s" arglist))
- `(progn
- (put ',fn 'definition-name ',name)
- (add-to-list 'pdf-virtual-adapter-alist ',(cons fn base-fn))
- (defun ,fn ,(cons base-fn-arg arglist)
- ,(format "%sPDF virtual adapter to `%s'.
-
- This function delegates to `%s', unless the FILE-OR-BUFFER
- argument denotes a VPDF document."
- (if doc (concat doc "\n\n") "")
- base-fn
- base-fn)
- (let ((,true-file-or-buffer
- (cond
- ((or (bufferp file-or-buffer)
- (stringp file-or-buffer)) file-or-buffer)
- ((or (null file-or-buffer)
- ,(not (null (memq '&rest arglist))))
- (current-buffer)))))
- (if (cond
- ((null ,true-file-or-buffer) t)
- ((bufferp ,true-file-or-buffer)
- (not (pdf-virtual-buffer-p ,true-file-or-buffer)))
- ((stringp ,true-file-or-buffer)
- (not (pdf-virtual-filename-p ,true-file-or-buffer))))
- (,(if (memq '&rest arglist) 'apply 'funcall) ,base-fn-arg ,@args)
- (when (stringp ,true-file-or-buffer)
- (setq ,true-file-or-buffer
- (find-file-noselect ,true-file-or-buffer)))
- (save-current-buffer
- (when (bufferp ,true-file-or-buffer)
- (set-buffer ,true-file-or-buffer))
- ,@body)))))))
-
- (define-error 'pdf-virtual-unsupported-operation
- "Operation not supported in VPDF buffer")
-
- (pdf-virtual-define-adapter open (&optional file-or-buffer password)
- (mapc (lambda (file)
- (pdf-info-open file password))
- (pdf-virtual-document-filenames)))
-
- (pdf-virtual-define-adapter close (&optional file-or-buffer)
- (let ((files (cl-remove-if 'find-buffer-visiting
- (pdf-virtual-document-filenames))))
- (pdf-info-compose-queries
- ((results (mapc 'pdf-info-close files)))
- (cl-some 'identity results))))
-
- (pdf-virtual-define-adapter metadata (&optional file-or-buffer)
- (pdf-info-compose-queries
- ((md (mapc 'pdf-info-metadata (pdf-virtual-document-filenames))))
- (apply 'cl-mapcar (lambda (&rest elts)
- (cons (caar elts)
- (cl-mapcar 'cdr elts)))
- md)))
-
- (pdf-virtual-define-adapter search-string (string &optional pages file-or-buffer)
- (pdf-virtual--perform-search
- string (pdf-virtual-document-normalize-pages pages)))
-
- (pdf-virtual-define-adapter search-regexp (pcre &optional
- pages no-error file-or-buffer)
- (pdf-virtual--perform-search
- pcre (pdf-virtual-document-normalize-pages pages) 'regexp no-error))
-
- (pdf-virtual-define-adapter pagelinks (page &optional file-or-buffer)
- (cl-destructuring-bind (filename ext-page region)
- (pdf-virtual-document-page page)
- (pdf-info-compose-queries
- ((links (pdf-info-pagelinks ext-page filename)))
- (mapcar
- (lambda (link)
- (let-alist link
- (if (not (eq .type 'goto-dest))
- link
- `((edges . ,(pdf-util-edges-transform region .edges t))
- ,@(pdf-virtual--transform-goto-dest link filename region)))))
- (pdf-virtual--filter-edges region (car links) 'car)))))
-
- (pdf-virtual-define-adapter number-of-pages (&optional file-or-buffer)
- (pdf-info-compose-queries nil (pdf-virtual-document-number-of-pages)))
-
- (pdf-virtual-define-adapter outline (&optional file-or-buffer)
- (let ((files (pdf-virtual-document-filenames)))
- (pdf-info-compose-queries
- ((outlines (mapc 'pdf-info-outline files)))
- (cl-mapcan
- (lambda (outline filename)
- `(((depth . 1)
- (type . goto-dest)
- (title . ,filename)
- (page . ,(pdf-virtual-document-page-of filename))
- (top . 0))
- ,@(delq
- nil
- (mapcar
- (lambda (item)
- (let-alist item
- (if (not (eq .type 'goto-dest))
- `((depth . ,(1+ .depth))
- ,@item)
- (cl-check-type filename string)
- (let ((page (pdf-virtual-document-page-of
- filename .page)))
- (when page
- `((depth . ,(1+ .depth))
- ,@(pdf-virtual--transform-goto-dest
- item filename
- (nth 2 (pdf-virtual-document-page page)))))))))
- outline))))
- outlines files))))
-
- (pdf-virtual-define-adapter gettext (page edges &optional
- selection-style file-or-buffer)
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (let ((edges (pdf-util-edges-transform region edges)))
- (pdf-info-gettext file-page edges selection-style filename))))
-
- (pdf-virtual-define-adapter getselection (page edges &optional
- selection-style file-or-buffer)
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (let ((edges (pdf-util-edges-transform region edges)))
- (pdf-info-compose-queries
- ((results (pdf-info-getselection file-page edges selection-style filename)))
- (pdf-util-edges-transform
- region
- (pdf-virtual--filter-edges region (car results)) t)))))
-
- (pdf-virtual-define-adapter charlayout (page &optional edges-or-pos file-or-buffer)
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (let ((edges-or-pos (pdf-util-edges-transform region edges-or-pos)))
- (pdf-info-compose-queries
- ((results (pdf-info-charlayout file-page edges-or-pos filename)))
- (mapcar (lambda (elt)
- `(,(car elt)
- . ,(pdf-util-edges-transform region (cdr elt) t)))
- (pdf-virtual--filter-edges region (car results) 'cadr))))))
-
- (pdf-virtual-define-adapter pagesize (page &optional file-or-buffer)
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (pdf-info-compose-queries
- ((result (pdf-info-pagesize file-page filename)))
- (if (null region)
- (car result)
- (pdf-util-with-edges (region)
- (pdf-util-scale
- (car result) (cons region-width region-height)))))))
-
- (pdf-virtual-define-adapter getannots (&optional pages file-or-buffer)
- (let* ((pages (pdf-virtual-document-normalize-pages pages))
- (file-pages (pdf-virtual-document-pages pages)))
- (pdf-info-compose-queries
- ((annotations
- (pdf-virtual-dopages (filename file-pages _region)
- file-pages
- (pdf-info-getannots file-pages filename))))
- (let ((page (car pages))
- result)
- (pdf-virtual-dopages (_filename file-pages region)
- file-pages
- (dolist (a (pop annotations))
- (let ((edges (delq nil `(,(cdr (assq 'edges a))
- ,@(cdr (assq 'markup-edges a))))))
- (when (pdf-virtual--filter-edges region edges)
- (let-alist a
- (setcdr (assq 'page a)
- (+ page (- .page (car file-pages))))
- (setcdr (assq 'id a)
- (intern (format "%s/%d" .id (cdr (assq 'page a)))))
- (when region
- (when .edges
- (setcdr (assq 'edges a)
- (pdf-util-edges-transform region .edges t)))
- (when .markup-edges
- (setcdr (assq 'markup-edges a)
- (pdf-util-edges-transform region .markup-edges t))))
- (push a result)))))
- (cl-incf page (1+ (- (cdr file-pages) (car file-pages)))))
- (nreverse result)))))
-
- (pdf-virtual-define-adapter getannot (id &optional file-or-buffer)
- (let ((name (symbol-name id))
- page)
- (save-match-data
- (when (string-match "\\(.*\\)/\\([0-9]+\\)\\'" name)
- (setq id (intern (match-string 1 name))
- page (string-to-number (match-string 2 name)))))
- (if page
- (cl-destructuring-bind (filename _ _)
- (pdf-virtual-document-page page)
- (pdf-info-compose-queries
- ((result (pdf-info-getannot id filename)))
- (let ((a (car result)))
- (cl-destructuring-bind (_ _ region)
- (pdf-virtual-document-page page)
- (setcdr (assq 'page a) page)
- (let-alist a
- (setcdr (assq 'id a)
- (intern (format "%s/%d" .id (cdr (assq 'page a)))))
- (when region
- (when .edges
- (setcdr (assq 'edges a)
- (pdf-util-edges-transform region .edges t)))
- (when .markup-edges
- (setcdr (assq 'markup-edges a)
- (pdf-util-edges-transform region .markup-edges t))))))
- a)))
- (pdf-info-compose-queries nil
- (error "No such annotation: %s" id)))))
-
- (pdf-virtual-define-adapter addannot (page edges type &optional
- file-or-buffer &rest markup-edges)
- (signal 'pdf-virtual-unsupported-operation (list 'addannot)))
-
- (pdf-virtual-define-adapter delannot (id &optional file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'delannot)))
-
- (pdf-virtual-define-adapter mvannot (id edges &optional file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'mvannot)))
-
- (pdf-virtual-define-adapter editannot (id modifications &optional file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'editannot)))
-
- (pdf-virtual-define-adapter save (&optional file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'save)))
-
- ;;(defvar-local pdf-virtual-annotation-mapping nil)
-
- (pdf-virtual-define-adapter getattachment-from-annot
- (id &optional do-save file-or-buffer)
- (let ((name (symbol-name id))
- page)
- (save-match-data
- (when (string-match "\\(.*\\)/\\([0-9]+\\)\\'" name)
- (setq id (intern (match-string 1 name))
- page (string-to-number (match-string 2 name)))))
- (if page
- (cl-destructuring-bind (filename _ _)
- (pdf-virtual-document-page page)
- (pdf-info-getattachment-from-annot id do-save filename))
- (pdf-info-compose-queries nil
- (error "No such annotation: %s" id)))))
-
- (pdf-virtual-define-adapter getattachments (&optional do-save file-or-buffer)
- (pdf-info-compose-queries
- ((results (mapc
- (lambda (f)
- (pdf-info-getattachments do-save f))
- (pdf-virtual-document-filenames))))
- (apply 'append results)))
-
- (pdf-virtual-define-adapter synctex-forward-search
- (source &optional line column file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'synctex-forward-search)))
-
- (pdf-virtual-define-adapter synctex-backward-search (page &optional x y file-or-buffer)
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (cl-destructuring-bind (x &rest y)
- (pdf-util-edges-transform region (cons x y))
- (pdf-info-synctex-backward-search file-page x y filename))))
-
- (pdf-virtual-define-adapter renderpage (page width &optional file-or-buffer
- &rest commands)
- (when (keywordp file-or-buffer)
- (push file-or-buffer commands)
- (setq file-or-buffer nil))
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (when region
- (setq commands (append (list :crop-to region) commands)
- width (pdf-util-with-edges (region)
- (round (* width (max 1 (/ 1.0 (max 1e-6 region-width))))))))
- (apply 'pdf-info-renderpage file-page width filename commands)))
-
- (pdf-virtual-define-adapter boundingbox (page &optional file-or-buffer)
- (cl-destructuring-bind (filename file-page region)
- (pdf-virtual-document-page page)
- (pdf-info-compose-queries
- ((results (unless region (pdf-info-boundingbox file-page filename))))
- (if region
- (list 0 0 1 1)
- (car results)))))
-
- (pdf-virtual-define-adapter pagelabels (&optional file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'pagelabels)))
-
- (pdf-virtual-define-adapter setoptions (&optional file-or-buffer &rest options)
- (when (keywordp file-or-buffer)
- (push file-or-buffer options)
- (setq file-or-buffer nil))
- (pdf-info-compose-queries
- ((_ (dolist (f (pdf-virtual-document-filenames))
- (apply 'pdf-info-setoptions f options))))
- nil))
-
- (pdf-virtual-define-adapter getoptions (&optional file-or-buffer)
- (signal 'pdf-virtual-unsupported-operation (list 'getoptions)))
-
- (pdf-virtual-define-adapter encrypted-p (&optional file-or-buffer)
- nil)
-
- (provide 'pdf-virtual)
- ;;; pdf-virtual.el ends here
|