pdf-annot.el (67341B)
1 ;;; pdf-annot.el --- Annotation support for PDF files. -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2013, 2014 Andreas Politz 4 5 ;; Author: Andreas Politz <politza@fh-trier.de> 6 ;; Keywords: 7 8 ;; This program is free software; you can redistribute it and/or modify 9 ;; it under the terms of the GNU General Public License as published by 10 ;; the Free Software Foundation, either version 3 of the License, or 11 ;; (at your option) any later version. 12 13 ;; This program is distributed in the hope that it will be useful, 14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 ;; GNU General Public License for more details. 17 18 ;; You should have received a copy of the GNU General Public License 19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>. 20 21 ;;; Commentary: 22 ;; 23 24 25 (require 'pdf-view) 26 (require 'pdf-info) 27 (require 'pdf-cache) 28 (require 'pdf-misc) 29 (require 'facemenu) ;; list-colors-duplicates 30 (require 'faces) ;; color-values 31 (require 'org) ;; org-create-formula-image 32 (require 'tablist) 33 (require 'cl-lib) 34 35 36 ;; * ================================================================== * 37 ;; * Customizations 38 ;; * ================================================================== * 39 40 (defgroup pdf-annot nil 41 "Annotation support for PDF documents." 42 :group 'pdf-tools) 43 44 (defcustom pdf-annot-activate-handler-functions nil 45 "A list of functions to activate a annotation. 46 47 The functions on this hook will be called when some annotation is 48 activated, usually by a mouse-click. Each one is called with the 49 annotation as a single argument and it should return a non-nil 50 value if it has `handled' it. If no such function exists, the 51 default handler `pdf-annot-default-handler' will be 52 called. 53 54 This hook is meant to allow for custom annotations. FIXME: 55 Implement and describe basic org example." 56 :group 'pdf-annot 57 :type 'hook) 58 59 (defcustom pdf-annot-default-text-annotation-properties nil 60 "Alist of initial properties for new text annotations." 61 :group 'pdf-annot 62 :type '(alist :key-type symbol :value-type sexp)) 63 64 (defcustom pdf-annot-default-markup-annotation-properties nil 65 "Alist of initial properties for new markup annotations." 66 :group 'pdf-annot 67 :type '(alist :key-type symbol :value-type sexp)) 68 69 (make-obsolete-variable 'pdf-annot-default-text-annotation-properties 70 'pdf-annot-default-annotation-properties 71 "0.90") 72 73 (make-obsolete-variable 'pdf-annot-default-markup-annotation-properties 74 'pdf-annot-default-annotation-properties 75 "0.90") 76 77 (defcustom pdf-annot-default-annotation-properties 78 `((t (label . ,user-full-name)) 79 (text (icon . "Note") 80 (color . "#ff0000")) 81 (highlight (color . "yellow")) 82 (squiggly (color . "orange")) 83 (strike-out(color . "red")) 84 (underline (color . "blue"))) 85 "An alist of initial properties for new annotations. 86 87 The alist contains a sub-alist for each of the currently available 88 annotation types, i.e. text, highlight, squiggly, strike-out and 89 underline. Additionally a sub-alist with a key of t acts as a default 90 entry. 91 92 Each of these sub-alists contain default property-values of newly 93 added annotations of its respective type. 94 95 Some of the most important properties and their types are label 96 \(a string\), contents \(a string\), color \(a color\) and, for 97 text-annotations only, icon \(one of the standard icon-types, see 98 `pdf-annot-standard-text-icons'\). 99 100 For example a value of 101 102 \(\(t \(color . \"red\"\) 103 \(label . \"Joe\"\) 104 \(highlight \(color . \"green\"\)\) 105 106 would use a green color for highlight and a red one for other 107 annotations. Additionally the label for all annotations is set 108 to \"Joe\"." 109 110 :group 'pdf-annot 111 :type (let* ((label '(cons :tag "Label" (const label) string)) 112 (contents '(cons :tag "Contents" (const contents) string)) 113 (color '(cons :tag "Color" (const color) color)) 114 (icon `(cons :tag "Icon" 115 (const icon) 116 (choice 117 ,@(mapcar (lambda (icon) 118 `(const ,icon)) 119 '("Note" "Comment" "Key" "Help" "NewParagraph" 120 "Paragraph" "Insert" "Cross" "Circle"))))) 121 (other '(repeat 122 :tag "Other properties" 123 (cons :tag "Property" 124 (symbol :tag "Key ") 125 (sexp :tag "Value")))) 126 (text-properties 127 `(set ,label ,contents ,color ,icon ,other)) 128 (markup-properties 129 `(set ,label ,contents ,color)) 130 (all-properties 131 `(set ,label ,contents ,color ,icon ,other))) 132 `(set 133 (cons :tag "All Annotations" (const t) ,all-properties) 134 (cons :tag "Text Annotations" (const text) ,text-properties) 135 (cons :tag "Highlight Annotations" (const highlight) ,markup-properties) 136 (cons :tag "Underline Annotations" (const underline) ,markup-properties) 137 (cons :tag "Squiggly Annotations" (const squiggly) ,markup-properties) 138 (cons :tag "Strike-out Annotations" (const strike-out) ,markup-properties)))) 139 140 (defcustom pdf-annot-print-annotation-functions 141 '(pdf-annot-print-annotation-latex-maybe) 142 "A alist of functions for printing annotations, e.g. for the tooltip. 143 144 The functions receive the annotation as single argument and 145 should return either a string or nil. The first string returned 146 will be used. 147 148 If all of them return nil, the default function 149 `pdf-annot-print-annotation-default' is used." 150 :group 'pdf-annot 151 :type 'hook) 152 153 (defcustom pdf-annot-latex-string-predicate 154 (lambda (str) 155 (and str (string-match "\\`[[:space:]\n]*[$\\]" str))) 156 "A predicate for recognizing LaTeX fragments. 157 158 It receives a string and should return non-nil, if string is a 159 LaTeX fragment." 160 :group 'pdf-annot 161 :type 'function) 162 163 (defcustom pdf-annot-latex-header 164 (concat org-format-latex-header 165 "\n\\setlength{\\textwidth}{12cm}") 166 "Header used when latex compiling annotations. 167 The default value is `org-format-latex-header' + 168 \"\\n\\\\setlength{\\\\textwidth}{12cm}\"." 169 :group 'pdf-annot 170 :type 'string) 171 172 (defcustom pdf-annot-tweak-tooltips t 173 "Whether this package should tweak some settings regarding tooltips. 174 175 If this variable has a non-nil value, 176 177 `x-gtk-use-system-tooltips' is set to nil if appropriate, in 178 order to display text properties; 179 180 `tooltip-hide-delay' is set to infinity, in order to not being 181 annoyed while reading the annotations." 182 :group 'pdf-annot 183 :type 'boolean) 184 185 (defcustom pdf-annot-activate-created-annotations nil 186 "Whether to activate (i.e. edit) created annotations." 187 :group 'pdf-annot 188 :type 'boolean) 189 190 (defcustom pdf-annot-attachment-display-buffer-action nil 191 "The display action used when displaying attachments." 192 :group 'pdf-annot 193 :type display-buffer--action-custom-type) 194 195 (defconst pdf-annot-annotation-types 196 '(3d caret circle file 197 free-text highlight ink line link movie poly-line polygon popup 198 printer-mark screen sound square squiggly stamp strike-out text 199 trap-net underline unknown watermark widget) 200 "Complete list of annotation types.") 201 202 (defcustom pdf-annot-list-listed-types 203 (if (pdf-info-markup-annotations-p) 204 (list 'text 'file 'squiggly 'highlight 'underline 'strike-out) 205 (list 'text 'file)) 206 "A list of annotation types displayed in the list buffer." 207 :group 'pdf-annot 208 :type `(set ,@(mapcar (lambda (type) 209 (list 'const type)) 210 pdf-annot-annotation-types))) 211 212 213 ;; * ================================================================== * 214 ;; * Variables and Macros 215 ;; * ================================================================== * 216 217 (defvar pdf-annot-color-history nil 218 "A list of recently used colors for annotations.") 219 220 (defvar-local pdf-annot-modified-functions nil 221 "Functions to call, when an annotation was modified. 222 223 A function on this hook should accept one argument: A CLOSURE 224 containing inserted, changed and deleted annotations. 225 226 It may access these annotations by calling CLOSURE with one of 227 these arguments: 228 229 `:inserted' The list of recently added annotations. 230 231 `:deleted' The list of recently deleted annotations. 232 233 `:changed' The list of recently changed annotations. 234 235 `t' The union of recently added, deleted or changed annotations. 236 237 `nil' Just returns nil. 238 239 Any other argument signals an error.") 240 241 (defconst pdf-annot-text-annotation-size '(24 . 24) 242 "The Size of text and file annotations in PDF points. 243 244 These values are hard-coded in poppler. And while the size of 245 these annotations may be changed, i.e. the edges property, it has 246 no effect on the rendering.") 247 248 (defconst pdf-annot-markup-annotation-types 249 '(text link free-text line square 250 circle polygon poly-line highlight underline squiggly 251 strike-out stamp caret ink file sound) 252 "List of defined markup annotation types.") 253 254 (defconst pdf-annot-standard-text-icons 255 '("Note" "Comment" "Key" "Help" "NewParagraph" 256 "Paragraph" "Insert" "Cross" "Circle") 257 "A list of standard icon properties for text annotations.") 258 259 (defvar pdf-annot-inhibit-modification-hooks nil 260 "Non-nil, if running `pdf-annot-modified-functions' should be 261 inhibited after some annotation has changed.") 262 263 (defvar-local pdf-annot-delayed-modified-annotations nil 264 "A plist of not yet propagated modifications. 265 266 It contains three entries :change, :delete and :insert. Each one 267 having a list of annotations as value.") 268 269 (defvar-local pdf-annot--attachment-file-alist nil 270 "Alist mapping attachment ids to unique relative filenames.") 271 272 (defmacro pdf-annot-with-atomic-modifications (&rest body) 273 "Execute BODY joining multiple modifications. 274 275 The effect is, that `pdf-annot-modified-functions' will be called 276 only once at the end of BODY. 277 278 BODY should not modify annotations in a different then the 279 current buffer, because that won't run the hooks properly." 280 (declare (indent 0) (debug t)) 281 `(unwind-protect 282 (save-current-buffer 283 (let ((pdf-annot-inhibit-modification-hooks t)) 284 (progn ,@body))) 285 (pdf-annot-run-modified-hooks))) 286 287 288 ;; * ================================================================== * 289 ;; * Minor mode 290 ;; * ================================================================== * 291 292 (defcustom pdf-annot-minor-mode-map-prefix (kbd "C-c C-a") 293 "The prefix to use for `pdf-annot-minor-mode-map'. 294 295 Setting this after the package was loaded has no effect." 296 :group 'pdf-annot 297 :type 'key-sequence) 298 299 (defvar pdf-annot-minor-mode-map 300 (let ((kmap (make-sparse-keymap)) 301 (smap (make-sparse-keymap))) 302 (define-key kmap pdf-annot-minor-mode-map-prefix smap) 303 (define-key smap "l" 'pdf-annot-list-annotations) 304 ;; (define-key smap "d" 'pdf-annot-toggle-display-annotations) 305 (define-key smap "a" 'pdf-annot-attachment-dired) 306 (when (pdf-info-writable-annotations-p) 307 (define-key smap "D" 'pdf-annot-delete) 308 (define-key smap "t" 'pdf-annot-add-text-annotation) 309 (when (pdf-info-markup-annotations-p) 310 (define-key smap "m" 'pdf-annot-add-markup-annotation) 311 (define-key smap "s" 'pdf-annot-add-squiggly-markup-annotation) 312 (define-key smap "u" 'pdf-annot-add-underline-markup-annotation) 313 (define-key smap "o" 'pdf-annot-add-strikeout-markup-annotation) 314 (define-key smap "h" 'pdf-annot-add-highlight-markup-annotation))) 315 kmap) 316 "Keymap used for `pdf-annot-minor-mode'.") 317 318 (defvar savehist-minibuffer-history-variables) 319 320 ;;;###autoload 321 (define-minor-mode pdf-annot-minor-mode 322 "Support for PDF Annotations. 323 324 \\{pdf-annot-minor-mode-map}" 325 :group 'pdf-annot 326 (cond 327 (pdf-annot-minor-mode 328 (when pdf-annot-tweak-tooltips 329 (when (boundp 'x-gtk-use-system-tooltips) 330 (setq x-gtk-use-system-tooltips nil)) 331 (setq tooltip-hide-delay 3600)) 332 (pdf-view-add-hotspot-function 'pdf-annot-hotspot-function 9) 333 (add-hook 'pdf-info-close-document-hook 334 'pdf-annot-attachment-delete-base-directory nil t) 335 (when (featurep 'savehist) 336 (add-to-list 'savehist-minibuffer-history-variables 337 'pdf-annot-color-history))) 338 (t 339 (pdf-view-remove-hotspot-function 'pdf-annot-hotspot-function) 340 (remove-hook 'pdf-info-close-document-hook 341 'pdf-annot-attachment-delete-base-directory t))) 342 (pdf-view-redisplay t)) 343 344 (defun pdf-annot-create-context-menu (a) 345 "Create a appropriate context menu for annotation A." 346 (let ((menu (make-sparse-keymap))) 347 ;; (when (and (bound-and-true-p pdf-misc-menu-bar-minor-mode) 348 ;; (bound-and-true-p pdf-misc-install-popup-menu)) 349 ;; (set-keymap-parent menu 350 ;; (lookup-key pdf-misc-menu-bar-minor-mode-map 351 ;; [menu-bar pdf-tools])) 352 ;; (define-key menu [sep-99] menu-bar-separator)) 353 (when (pdf-info-writable-annotations-p) 354 (define-key menu [delete-annotation] 355 `(menu-item "Delete annotation" 356 ,(lambda () 357 (interactive) 358 (pdf-annot-delete a) 359 (message "Annotation deleted")) 360 :help 361 "Delete this annotation."))) 362 (define-key menu [goto-annotation] 363 `(menu-item "List annotation" 364 ,(lambda () 365 (interactive) 366 (pdf-annot-show-annotation a t) 367 (pdf-annot-list-annotations) 368 (pdf-annot-list-goto-annotation a)) 369 :help "Find this annotation in the list buffer.")) 370 (when (pdf-annot-text-annotation-p a) 371 (define-key menu [change-text-icon] 372 `(menu-item "Change icon" 373 ,(pdf-annot-create-icon-submenu a) 374 :help "Change the appearance of this annotation."))) 375 (define-key menu [change-color] 376 `(menu-item "Change color" 377 ,(pdf-annot-create-color-submenu a) 378 :help "Change the appearance of this annotation.")) 379 (define-key menu [activate-annotation] 380 `(menu-item "Activate" 381 ,(lambda () 382 (interactive) 383 (pdf-annot-activate-annotation a)) 384 :help "Activate this annotation.")) 385 menu)) 386 387 (defun pdf-annot-create-color-submenu (a) 388 (let ((menu (make-sparse-keymap))) 389 (define-key menu [color-chooser] 390 `(menu-item "Choose ..." 391 ,(lambda () 392 (interactive) 393 (list-colors-display 394 nil "*Choose annotation color*" 395 ;; list-colors-print does not like closures. 396 (let ((callback (make-symbol "xcallback"))) 397 (fset callback 398 (lambda (color) 399 (pdf-annot-put a 'color color) 400 (setq pdf-annot-color-history 401 (cons color 402 (remove color pdf-annot-color-history))) 403 (quit-window t))) 404 (list 'function callback)))))) 405 (dolist (color (butlast (reverse pdf-annot-color-history) 406 (max 0 (- (length pdf-annot-color-history) 407 12)))) 408 (define-key menu (vector (intern (format "color-%s" color))) 409 `(menu-item ,color 410 ,(lambda nil 411 (interactive) 412 (pdf-annot-put a 'color color))))) 413 menu)) 414 415 (defun pdf-annot-create-icon-submenu (a) 416 (let ((menu (make-sparse-keymap))) 417 (dolist (icon (reverse pdf-annot-standard-text-icons)) 418 (define-key menu (vector (intern (format "icon-%s" icon))) 419 `(menu-item ,icon 420 ,(lambda nil 421 (interactive) 422 (pdf-annot-put a 'icon icon))))) 423 menu)) 424 425 ;; * ================================================================== * 426 ;; * Annotation Basics 427 ;; * ================================================================== * 428 429 (defun pdf-annot-create (alist &optional buffer) 430 "Create a annotation from ALIST in BUFFER. 431 432 ALIST should be a property list as returned by 433 `pdf-cache-getannots'. BUFFER should be the buffer of the 434 corresponding PDF document. It defaults to the current buffer." 435 436 (cons `(buffer . ,(or buffer (current-buffer))) 437 alist)) 438 439 (defun pdf-annot-getannots (&optional pages types buffer) 440 "Return a list of annotations on PAGES of TYPES in BUFFER. 441 442 See `pdf-info-normalize-pages' for valid values of PAGES. TYPES 443 may be a symbol or list of symbols denoting annotation types. 444 445 PAGES defaults to all pages, TYPES to all types and BUFFER to the 446 current buffer." 447 448 (pdf-util-assert-pdf-buffer buffer) 449 (unless buffer 450 (setq buffer (current-buffer))) 451 (unless (listp types) 452 (setq types (list types))) 453 (with-current-buffer buffer 454 (let (result) 455 (dolist (a (pdf-info-getannots pages)) 456 (when (or (null types) 457 (memq (pdf-annot-get a 'type) types)) 458 (push (pdf-annot-create a) result))) 459 result))) 460 461 (defun pdf-annot-getannot (id &optional buffer) 462 (pdf-annot-create 463 (pdf-info-getannot id buffer) 464 buffer)) 465 466 (defun pdf-annot-get (a property &optional default) 467 "Get annotation A's value of PROPERTY. 468 469 Return DEFAULT, if value is nil." 470 (or (cdr (assq property a)) default)) 471 472 (defun pdf-annot-put (a property value) 473 "Set annotation A's PROPERTY to VALUE. 474 475 Unless VALUE is `equal' to the current value, sets A's buffer's 476 modified flag and runs the hook `pdf-annot-modified-functions'. 477 478 Signals an error, if PROPERTY is not modifiable. 479 480 Returns the modified annotation." 481 482 (declare (indent 2)) 483 (unless (equal value (pdf-annot-get a property)) 484 (unless (pdf-annot-property-modifiable-p a property) 485 (error "Property `%s' is read-only for this annotation" 486 property)) 487 (with-current-buffer (pdf-annot-get-buffer a) 488 (setq a (pdf-annot-create 489 (pdf-info-editannot 490 (pdf-annot-get-id a) 491 `((,property . ,value))))) 492 (set-buffer-modified-p t) 493 (pdf-annot-run-modified-hooks :change a))) 494 a) 495 496 (defun pdf-annot-run-modified-hooks (&optional operation &rest annotations) 497 "Run `pdf-annot-modified-functions' using OPERATION on ANNOTATIONS. 498 499 OPERATION should be one of nil, :change, :insert or :delete. If 500 nil, annotations should be empty. 501 502 Redisplay modified pages. 503 504 If `pdf-annot-inhibit-modification-hooks' in non-nil, this just 505 saves ANNOTATIONS and does not call the hooks until later, when 506 the variable is nil and this function is called again." 507 508 (unless (memq operation '(nil :insert :change :delete)) 509 (error "Invalid operation: %s" operation)) 510 (when (and (null operation) annotations) 511 (error "Missing operation argument")) 512 513 (when operation 514 (let ((list (plist-get pdf-annot-delayed-modified-annotations operation))) 515 (dolist (a annotations) 516 (cl-pushnew a list :test 'pdf-annot-equal)) 517 (setq pdf-annot-delayed-modified-annotations 518 (plist-put pdf-annot-delayed-modified-annotations 519 operation list)))) 520 (unless pdf-annot-inhibit-modification-hooks 521 (let* ((changed (plist-get pdf-annot-delayed-modified-annotations :change)) 522 (inserted (mapcar (lambda (a) 523 (or (car (cl-member a changed :test 'pdf-annot-equal)) 524 a)) 525 (plist-get pdf-annot-delayed-modified-annotations :insert))) 526 (deleted (plist-get pdf-annot-delayed-modified-annotations :delete)) 527 (union (cl-union (cl-union changed inserted :test 'pdf-annot-equal) 528 deleted :test 'pdf-annot-equal)) 529 (closure (lambda (arg) 530 (cl-ecase arg 531 (:inserted (copy-sequence inserted)) 532 (:changed (copy-sequence changed)) 533 (:deleted (copy-sequence deleted)) 534 (t (copy-sequence union)) 535 (nil nil)))) 536 (pages (mapcar (lambda (a) (pdf-annot-get a 'page)) union))) 537 (when union 538 (unwind-protect 539 (run-hook-with-args 540 'pdf-annot-modified-functions closure) 541 (setq pdf-annot-delayed-modified-annotations nil) 542 (apply 'pdf-view-redisplay-pages pages)))))) 543 544 (defun pdf-annot-equal (a1 a2) 545 "Return non-nil, if annotations A1 and A2 are equal. 546 547 Two annotations are equal, if they belong to the same buffer and 548 have identical id properties." 549 (and (eq (pdf-annot-get-buffer a1) 550 (pdf-annot-get-buffer a2)) 551 (eq (pdf-annot-get-id a1) 552 (pdf-annot-get-id a2)))) 553 554 (defun pdf-annot-get-buffer (a) 555 "Return annotation A's buffer." 556 (pdf-annot-get a 'buffer)) 557 558 (defun pdf-annot-get-id (a) 559 "Return id property of annotation A." 560 (pdf-annot-get a 'id)) 561 562 (defun pdf-annot-get-type (a) 563 "Return type property of annotation A." 564 (pdf-annot-get a 'type)) 565 566 (defun pdf-annot-get-display-edges (a) 567 "Return a list of EDGES used for display for annotation A. 568 569 This returns a list of \(LEFT TOP RIGHT BOT\) demarking the 570 rectangles of the page where A is rendered." 571 572 (or (pdf-annot-get a 'markup-edges) 573 (list (pdf-annot-get a 'edges)))) 574 575 (defun pdf-annot-delete (a) 576 "Delete annotation A. 577 578 Sets A's buffer's modified flag and runs the hook 579 `pdf-annot-modified-functions'. 580 581 This function always returns nil." 582 (interactive 583 (list (pdf-annot-read-annotation 584 "Click on the annotation you wish to delete"))) 585 (with-current-buffer (pdf-annot-get-buffer a) 586 (pdf-info-delannot 587 (pdf-annot-get-id a)) 588 (set-buffer-modified-p t) 589 (pdf-annot-run-modified-hooks :delete a)) 590 (when (called-interactively-p 'any) 591 (message "Annotation deleted")) 592 nil) 593 594 (defun pdf-annot-text-annotation-p (a) 595 (eq 'text (pdf-annot-get a 'type))) 596 597 (defun pdf-annot-markup-annotation-p (a) 598 (not (null 599 (memq (pdf-annot-get a 'type) 600 pdf-annot-markup-annotation-types)))) 601 602 (defun pdf-annot-property-modifiable-p (a property) 603 (or (memq property '(edges color flags contents)) 604 (and (pdf-annot-markup-annotation-p a) 605 (memq property '(label opacity popup popup-is-open))) 606 (and (pdf-annot-text-annotation-p a) 607 (memq property '(icon is-open))))) 608 609 (defun pdf-annot-activate-annotation (a) 610 (or (run-hook-with-args-until-success 611 'pdf-annot-activate-handler-functions 612 a) 613 (pdf-annot-default-activate-handler a))) 614 615 (defun pdf-annot-default-activate-handler (a) 616 (cond 617 ((pdf-annot-has-attachment-p a) 618 (pdf-annot-pop-to-attachment a)) 619 (t (pdf-annot-edit-contents a)))) 620 621 622 ;; * ================================================================== * 623 ;; * Handling attachments 624 ;; * ================================================================== * 625 626 (defun pdf-annot-has-attachment-p (a) 627 "Return non-nil if annotation A's has data attached." 628 (eq 'file (pdf-annot-get a 'type))) 629 630 (defun pdf-annot-get-attachment (a &optional do-save) 631 "Retrieve annotation A's attachment. 632 633 The DO-SAVE argument is given to 634 `pdf-info-getattachment-from-annot', which see." 635 (unless (pdf-annot-has-attachment-p a) 636 (error "Annotation has no data attached: %s" a)) 637 (pdf-info-getattachment-from-annot 638 (pdf-annot-get-id a) 639 do-save 640 (pdf-annot-get-buffer a))) 641 642 (defun pdf-annot-attachment-base-directory () 643 "Return the base directory for saving attachments." 644 (let ((dir (pdf-util-expand-file-name "attachments"))) 645 (unless (file-exists-p dir) 646 (make-directory dir)) 647 dir)) 648 649 (defun pdf-annot-attachment-delete-base-directory () 650 "Delete all saved attachment files of the current buffer." 651 (setq pdf-annot--attachment-file-alist nil) 652 (delete-directory (pdf-annot-attachment-base-directory) t)) 653 654 (defun pdf-annot-attachment-unique-filename (attachment) 655 "Return a unique absolute filename for ATTACHMENT." 656 (let* ((filename (or (cdr (assq 'filename attachment)) 657 "attachment")) 658 (id (cdr (assq 'id attachment))) 659 (unique 660 (or (cdr (assoc id pdf-annot--attachment-file-alist)) 661 (let* ((sans-ext 662 (expand-file-name 663 (concat (file-name-as-directory ".") 664 (file-name-sans-extension filename)) 665 (pdf-annot-attachment-base-directory))) 666 (ext (file-name-extension filename)) 667 (newname (concat sans-ext "." ext)) 668 (i 0)) 669 (while (rassoc newname pdf-annot--attachment-file-alist) 670 (setq newname (format "%s-%d.%s" sans-ext (cl-incf i) ext))) 671 (push (cons id newname) pdf-annot--attachment-file-alist) 672 newname))) 673 (directory (file-name-directory unique))) 674 (unless (file-exists-p directory) 675 (make-directory directory t)) 676 unique)) 677 678 679 (defun pdf-annot-attachment-save (attachment &optional regenerate-p) 680 "Save ATTACHMENT's data to a unique filename and return it's name. 681 682 If REGENERATE-P is non-nil, copy attachment's file even if the 683 copy already exists. 684 685 Signal an error, if ATTACHMENT has no, or a non-existing, `file' 686 property, i.e. it was retrieved with an unset do-save argument. 687 See `pdf-info-getattachments'" 688 689 (let ((datafile (cdr (assq 'file attachment)))) 690 (unless (and datafile 691 (file-exists-p datafile)) 692 (error "Attachment's file property is invalid")) 693 (let* ((filename 694 (pdf-annot-attachment-unique-filename attachment))) 695 (when (or regenerate-p 696 (not (file-exists-p filename))) 697 (copy-file datafile filename nil nil t t)) 698 filename))) 699 700 (defun pdf-annot-find-attachment-noselect (a) 701 "Find annotation A's attachment in a buffer, without selecting it. 702 703 Signals an error, if A has no data attached." 704 (let ((attachment (pdf-annot-get-attachment a t))) 705 (unwind-protect 706 (find-file-noselect 707 (pdf-annot-attachment-save attachment)) 708 (let ((tmpfile (cdr (assq 'file attachment)))) 709 (when (and tmpfile 710 (file-exists-p tmpfile)) 711 (delete-file tmpfile)))))) 712 713 (defun pdf-annot-attachment-dired (&optional regenerate-p) 714 "List all attachments in a dired buffer. 715 716 If REGENERATE-P is non-nil, create attachment's files even if 717 they already exist. Interactively REGENERATE-P is non-nil if a 718 prefix argument was given. 719 720 Return the dired buffer." 721 (interactive (list current-prefix-arg)) 722 (let ((attachments (pdf-info-getattachments t))) 723 (unwind-protect 724 (progn 725 (dolist (a (pdf-annot-getannots nil 'file)) 726 (push (pdf-annot-get-attachment a t) 727 attachments )) 728 (dolist (att attachments) 729 (pdf-annot-attachment-save att regenerate-p)) 730 (unless attachments 731 (error "Document has no data attached")) 732 (dired (pdf-annot-attachment-base-directory))) 733 (dolist (att attachments) 734 (let ((tmpfile (cdr (assq 'file att)))) 735 (when (and tmpfile (file-exists-p tmpfile)) 736 (delete-file tmpfile))))))) 737 738 (defun pdf-annot-display-attachment (a &optional display-action select-window-p) 739 "Display file annotation A's data in a buffer. 740 741 DISPLAY-ACTION should be a valid `display-buffer' action. If 742 nil, `pdf-annot-attachment-display-buffer-action' is used. 743 744 Select the window, if SELECT-WINDOW-P is non-nil. 745 746 Return the window attachment is displayed in." 747 748 (interactive 749 (list (pdf-annot-read-annotation 750 "Select a file annotation by clicking on it"))) 751 (let* ((buffer (pdf-annot-find-attachment-noselect a)) 752 (window (display-buffer 753 buffer (or display-action 754 pdf-annot-attachment-display-buffer-action)))) 755 (when select-window-p 756 (select-window window)) 757 window)) 758 759 (defun pdf-annot-pop-to-attachment (a) 760 "Display annotation A's attachment in a window and select it." 761 (interactive 762 (list (pdf-annot-read-annotation 763 "Select a file annotation by clicking on it"))) 764 (pdf-annot-display-attachment a nil t)) 765 766 767 ;; * ================================================================== * 768 ;; * Interfacing with the display 769 ;; * ================================================================== * 770 771 (defun pdf-annot-image-position (a &optional image-size) 772 "Return the position of annotation A in image coordinates. 773 774 IMAGE-SIZE should be a cons \(WIDTH . HEIGHT\) and defaults to 775 the page-image of the selected window." 776 777 (unless image-size 778 (pdf-util-assert-pdf-window) 779 (setq image-size (pdf-view-image-size))) 780 (let ((e (pdf-util-scale 781 (pdf-annot-get a 'edges) 782 image-size))) 783 (pdf-util-with-edges (e) 784 `(,e-left . ,e-top)))) 785 786 (defun pdf-annot-image-set-position (a x y &optional image-size) 787 "Set annotation A's position to X,Y in image coordinates. 788 789 See `pdf-annot-image-position' for IMAGE-SIZE." 790 791 (unless image-size 792 (pdf-util-assert-pdf-window) 793 (setq image-size (pdf-view-image-size))) 794 (let* ((edges (pdf-annot-get a 'edges)) 795 (x (/ x (float (car image-size)))) 796 (y (/ y (float (cdr image-size))))) 797 (pdf-util-with-edges (edges) 798 (let* ((w edges-width) 799 (h edges-height) 800 (x (max 0 (min x (- 1 w)))) 801 (y (max 0 (min y (- 1 h))))) 802 (pdf-annot-put a 'edges 803 (list x y -1 -1)))))) 804 805 (defun pdf-annot-image-size (a &optional image-size) 806 "Return the size of annotation A in image coordinates. 807 808 Returns \(WIDTH . HEIGHT\). 809 810 See `pdf-annot-image-position' for IMAGE-SIZE." 811 (unless image-size 812 (pdf-util-assert-pdf-window) 813 (setq image-size (pdf-view-image-size))) 814 (let ((edges (pdf-util-scale 815 (pdf-annot-get a 'edges) image-size))) 816 (pdf-util-with-edges (edges) 817 (cons edges-width edges-height)))) 818 819 (defun pdf-annot-image-set-size (a &optional width height image-size) 820 "Set annotation A's size in image to WIDTH and/or HEIGHT. 821 822 See `pdf-annot-image-position' for IMAGE-SIZE." 823 (unless image-size 824 (pdf-util-assert-pdf-window) 825 (setq image-size (pdf-view-image-size))) 826 (let* ((edges (pdf-annot-get a 'edges)) 827 (w (and width 828 (/ width (float (car image-size))))) 829 (h (and height 830 (/ height (float (cdr image-size)))))) 831 (pdf-util-with-edges (edges) 832 (pdf-annot-put a 'edges 833 (list edges-left 834 edges-top 835 (if w (+ edges-left w) edges-right) 836 (if h (+ edges-top h) edges-bot)))))) 837 838 (defun pdf-annot-at-position (pos) 839 "Return annotation at POS in the selected window. 840 841 POS should be an absolute image position as a cons \(X . Y\). 842 Alternatively POS may also be an event position, in which case 843 `posn-window' and `posn-object-x-y' is used to find the image 844 position. 845 846 Return nil, if no annotation was found." 847 (let (window) 848 (when (posnp pos) 849 (setq window (posn-window pos) 850 pos (posn-object-x-y pos))) 851 (save-selected-window 852 (when window (select-window window 'norecord)) 853 (let* ((annots (pdf-annot-getannots (pdf-view-current-page))) 854 (size (pdf-view-image-size)) 855 (rx (/ (car pos) (float (car size)))) 856 (ry (/ (cdr pos) (float (cdr size)))) 857 (rpos (cons rx ry))) 858 (or (cl-some (lambda (a) 859 (and (cl-some 860 (lambda (e) 861 (pdf-util-edges-inside-p e rpos)) 862 (pdf-annot-get-display-edges a)) 863 a)) 864 annots) 865 (error "No annotation at this position")))))) 866 867 (defun pdf-annot-mouse-move (event &optional annot) 868 "Start moving an annotation at EVENT's position. 869 870 EVENT should be a mouse event originating the request and is used 871 as a reference point. 872 873 ANNOT is the annotation to operate on and defaults to the 874 annotation at EVENT's start position. 875 876 This function does not return until the operation is completed, 877 i.e. a non mouse-movement event is read." 878 879 (interactive "@e") 880 (pdf-util-assert-pdf-window (posn-window (event-start event))) 881 (select-window (posn-window (event-start event))) 882 (let* ((mpos (posn-object-x-y (event-start event))) 883 (a (or annot 884 (pdf-annot-at-position mpos)))) 885 (unless a 886 (error "No annotation at this position: %s" mpos)) 887 (let* ((apos (pdf-annot-image-position a)) 888 (offset (cons (- (car mpos) (car apos)) 889 (- (cdr mpos) (cdr apos)))) 890 (window (selected-window)) 891 make-pointer-invisible) 892 (when (pdf-util-track-mouse-dragging (ev 0.1) 893 (when (and (eq window (posn-window (event-start ev))) 894 (eq 'image (car-safe (posn-object (event-start ev))))) 895 (let ((pdf-view-inhibit-hotspots t) 896 (pdf-annot-inhibit-modification-hooks t) 897 (pdf-cache-image-inihibit t) 898 (xy (posn-object-x-y (event-start ev)))) 899 (pdf-annot-image-set-position 900 a (- (car xy) (car offset)) 901 (- (cdr xy) (cdr offset))) 902 (pdf-view-redisplay)))) 903 (pdf-annot-run-modified-hooks))) 904 nil)) 905 906 (defun pdf-annot-hotspot-function (page size) 907 "Create image hotspots for page PAGE of size SIZE." 908 (apply 'nconc (mapcar (lambda (a) 909 (unless (eq (pdf-annot-get a 'type) 910 'link) 911 (pdf-annot-create-hotspots a size))) 912 (pdf-annot-getannots page)))) 913 914 (defun pdf-annot-create-hotspots (a size) 915 "Return a list of image hotspots for annotation A." 916 (let ((id (pdf-annot-get-id a)) 917 (edges (pdf-util-scale 918 (pdf-annot-get-display-edges a) 919 size 'round)) 920 (moveable-p (memq (pdf-annot-get a 'type) 921 '(file text))) 922 hotspots) 923 (dolist (e edges) 924 (pdf-util-with-edges (e) 925 (push `((rect . ((,e-left . ,e-top) . (,e-right . ,e-bot))) 926 ,id 927 (pointer 928 hand 929 help-echo 930 ,(pdf-annot-print-annotation a))) 931 hotspots))) 932 (pdf-annot-create-hotspot-binding id moveable-p a) 933 hotspots)) 934 935 ;; FIXME: Define a keymap as a template for this. Much cleaner. 936 (defun pdf-annot-create-hotspot-binding (id moveable-p annotation) 937 ;; Activating 938 (local-set-key 939 (vector id 'mouse-1) 940 (lambda () 941 (interactive) 942 (pdf-annot-activate-annotation annotation))) 943 ;; Move 944 (when moveable-p 945 (local-set-key 946 (vector id 'down-mouse-1) 947 (lambda (ev) 948 (interactive "@e") 949 (pdf-annot-mouse-move ev annotation)))) 950 ;; Context Menu 951 (local-set-key 952 (vector id 'down-mouse-3) 953 (lambda () 954 (interactive "@") 955 (popup-menu (pdf-annot-create-context-menu annotation)))) 956 ;; Everything else 957 (local-set-key 958 (vector id t) 959 'pdf-util-image-map-mouse-event-proxy)) 960 961 (defun pdf-annot-show-annotation (a &optional highlight-p window) 962 "Make annotation A visible. 963 964 Turn to A's page in WINDOW, and scroll it if necessary. 965 966 If HIGHLIGHT-P is non-nil, visually distinguish annotation A from 967 other annotations." 968 969 (save-selected-window 970 (when window (select-window window 'norecord)) 971 (pdf-util-assert-pdf-window) 972 (let ((page (pdf-annot-get a 'page)) 973 (size (pdf-view-image-size))) 974 (unless (= page (pdf-view-current-page)) 975 (pdf-view-goto-page page)) 976 (let ((edges (pdf-annot-get-display-edges a))) 977 (when highlight-p 978 (pdf-view-display-image 979 (pdf-view-create-image 980 (pdf-cache-renderpage-highlight 981 page (car size) 982 `("white" "steel blue" 0.35 ,@edges)) 983 :map (pdf-view-apply-hotspot-functions 984 window page size) 985 :width (car size)))) 986 (pdf-util-scroll-to-edges 987 (pdf-util-scale-relative-to-pixel (car edges))))))) 988 989 (defun pdf-annot-read-annotation (&optional prompt) 990 "Let the user choose a annotation a mouse click using PROMPT." 991 (pdf-annot-at-position 992 (pdf-util-read-image-position 993 (or prompt "Choose a annotation by clicking on it")))) 994 995 996 ;; * ================================================================== * 997 ;; * Creating annotations 998 ;; * ================================================================== * 999 1000 (defun pdf-annot-add-annotation (type edges &optional property-alist page) 1001 "Creates and adds a new annotation of type TYPE to the document. 1002 1003 TYPE determines the kind of annotation to add and maybe one of 1004 `text', `squiggly', `underline', `strike-out' or `highlight'. 1005 1006 EDGES determines where the annotation will appear on the page. 1007 If type is `text', this should be a single list of \(LEFT TOP 1008 RIGHT BOT\). Though, in this case only LEFT and TOP are used, 1009 since the size of text annotations is fixed. Otherwise EDGES may 1010 be a list of such elements. All values should be image relative 1011 coordinates, i.e. in the range \[0;1\]. 1012 1013 PROPERTY-ALIST is a list of annotation properties, which will be 1014 put on the created annotation. 1015 1016 PAGE determines the page of the annotation. It defaults to the 1017 page currently displayed in the selected window. 1018 1019 Signal an error, if PROPERTY-ALIST contains non-modifiable 1020 properties or PAGE is nil and the selected window does not 1021 display a PDF document or creating annotations of type TYPE is 1022 not supported. 1023 1024 Set buffers modified flag and calls 1025 `pdf-annot-activate-annotation' if 1026 `pdf-annot-activate-created-annotations' is non-nil. 1027 1028 Return the new annotation." 1029 1030 (unless (memq type (pdf-info-creatable-annotation-types)) 1031 (error "Unsupported annotation type: %s" type)) 1032 (unless page 1033 (pdf-util-assert-pdf-window) 1034 (setq page (pdf-view-current-page))) 1035 (unless (consp (car-safe edges)) 1036 (setq edges (list edges))) 1037 (when (and (eq type 'text) 1038 (> (length edges) 1)) 1039 (error "Edges argument should be a single edge-list for text annotations")) 1040 (let* ((a (apply 'pdf-info-addannot 1041 page 1042 (if (eq type 'text) 1043 (car edges) 1044 (apply #'pdf-util-edges-union 1045 (apply #'append 1046 (mapcar 1047 (lambda (e) 1048 (pdf-info-getselection page e)) 1049 edges)))) 1050 type 1051 nil 1052 (if (not (eq type 'text)) edges))) 1053 (id (pdf-annot-get-id a))) 1054 (when property-alist 1055 (condition-case err 1056 (setq a (pdf-info-editannot id property-alist)) 1057 (error 1058 (pdf-info-delannot id) 1059 (signal (car err) (cdr err))))) 1060 (setq a (pdf-annot-create a)) 1061 (set-buffer-modified-p t) 1062 (pdf-annot-run-modified-hooks :insert a) 1063 (when pdf-annot-activate-created-annotations 1064 (pdf-annot-activate-annotation a)) 1065 a)) 1066 1067 (defun pdf-annot-add-text-annotation (pos &optional icon property-alist) 1068 "Add a new text annotation at POS in the selected window. 1069 1070 POS should be a image position object or a cons \(X . Y\), both 1071 being image coordinates. 1072 1073 ICON determines how the annotation is displayed and should be 1074 listed in `pdf-annot-standard-text-icons'. Any other value is ok 1075 as well, but will render the annotation invisible. 1076 1077 Adjust X and Y accordingly, if the position would render the 1078 annotation off-page. 1079 1080 Merge ICON as a icon property with PROPERTY-ALIST and 1081 `pdf-annot-default-text-annotation-properties' and apply the 1082 result to the created annotation. 1083 1084 See also `pdf-annot-add-annotation'. 1085 1086 Return the new annotation." 1087 1088 (interactive 1089 (let* ((posn (pdf-util-read-image-position 1090 "Click where a new text annotation should be added ...")) 1091 (window (posn-window posn))) 1092 (select-window window) 1093 (list posn))) 1094 (pdf-util-assert-pdf-window) 1095 (when (posnp pos) 1096 (setq pos (posn-object-x-y pos))) 1097 (let ((isize (pdf-view-image-size)) 1098 (x (car pos)) 1099 (y (cdr pos))) 1100 (unless (and (>= x 0) 1101 (< x (car isize))) 1102 (signal 'args-out-of-range (list pos))) 1103 (unless (and (>= y 0) 1104 (< y (cdr isize))) 1105 (signal 'args-out-of-range (list pos))) 1106 (let ((size (pdf-util-scale-points-to-pixel 1107 pdf-annot-text-annotation-size 'round))) 1108 (setcar size (min (car size) (car isize))) 1109 (setcdr size (min (cdr size) (cdr isize))) 1110 (cl-decf x (max 0 (- (+ x (car size)) (car isize)))) 1111 (cl-decf y (max 0 (- (+ y (cdr size)) (cdr isize)))) 1112 (pdf-annot-add-annotation 1113 'text (pdf-util-scale-pixel-to-relative 1114 (list x y -1 -1)) 1115 (pdf-annot-merge-alists 1116 (and icon `((icon . ,icon))) 1117 property-alist 1118 pdf-annot-default-text-annotation-properties 1119 (cdr (assq 'text pdf-annot-default-annotation-properties)) 1120 (cdr (assq t pdf-annot-default-annotation-properties)) 1121 `((color . ,(car pdf-annot-color-history)))))))) 1122 1123 (defun pdf-annot-mouse-add-text-annotation (ev) 1124 (interactive "@e") 1125 (pdf-annot-add-text-annotation 1126 (if (eq (car-safe ev) 1127 'menu-bar) 1128 (let (echo-keystrokes) 1129 (message nil) 1130 (pdf-util-read-image-position 1131 "Click where a new text annotation should be added ...")) 1132 (event-start ev)))) 1133 1134 (defun pdf-annot-add-markup-annotation (list-of-edges type &optional color 1135 property-alist) 1136 "Add a new markup annotation in the selected window. 1137 1138 LIST-OF-EDGES determines the marked up area and should be a list 1139 of \(LEFT TOP RIGHT BOT\), each value a relative coordinate. 1140 1141 TYPE should be one of `squiggly', `underline', `strike-out' or 1142 `highlight'. 1143 1144 Merge COLOR as a color property with PROPERTY-ALIST and 1145 `pdf-annot-default-markup-annotation-properties' and apply the 1146 result to the created annotation. 1147 1148 See also `pdf-annot-add-annotation'. 1149 1150 Return the new annotation." 1151 (interactive 1152 (list (pdf-view-active-region t) 1153 (let ((type (completing-read "Markup type (default highlight): " 1154 '("squiggly" "highlight" "underline" "strike-out") 1155 nil t))) 1156 (if (equal type "") 'highlight (intern type))) 1157 (pdf-annot-read-color))) 1158 (pdf-util-assert-pdf-window) 1159 (pdf-annot-add-annotation 1160 type 1161 list-of-edges 1162 (pdf-annot-merge-alists 1163 (and color `((color . ,color))) 1164 property-alist 1165 pdf-annot-default-markup-annotation-properties 1166 (cdr (assq type pdf-annot-default-annotation-properties)) 1167 (cdr (assq t pdf-annot-default-annotation-properties)) 1168 (when pdf-annot-color-history 1169 `((color . ,(car pdf-annot-color-history)))) 1170 '((color . "#ffff00"))) 1171 (pdf-view-current-page))) 1172 1173 (defun pdf-annot-add-squiggly-markup-annotation (list-of-edges 1174 &optional color property-alist) 1175 "Add a new squiggly annotation in the selected window. 1176 1177 See also `pdf-annot-add-markup-annotation'." 1178 (interactive (list (pdf-view-active-region t))) 1179 (pdf-annot-add-markup-annotation list-of-edges 'squiggly color property-alist)) 1180 1181 (defun pdf-annot-add-underline-markup-annotation (list-of-edges 1182 &optional color property-alist) 1183 "Add a new underline annotation in the selected window. 1184 1185 See also `pdf-annot-add-markup-annotation'." 1186 (interactive (list (pdf-view-active-region t))) 1187 (pdf-annot-add-markup-annotation list-of-edges 'underline color property-alist)) 1188 1189 (defun pdf-annot-add-strikeout-markup-annotation (list-of-edges 1190 &optional color property-alist) 1191 "Add a new strike-out annotation in the selected window. 1192 1193 See also `pdf-annot-add-markup-annotation'." 1194 (interactive (list (pdf-view-active-region t))) 1195 (pdf-annot-add-markup-annotation list-of-edges 'strike-out color property-alist)) 1196 1197 (defun pdf-annot-add-highlight-markup-annotation (list-of-edges 1198 &optional color property-alist) 1199 "Add a new highlight annotation in the selected window. 1200 1201 See also `pdf-annot-add-markup-annotation'." 1202 (interactive (list (pdf-view-active-region t))) 1203 (pdf-annot-add-markup-annotation list-of-edges 'highlight color property-alist)) 1204 1205 (defun pdf-annot-read-color (&optional prompt) 1206 "Read and return a color using PROMPT. 1207 1208 Offer `pdf-annot-color-history' as default values." 1209 (let* ((defaults (append 1210 (delq nil 1211 (list 1212 (cdr (assq 'color 1213 pdf-annot-default-markup-annotation-properties)) 1214 (cdr (assq 'color 1215 pdf-annot-default-text-annotation-properties)))) 1216 pdf-annot-color-history)) 1217 (prompt 1218 (format "%s%s: " 1219 (or prompt "Color") 1220 (if defaults (format " (default %s)" (car defaults)) ""))) 1221 (current-completing-read-function completing-read-function) 1222 (completing-read-function 1223 (lambda (prompt collection &optional predicate require-match 1224 initial-input _hist _def inherit-input-method) 1225 (funcall current-completing-read-function 1226 prompt collection predicate require-match 1227 initial-input 'pdf-annot-color-history 1228 defaults 1229 inherit-input-method)))) 1230 (read-color prompt))) 1231 1232 (defun pdf-annot-merge-alists (&rest alists) 1233 "Merge ALISTS into a single one. 1234 1235 Suppresses successive duplicate entries of keys after the first 1236 occurrence in ALISTS." 1237 1238 (let (merged) 1239 (dolist (elt (apply 'append alists)) 1240 (unless (assq (car elt) merged) 1241 (push elt merged))) 1242 (nreverse merged))) 1243 1244 1245 1246 ;; * ================================================================== * 1247 ;; * Displaying annotation contents 1248 ;; * ================================================================== * 1249 1250 (defun pdf-annot-print-property (a property) 1251 "Pretty print annotation A's property PROPERTY." 1252 (let ((value (pdf-annot-get a property))) 1253 (cl-case property 1254 (color 1255 (propertize (or value "") 1256 'face (and value 1257 `(:background ,value)))) 1258 ((created modified) 1259 (let ((date value)) 1260 (if (null date) 1261 "No date" 1262 (current-time-string date)))) 1263 ;; print verbatim 1264 (subject 1265 (or value "No subject")) 1266 (opacity 1267 (let ((opacity (or value 1.0))) 1268 (format "%d%%" (round (* 100 opacity))))) 1269 (t (format "%s" (or value "")))))) 1270 1271 (defun pdf-annot-print-annotation (a) 1272 "Pretty print annotation A." 1273 (or (run-hook-with-args-until-success 1274 'pdf-annot-print-annotation-functions a) 1275 (pdf-annot-print-annotation-default a))) 1276 1277 (defun pdf-annot-print-annotation-default (a) 1278 "Default pretty printer for annotation A. 1279 1280 The result consists of a header (as printed with 1281 `pdf-annot-print-annotation-header') a newline and A's contents 1282 property." 1283 (concat 1284 (pdf-annot-print-annotation-header a) 1285 "\n" 1286 (pdf-annot-get a 'contents))) 1287 1288 (defun pdf-annot-print-annotation-header (a) 1289 "Emit a suitable header string for annotation A." 1290 (let ((header 1291 (cond 1292 ((eq 'file (pdf-annot-get a 'type)) 1293 (let ((att (pdf-annot-get-attachment a))) 1294 (format "File attachment `%s' of %s" 1295 (or (cdr (assq 'filename att)) "unnamed") 1296 (if (cdr (assq 'size att)) 1297 (format "size %s" (file-size-human-readable 1298 (cdr (assq 'size att)))) 1299 "unknown size")))) 1300 (t 1301 (format "%s" 1302 (mapconcat 1303 'identity 1304 (mapcar 1305 (lambda (property) 1306 (pdf-annot-print-property 1307 a property)) 1308 `(subject 1309 label 1310 modified)) 1311 ";")))))) 1312 (setq header (propertize header 'face 'header-line 1313 'intangible t 'read-only t)) 1314 ;; This `trick' makes the face apply in a tooltip. 1315 (propertize header 'display header))) 1316 1317 (defun pdf-annot-print-annotation-latex-maybe (a) 1318 "Maybe print annotation A's content as a LaTeX fragment. 1319 1320 See `pdf-annot-latex-string-predicate'." 1321 (when (and (functionp pdf-annot-latex-string-predicate) 1322 (funcall pdf-annot-latex-string-predicate 1323 (pdf-annot-get a 'contents))) 1324 (pdf-annot-print-annotation-latex a))) 1325 1326 (defun pdf-annot-print-annotation-latex (a) 1327 "Print annotation A's content as a LaTeX fragment. 1328 1329 This compiles A's contents as a LaTeX fragment and puts the 1330 resulting image as a display property on the contents, prefixed 1331 by a header." 1332 1333 (let (tempfile) 1334 (unwind-protect 1335 (with-current-buffer (pdf-annot-get-buffer a) 1336 (let* ((page (pdf-annot-get a 'page)) 1337 (header (pdf-annot-print-annotation-header a)) 1338 (contents (pdf-annot-get a 'contents)) 1339 (hash (sxhash (format 1340 "pdf-annot-print-annotation-latex%s%s%s" 1341 page header contents))) 1342 (data (pdf-cache-lookup-image page 0 nil hash)) 1343 (org-format-latex-header 1344 pdf-annot-latex-header) 1345 (temporary-file-directory 1346 (pdf-util-expand-file-name "pdf-annot-print-annotation-latex"))) 1347 (unless (file-directory-p temporary-file-directory) 1348 (make-directory temporary-file-directory)) 1349 (unless data 1350 (setq tempfile (make-temp-file "pdf-annot" nil ".png")) 1351 ;; FIXME: Why is this with-temp-buffer needed (which it is) ? 1352 (with-temp-buffer 1353 (org-create-formula-image 1354 contents tempfile org-format-latex-options t)) 1355 (setq data (pdf-util-munch-file tempfile)) 1356 (if (and (> (length data) 3) 1357 (equal (substring data 1 4) 1358 "PNG")) 1359 (pdf-cache-put-image page 0 data hash) 1360 (setq data nil))) 1361 (concat 1362 header 1363 "\n" 1364 (if data 1365 (propertize 1366 contents 'display (pdf-view-create-image data)) 1367 (propertize 1368 contents 1369 'display 1370 (concat 1371 (propertize "Failed to compile latex fragment\n" 1372 'face 'error) 1373 contents)))))) 1374 (when (and tempfile 1375 (file-exists-p tempfile)) 1376 (delete-file tempfile))))) 1377 1378 1379 ;; * ================================================================== * 1380 ;; * Editing annotation contents 1381 ;; * ================================================================== * 1382 1383 (defvar-local pdf-annot-edit-contents--annotation nil) 1384 (put 'pdf-annot-edit-contents--annotation 'permanent-local t) 1385 (defvar-local pdf-annot-edit-contents--buffer nil) 1386 1387 (defcustom pdf-annot-edit-contents-setup-function 1388 (lambda (a) 1389 (let ((mode (if (funcall pdf-annot-latex-string-predicate 1390 (pdf-annot-get a 'contents)) 1391 'latex-mode 1392 'text-mode))) 1393 (unless (derived-mode-p mode) 1394 (funcall mode)))) 1395 "A function for setting up, e.g. the major-mode, of the edit buffer. 1396 1397 The function receives one argument, the annotation whose contents 1398 is about to be edited in this buffer. 1399 1400 The default value turns on `latex-mode' if 1401 `pdf-annot-latex-string-predicate' returns non-nil on the 1402 annotation's contents and otherwise `text-mode'. " 1403 :group 'pdf-annot 1404 :type 'function) 1405 1406 (defcustom pdf-annot-edit-contents-display-buffer-action 1407 '((display-buffer-reuse-window 1408 display-buffer-split-below-and-attach) 1409 (inhibit-same-window . t) 1410 (window-height . 0.25)) 1411 "Display action when showing the edit buffer." 1412 :group 'pdf-annot 1413 :type display-buffer--action-custom-type) 1414 1415 (defvar pdf-annot-edit-contents-minor-mode-map 1416 (let ((kmap (make-sparse-keymap))) 1417 (set-keymap-parent kmap text-mode-map) 1418 (define-key kmap (kbd "C-c C-c") 'pdf-annot-edit-contents-commit) 1419 (define-key kmap (kbd "C-c C-q") 'pdf-annot-edit-contents-abort) 1420 kmap)) 1421 1422 (define-minor-mode pdf-annot-edit-contents-minor-mode 1423 "Active when editing the contents of annotations." 1424 :group 'pdf-annot 1425 (when pdf-annot-edit-contents-minor-mode 1426 (message "%s" 1427 (substitute-command-keys 1428 "Press \\[pdf-annot-edit-contents-commit] to commit your changes, \\[pdf-annot-edit-contents-abort] to abandon them.")))) 1429 1430 (put 'pdf-annot-edit-contents-minor-mode 'permanent-local t) 1431 1432 ;; FIXME: Document pdf-annot-edit-* functions below. 1433 (defun pdf-annot-edit-contents-finalize (do-save &optional do-kill) 1434 (when (buffer-modified-p) 1435 (cond 1436 ((eq do-save 'ask) 1437 (save-window-excursion 1438 (display-buffer (current-buffer) nil (selected-frame)) 1439 (when (y-or-n-p "Save changes to this annotation ?") 1440 (pdf-annot-edit-contents-save-annotation)))) 1441 (do-save 1442 (pdf-annot-edit-contents-save-annotation))) 1443 (set-buffer-modified-p nil)) 1444 (dolist (win (get-buffer-window-list)) 1445 (quit-window do-kill win))) 1446 1447 (defun pdf-annot-edit-contents-save-annotation () 1448 (when pdf-annot-edit-contents--annotation 1449 (pdf-annot-put pdf-annot-edit-contents--annotation 1450 'contents 1451 (buffer-substring-no-properties (point-min) (point-max))) 1452 (set-buffer-modified-p nil))) 1453 1454 (defun pdf-annot-edit-contents-commit () 1455 (interactive) 1456 (pdf-annot-edit-contents-finalize t)) 1457 1458 (defun pdf-annot-edit-contents-abort () 1459 (interactive) 1460 (pdf-annot-edit-contents-finalize nil t)) 1461 1462 (defun pdf-annot-edit-contents-noselect (a) 1463 (with-current-buffer (pdf-annot-get-buffer a) 1464 (when (and (buffer-live-p pdf-annot-edit-contents--buffer) 1465 (not (eq a pdf-annot-edit-contents--annotation))) 1466 (with-current-buffer pdf-annot-edit-contents--buffer 1467 (pdf-annot-edit-contents-finalize 'ask))) 1468 (unless (buffer-live-p pdf-annot-edit-contents--buffer) 1469 (setq pdf-annot-edit-contents--buffer 1470 (with-current-buffer (get-buffer-create 1471 (format "*Edit Annotation %s*" 1472 (buffer-name))) 1473 (pdf-annot-edit-contents-minor-mode 1) 1474 (current-buffer)))) 1475 (with-current-buffer pdf-annot-edit-contents--buffer 1476 (let ((inhibit-read-only t)) 1477 (erase-buffer) 1478 (save-excursion (insert (pdf-annot-get a 'contents))) 1479 (set-buffer-modified-p nil)) 1480 (setq pdf-annot-edit-contents--annotation a) 1481 (funcall pdf-annot-edit-contents-setup-function a) 1482 (current-buffer)))) 1483 1484 (defun pdf-annot-edit-contents (a) 1485 (select-window 1486 (display-buffer 1487 (pdf-annot-edit-contents-noselect a) 1488 pdf-annot-edit-contents-display-buffer-action))) 1489 1490 (defun pdf-annot-edit-contents-mouse (ev) 1491 (interactive "@e") 1492 (let* ((pos (posn-object-x-y (event-start ev))) 1493 (a (and pos (pdf-annot-at-position pos)))) 1494 (unless a 1495 (error "No annotation at this position")) 1496 (pdf-annot-edit-contents a))) 1497 1498 1499 1500 ;; * ================================================================== * 1501 ;; * Listing annotations 1502 ;; * ================================================================== * 1503 1504 (defcustom pdf-annot-list-display-buffer-action 1505 '((display-buffer-reuse-window 1506 display-buffer-pop-up-window) 1507 (inhibit-same-window . t)) 1508 "Display action used when displaying the list buffer." 1509 :group 'pdf-annot 1510 :type display-buffer--action-custom-type) 1511 1512 (defcustom pdf-annot-list-format 1513 '((page . 3) 1514 (type . 10) 1515 (label . 24) 1516 (date . 24)) 1517 "Annotation properties visible in the annotation list. 1518 1519 It should be a list of \(PROPERTIZE. WIDTH\), where PROPERTY is a 1520 symbol naming one of supported properties to list and WIDTH its 1521 desired column-width. 1522 1523 Currently supported properties are page, type, label, date and contents." 1524 :type '(alist :key-type (symbol)) 1525 :options '((page (integer :value 3 :tag "Column Width")) 1526 (type (integer :value 10 :tag "Column Width" )) 1527 (label (integer :value 24 :tag "Column Width")) 1528 (date (integer :value 24 :tag "Column Width")) 1529 (contents (integer :value 56 :tag "Column Width"))) 1530 :group 'pdf-annot) 1531 1532 (defcustom pdf-annot-list-highlight-type nil 1533 "Whether to highlight \"Type\" column annotation list with annotation color." 1534 :group 'pdf-annot 1535 :type 'boolean) 1536 1537 (defvar-local pdf-annot-list-buffer nil) 1538 1539 (defvar-local pdf-annot-list-document-buffer nil) 1540 1541 (defvar pdf-annot-list-mode-map 1542 (let ((km (make-sparse-keymap))) 1543 (define-key km (kbd "C-c C-f") 'pdf-annot-list-follow-minor-mode) 1544 (define-key km (kbd "SPC") 'pdf-annot-list-display-annotation-from-id) 1545 km)) 1546 1547 (defun pdf-annot-property-completions (property) 1548 "Return a list of completion candidates for annotation property PROPERTY. 1549 1550 Return nil, if not available." 1551 (cl-case property 1552 (color (pdf-util-color-completions)) 1553 (icon (copy-sequence pdf-annot-standard-text-icons)))) 1554 1555 (defun pdf-annot-compare-annotations (a1 a2) 1556 "Compare annotations A1 and A2. 1557 1558 Return non-nil if A1's page is less than A2's one or if they 1559 belong to the same page and A1 is displayed above/left of A2." 1560 (let ((p1 (pdf-annot-get a1 'page)) 1561 (p2 (pdf-annot-get a2 'page))) 1562 (or (< p1 p2) 1563 (and (= p1 p2) 1564 (let ((e1 (pdf-util-scale 1565 (car (pdf-annot-get-display-edges a1)) 1566 '(1000 . 1000))) 1567 (e2 (pdf-util-scale 1568 (car (pdf-annot-get-display-edges a2)) 1569 '(1000 . 1000)))) 1570 (pdf-util-with-edges (e1 e2) 1571 (or (< e1-top e2-top) 1572 (and (= e1-top e2-top) 1573 (<= e1-left e2-left))))))))) 1574 1575 (defun pdf-annot-list-entries () 1576 (unless (buffer-live-p pdf-annot-list-document-buffer) 1577 (error "No PDF document associated with this buffer")) 1578 (mapcar 'pdf-annot-list-create-entry 1579 (sort (pdf-annot-getannots nil pdf-annot-list-listed-types 1580 pdf-annot-list-document-buffer) 1581 'pdf-annot-compare-annotations))) 1582 1583 (defun pdf-annot--make-entry-formatter (a) 1584 (lambda (fmt) 1585 (let ((entry-type (car fmt)) 1586 (entry-width (cdr fmt)) 1587 ;; Taken from css-mode.el 1588 (contrasty-color 1589 (lambda (name) 1590 (if (> (color-distance name "black") 292485) 1591 "black" "white"))) 1592 (prune-newlines 1593 (lambda (str) 1594 (replace-regexp-in-string "\n" " " str t t)))) 1595 (cl-ecase entry-type 1596 (date (pdf-annot-print-property a 'modified)) 1597 (page (pdf-annot-print-property a 'page)) 1598 (label (funcall prune-newlines 1599 (pdf-annot-print-property a 'label))) 1600 (contents 1601 (truncate-string-to-width 1602 (funcall prune-newlines 1603 (pdf-annot-print-property a 'contents)) 1604 entry-width)) 1605 (type 1606 (let ((color (pdf-annot-get a 'color)) 1607 (type (pdf-annot-print-property a 'type))) 1608 (if pdf-annot-list-highlight-type 1609 (propertize 1610 type 'face 1611 `(:background ,color 1612 :foreground ,(funcall contrasty-color color))) 1613 type))))))) 1614 1615 (defun pdf-annot-list-create-entry (a) 1616 "Create a `tabulated-list-entries' entry for annotation A." 1617 (list (pdf-annot-get-id a) 1618 (vconcat 1619 (mapcar (pdf-annot--make-entry-formatter a) 1620 pdf-annot-list-format)))) 1621 1622 (define-derived-mode pdf-annot-list-mode tablist-mode "Annots" 1623 (let* ((page-sorter 1624 (lambda (a b) 1625 (< (string-to-number (aref (cadr a) 0)) 1626 (string-to-number (aref (cadr b) 0))))) 1627 (format-generator 1628 (lambda (format) 1629 (let ((field (car format)) 1630 (width (cdr format))) 1631 (cl-case field 1632 (page `("Pg." 3 ,page-sorter :read-only t :right-alight t)) 1633 (t (list 1634 (capitalize (symbol-name field)) 1635 width t :read-only t))))))) 1636 (setq tabulated-list-entries 'pdf-annot-list-entries 1637 tabulated-list-format (vconcat 1638 (mapcar 1639 format-generator 1640 pdf-annot-list-format)) 1641 tabulated-list-padding 2)) 1642 (set-keymap-parent pdf-annot-list-mode-map tablist-mode-map) 1643 (use-local-map pdf-annot-list-mode-map) 1644 (when (assq 'type pdf-annot-list-format) 1645 (setq tablist-current-filter 1646 `(not (== "Type" "link")))) 1647 (tabulated-list-init-header)) 1648 1649 (defun pdf-annot-list-annotations () 1650 "List annotations in a dired like buffer. 1651 1652 \\{pdf-annot-list-mode-map}" 1653 (interactive) 1654 (pdf-util-assert-pdf-buffer) 1655 (let ((buffer (current-buffer))) 1656 (with-current-buffer (get-buffer-create 1657 (format "*%s's annots*" 1658 (file-name-sans-extension 1659 (buffer-name)))) 1660 (delay-mode-hooks 1661 (unless (derived-mode-p 'pdf-annot-list-mode) 1662 (pdf-annot-list-mode)) 1663 (setq pdf-annot-list-document-buffer buffer) 1664 (tabulated-list-print) 1665 (setq tablist-context-window-function 1666 (lambda (id) (pdf-annot-list-context-function id buffer)) 1667 tablist-operations-function 'pdf-annot-list-operation-function) 1668 (let ((list-buffer (current-buffer))) 1669 (with-current-buffer buffer 1670 (setq pdf-annot-list-buffer list-buffer)))) 1671 (run-mode-hooks) 1672 (pop-to-buffer 1673 (current-buffer) 1674 pdf-annot-list-display-buffer-action) 1675 (tablist-move-to-major-column) 1676 (tablist-display-context-window)) 1677 (add-hook 'pdf-info-close-document-hook 1678 'pdf-annot-list-update nil t) 1679 (add-hook 'pdf-annot-modified-functions 1680 'pdf-annot-list-update nil t))) 1681 1682 (defun pdf-annot-list-goto-annotation (a) 1683 (with-current-buffer (pdf-annot-get-buffer a) 1684 (unless (and (buffer-live-p pdf-annot-list-buffer) 1685 (get-buffer-window pdf-annot-list-buffer)) 1686 (pdf-annot-list-annotations)) 1687 (with-selected-window (get-buffer-window pdf-annot-list-buffer) 1688 (goto-char (point-min)) 1689 (let ((id (pdf-annot-get-id a))) 1690 (while (and (not (eobp)) 1691 (not (eq id (tabulated-list-get-id)))) 1692 (forward-line)) 1693 (unless (eq id (tabulated-list-get-id)) 1694 (error "Unable to find annotation")) 1695 (when (invisible-p (point)) 1696 (tablist-suspend-filter t)) 1697 (tablist-move-to-major-column))))) 1698 1699 1700 (defun pdf-annot-list-update (&optional _fn) 1701 (when (buffer-live-p pdf-annot-list-buffer) 1702 (with-current-buffer pdf-annot-list-buffer 1703 (unless tablist-edit-column-minor-mode 1704 (tablist-revert)) 1705 (tablist-context-window-update)))) 1706 1707 (defun pdf-annot-list-context-function (id buffer) 1708 (with-current-buffer (get-buffer-create "*Contents*") 1709 (set-window-buffer nil (current-buffer)) 1710 (let ((inhibit-read-only t)) 1711 (erase-buffer) 1712 (when id 1713 (save-excursion 1714 (insert 1715 (pdf-annot-print-annotation 1716 (pdf-annot-getannot id buffer))))) 1717 (read-only-mode 1)))) 1718 1719 (defun pdf-annot-list-operation-function (op &rest args) 1720 (cl-ecase op 1721 (supported-operations '(delete find-entry)) 1722 (delete 1723 (cl-destructuring-bind (ids) 1724 args 1725 (when (buffer-live-p pdf-annot-list-document-buffer) 1726 (with-current-buffer pdf-annot-list-document-buffer 1727 (pdf-annot-with-atomic-modifications 1728 (dolist (a (mapcar 'pdf-annot-getannot ids)) 1729 (pdf-annot-delete a))))))) 1730 (find-entry 1731 (cl-destructuring-bind (id) 1732 args 1733 (unless (buffer-live-p pdf-annot-list-document-buffer) 1734 (error "No PDF document associated with this buffer")) 1735 (let* ((buffer pdf-annot-list-document-buffer) 1736 (a (pdf-annot-getannot id buffer)) 1737 (pdf-window (save-selected-window 1738 (or (get-buffer-window buffer) 1739 (display-buffer buffer)))) 1740 window) 1741 (with-current-buffer buffer 1742 (pdf-annot-activate-annotation a) 1743 (setq window (selected-window))) 1744 ;; Make it so that quitting the edit window returns to the 1745 ;; list window. 1746 (unless (memq window (list (selected-window) pdf-window)) 1747 (let* ((quit-restore 1748 (window-parameter window 'quit-restore))) 1749 (when quit-restore 1750 (setcar (nthcdr 2 quit-restore) (selected-window)))))))))) 1751 1752 (defvar pdf-annot-list-display-annotation--timer nil) 1753 1754 (defun pdf-annot-list-display-annotation-from-id (id) 1755 (interactive (list (tabulated-list-get-id))) 1756 (when id 1757 (unless (buffer-live-p pdf-annot-list-document-buffer) 1758 (error "PDF buffer was killed")) 1759 (when (timerp pdf-annot-list-display-annotation--timer) 1760 (cancel-timer pdf-annot-list-display-annotation--timer)) 1761 (setq pdf-annot-list-display-annotation--timer 1762 (run-with-idle-timer 0.1 nil 1763 (lambda (buffer a) 1764 (when (buffer-live-p buffer) 1765 (with-selected-window 1766 (or (get-buffer-window buffer) 1767 (display-buffer 1768 buffer 1769 '(nil (inhibit-same-window . t)))) 1770 (pdf-annot-show-annotation a t)))) 1771 pdf-annot-list-document-buffer 1772 (pdf-annot-getannot id pdf-annot-list-document-buffer))))) 1773 1774 (define-minor-mode pdf-annot-list-follow-minor-mode 1775 "" 1776 :group 'pdf-annot 1777 (unless (derived-mode-p 'pdf-annot-list-mode) 1778 (error "No in pdf-annot-list-mode.")) 1779 (cond 1780 (pdf-annot-list-follow-minor-mode 1781 (add-hook 'tablist-selection-changed-functions 1782 'pdf-annot-list-display-annotation-from-id nil t) 1783 (let ((id (tabulated-list-get-id))) 1784 (when id 1785 (pdf-annot-list-display-annotation-from-id id)))) 1786 (t 1787 (remove-hook 'tablist-selection-changed-functions 1788 'pdf-annot-list-display-annotation-from-id t)))) 1789 1790 (provide 'pdf-annot) 1791 ;;; pdf-annot.el ends here