;;; pdf-virtual.el --- Virtual PDF documents -*- lexical-binding: t; -*- ;; Copyright (C) 2015 Andreas Politz ;; Author: Andreas Politz ;; 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 . ;;; 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 these 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) (declare-function pdf-view-mode "pdf-view.el") ;; * ================================================================== * ;; * 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." :global t :group 'pdf-tools (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 "Document 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 'norecord)) (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