pdf-util.el (48662B)
1 ;;; pdf-util.el --- PDF Utility functions. -*- lexical-binding: t -*- 2 3 ;; Copyright (C) 2013, 2014 Andreas Politz 4 5 ;; Author: Andreas Politz <politza@fh-trier.de> 6 ;; Keywords: files, multimedia 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 ;;; Todo: 24 ;; 25 26 ;;; Code: 27 28 (require 'pdf-macs) 29 (require 'cl-lib) 30 (require 'format-spec) 31 (require 'faces) 32 33 ;; These functions are only used after a PdfView window was asserted, 34 ;; which won't succeed, if pdf-view.el isn't loaded. 35 (declare-function pdf-view-image-size "pdf-view") 36 (declare-function pdf-view-image-offset "pdf-view") 37 (declare-function pdf-cache-pagesize "pdf-cache") 38 (declare-function pdf-view-image-type "pdf-view") 39 40 41 42 ;; * ================================================================== * 43 ;; * Transforming coordinates 44 ;; * ================================================================== * 45 46 47 (defun pdf-util-scale (list-of-edges-or-pos scale &optional rounding-fn) 48 "Scale LIST-OF-EDGES-OR-POS by SCALE. 49 50 SCALE is a cons (SX . SY), by which edges/positions are scaled. 51 If ROUNDING-FN is non-nil, it should be a function of one 52 argument, a real value, returning a rounded 53 value (e.g. `ceiling'). 54 55 The elements in LIST-OF-EDGES-OR-POS should be either a list 56 \(LEFT TOP RIGHT BOT\) or a position \(X . Y\). 57 58 LIST-OF-EDGES-OR-POS may also be a single such element. 59 60 Return scaled list of edges if LIST-OF-EDGES-OR-POS was indeed a list, 61 else return the scaled singleton." 62 63 (let ((have-list-p (listp (car list-of-edges-or-pos)))) 64 (unless have-list-p 65 (setq list-of-edges-or-pos (list list-of-edges-or-pos))) 66 (let* ((sx (car scale)) 67 (sy (cdr scale)) 68 (result 69 (mapcar 70 (lambda (edges) 71 (cond 72 ((consp (cdr edges)) 73 (let ((e (list (* (nth 0 edges) sx) 74 (* (nth 1 edges) sy) 75 (* (nth 2 edges) sx) 76 (* (nth 3 edges) sy)))) 77 (if rounding-fn 78 (mapcar rounding-fn e) 79 e))) 80 (rounding-fn 81 (cons (funcall rounding-fn (* (car edges) sx)) 82 (funcall rounding-fn (* (cdr edges) sy)))) 83 (t 84 (cons (* (car edges) sx) 85 (* (cdr edges) sy))))) 86 list-of-edges-or-pos))) 87 (if have-list-p 88 result 89 (car result))))) 90 91 (defun pdf-util-scale-to (list-of-edges from to &optional rounding-fn) 92 "Scale LIST-OF-EDGES in FROM basis to TO. 93 94 FROM and TO should both be a cons \(WIDTH . HEIGHT\). See also 95 `pdf-util-scale'." 96 97 (pdf-util-scale list-of-edges 98 (cons (/ (float (car to)) 99 (float (car from))) 100 (/ (float (cdr to)) 101 (float (cdr from)))) 102 rounding-fn)) 103 104 (defun pdf-util-scale-pixel-to-points (list-of-pixel-edges 105 &optional rounding-fn displayed-p window) 106 "Scale LIST-OF-PIXEL-EDGES to point values. 107 108 The result depends on the currently displayed page in WINDOW. 109 See also `pdf-util-scale'." 110 (pdf-util-assert-pdf-window window) 111 (pdf-util-scale-to 112 list-of-pixel-edges 113 (pdf-view-image-size displayed-p window) 114 (pdf-cache-pagesize (pdf-view-current-page window)) 115 rounding-fn)) 116 117 (defun pdf-util-scale-points-to-pixel (list-of-points-edges 118 &optional rounding-fn displayed-p window) 119 "Scale LIST-OF-POINTS-EDGES to point values. 120 121 The result depends on the currently displayed page in WINDOW. 122 See also `pdf-util-scale'." 123 (pdf-util-assert-pdf-window window) 124 (pdf-util-scale-to 125 list-of-points-edges 126 (pdf-cache-pagesize (pdf-view-current-page window)) 127 (pdf-view-image-size displayed-p window) 128 rounding-fn)) 129 130 (defun pdf-util-scale-relative-to-points (list-of-relative-edges 131 &optional rounding-fn window) 132 "Scale LIST-OF-RELATIVE-EDGES to point values. 133 134 The result depends on the currently displayed page in WINDOW. 135 See also `pdf-util-scale'." 136 (pdf-util-assert-pdf-window window) 137 (pdf-util-scale-to 138 list-of-relative-edges 139 '(1.0 . 1.0) 140 (pdf-cache-pagesize (pdf-view-current-page window)) 141 rounding-fn)) 142 143 (defun pdf-util-scale-points-to-relative (list-of-points-edges 144 &optional rounding-fn window) 145 "Scale LIST-OF-POINTS-EDGES to relative values. 146 147 See also `pdf-util-scale'." 148 (pdf-util-assert-pdf-window window) 149 (pdf-util-scale-to 150 list-of-points-edges 151 (pdf-cache-pagesize (pdf-view-current-page window)) 152 '(1.0 . 1.0) 153 rounding-fn)) 154 155 (defun pdf-util-scale-pixel-to-relative (list-of-pixel-edges 156 &optional rounding-fn displayed-p window) 157 "Scale LIST-OF-PIXEL-EDGES to relative values. 158 159 The result depends on the currently displayed page in WINDOW. 160 See also `pdf-util-scale'." 161 (pdf-util-assert-pdf-window window) 162 (pdf-util-scale-to 163 list-of-pixel-edges 164 (pdf-view-image-size displayed-p window) 165 '(1.0 . 1.0) 166 rounding-fn)) 167 168 169 (defun pdf-util-scale-relative-to-pixel (list-of-relative-edges 170 &optional rounding-fn displayed-p window) 171 "Scale LIST-OF-EDGES to match SIZE. 172 173 The result depends on the currently displayed page in WINDOW. 174 See also `pdf-util-scale'." 175 (pdf-util-assert-pdf-window window) 176 (pdf-util-scale-to 177 list-of-relative-edges 178 '(1.0 . 1.0) 179 (pdf-view-image-size displayed-p window) 180 rounding-fn)) 181 182 (defun pdf-util-translate (list-of-edges-or-pos 183 offset &optional opposite-direction-p) 184 "Translate LIST-OF-EDGES-OR-POS by OFFSET 185 186 OFFSET should be a cons \(X . Y\), by which to translate 187 LIST-OF-EDGES-OR-POS. If OPPOSITE-DIRECTION-P is non-nil 188 translate by \(-X . -Y\). 189 190 See `pdf-util-scale' for the LIST-OF-EDGES-OR-POS argument." 191 192 (let ((have-list-p (listp (car list-of-edges-or-pos)))) 193 (unless have-list-p 194 (setq list-of-edges-or-pos (list list-of-edges-or-pos))) 195 (let* ((ox (if opposite-direction-p 196 (- (car offset)) 197 (car offset))) 198 (oy (if opposite-direction-p 199 (- (cdr offset)) 200 (cdr offset))) 201 (result 202 (mapcar 203 (lambda (edges) 204 (cond 205 ((consp (cdr edges)) 206 (list (+ (nth 0 edges) ox) 207 (+ (nth 1 edges) oy) 208 (+ (nth 2 edges) ox) 209 (+ (nth 3 edges) oy))) 210 (t 211 (cons (+ (car edges) ox) 212 (+ (cdr edges) oy))))) 213 list-of-edges-or-pos))) 214 (if have-list-p 215 result 216 (car result))))) 217 218 (defmacro pdf-util-with-edges (list-of-edges &rest body) 219 "Provide some convenient macros for the edges in LIST-OF-EDGES. 220 221 LIST-OF-EDGES should be a list of variables \(X ...\), each one 222 holding a list of edges. Inside BODY the symbols X-left, X-top, 223 X-right, X-bot, X-width and X-height expand to their respective 224 values." 225 226 (declare (indent 1) (debug (sexp &rest form))) 227 (unless (cl-every 'symbolp list-of-edges) 228 (error "Argument should be a list of symbols")) 229 (let ((list-of-syms 230 (mapcar (lambda (edge) 231 (cons edge (mapcar 232 (lambda (kind) 233 (intern (format "%s-%s" edge kind))) 234 '(left top right bot width height)))) 235 list-of-edges))) 236 (macroexpand-all 237 `(cl-symbol-macrolet 238 ,(apply #'nconc 239 (mapcar 240 (lambda (edge-syms) 241 (let ((edge (nth 0 edge-syms)) 242 (syms (cdr edge-syms))) 243 `((,(pop syms) (nth 0 ,edge)) 244 (,(pop syms) (nth 1 ,edge)) 245 (,(pop syms) (nth 2 ,edge)) 246 (,(pop syms) (nth 3 ,edge)) 247 (,(pop syms) (- (nth 2 ,edge) 248 (nth 0 ,edge))) 249 (,(pop syms) (- (nth 3 ,edge) 250 (nth 1 ,edge)))))) 251 list-of-syms)) 252 ,@body)))) 253 254 (defun pdf-util-edges-transform (region elts &optional to-region-p) 255 "Translate ELTS according to REGION. 256 257 ELTS may be one edges list or a position or a list thereof. 258 Translate each from region coordinates to (0 0 1 1) or the 259 opposite, if TO-REGION-P is non-nil. All coordinates should be 260 relative. 261 262 Returns the translated list of elements or the single one 263 depending on the input." 264 265 (when elts 266 (let ((have-list-p (consp (car-safe elts)))) 267 (unless have-list-p 268 (setq elts (list elts))) 269 (let ((result 270 (if (null region) 271 elts 272 (mapcar (lambda (edges) 273 (let ((have-pos-p (numberp (cdr edges)))) 274 (when have-pos-p 275 (setq edges (list (car edges) (cdr edges) 276 (car edges) (cdr edges)))) 277 (pdf-util-with-edges (edges region) 278 (let ((newedges 279 (mapcar (lambda (n) 280 (min 1.0 (max 0.0 n))) 281 (if to-region-p 282 `(,(/ (- edges-left region-left) 283 region-width) 284 ,(/ (- edges-top region-top) 285 region-height) 286 ,(/ (- edges-right region-left) 287 region-width) 288 ,(/ (- edges-bot region-top) 289 region-height)) 290 `(,(+ (* edges-left region-width) 291 region-left) 292 ,(+ (* edges-top region-height) 293 region-top) 294 ,(+ (* edges-right region-width) 295 region-left) 296 ,(+ (* edges-bot region-height) 297 region-top)))))) 298 (if have-pos-p 299 (cons (car newedges) (cadr newedges)) 300 newedges))))) 301 elts)))) 302 (if have-list-p 303 result 304 (car result)))))) 305 306 ;; * ================================================================== * 307 ;; * Scrolling 308 ;; * ================================================================== * 309 310 (defun pdf-util-image-displayed-edges (&optional window displayed-p) 311 "Return the visible region of the image in WINDOW. 312 313 Returns a list of pixel edges." 314 (pdf-util-assert-pdf-window) 315 (let* ((edges (window-inside-pixel-edges window)) 316 (isize (pdf-view-image-size displayed-p window)) 317 (offset (if displayed-p 318 `(0 . 0) 319 (pdf-view-image-offset window))) 320 (hscroll (* (window-hscroll window) 321 (frame-char-width (window-frame window)))) 322 (vscroll (window-vscroll window t)) 323 (x0 (+ hscroll (car offset))) 324 (y0 (+ vscroll (cdr offset))) 325 (x1 (min (car isize) 326 (+ x0 (- (nth 2 edges) (nth 0 edges))))) 327 (y1 (min (cdr isize) 328 (+ y0 (- (nth 3 edges) (nth 1 edges)))))) 329 (mapcar #'round (list x0 y0 x1 y1)))) 330 331 (defun pdf-util-required-hscroll (edges &optional eager-p context-pixel) 332 "Return the amount of scrolling necessary, to make image EDGES visible. 333 334 Scroll as little as necessary. Unless EAGER-P is non-nil, in 335 which case scroll as much as possible. 336 337 Keep CONTEXT-PIXEL pixel of the image visible at the bottom and 338 top of the window. CONTEXT-PIXEL defaults to 0. 339 340 Return the required hscroll in columns or nil, if scrolling is not 341 needed." 342 343 (pdf-util-assert-pdf-window) 344 (unless context-pixel 345 (setq context-pixel 0)) 346 (let* ((win (window-inside-pixel-edges)) 347 (image-width (car (pdf-view-image-size t))) 348 (image-left (* (frame-char-width) 349 (window-hscroll))) 350 (edges (pdf-util-translate 351 edges 352 (pdf-view-image-offset) t))) 353 (pdf-util-with-edges (win edges) 354 (let* ((edges-left (- edges-left context-pixel)) 355 (edges-right (+ edges-right context-pixel))) 356 (if (< edges-left image-left) 357 (round (/ (max 0 (if eager-p 358 (- edges-right win-width) 359 edges-left)) 360 (frame-char-width))) 361 (if (> (min image-width 362 edges-right) 363 (+ image-left win-width)) 364 (round (/ (min (- image-width win-width) 365 (if eager-p 366 edges-left 367 (- edges-right win-width))) 368 (frame-char-width))))))))) 369 370 (defun pdf-util-required-vscroll (edges &optional eager-p context-pixel) 371 "Return the amount of scrolling necessary, to make image EDGES visible. 372 373 Scroll as little as necessary. Unless EAGER-P is non-nil, in 374 which case scroll as much as possible. 375 376 Keep CONTEXT-PIXEL pixel of the image visible at the bottom and 377 top of the window. CONTEXT-PIXEL defaults to an equivalent pixel 378 value of `next-screen-context-lines'. 379 380 Return the required vscroll in pixels or nil, if scrolling is not 381 needed. 382 383 Note: For versions of emacs before 27 this will return lines instead of 384 pixels. This is because of a change that occurred to `image-mode' in 27." 385 (pdf-util-assert-pdf-window) 386 (let* ((win (window-inside-pixel-edges)) 387 (image-height (cdr (pdf-view-image-size t))) 388 (image-top (window-vscroll nil t)) 389 (edges (pdf-util-translate 390 edges 391 (pdf-view-image-offset) t))) 392 (pdf-util-with-edges (win edges) 393 (let* ((context-pixel (or context-pixel 394 (* next-screen-context-lines 395 (frame-char-height)))) 396 ;;Be careful not to modify edges. 397 (edges-top (- edges-top context-pixel)) 398 (edges-bot (+ edges-bot context-pixel)) 399 (vscroll 400 (cond ((< edges-top image-top) 401 (max 0 (if eager-p 402 (- edges-bot win-height) 403 edges-top))) 404 ((> (min image-height 405 edges-bot) 406 (+ image-top win-height)) 407 (min (- image-height win-height) 408 (if eager-p 409 edges-top 410 (- edges-bot win-height))))))) 411 412 413 (when vscroll 414 (round 415 ;; `image-set-window-vscroll' changed in version 27 to using 416 ;; pixels, not lines. 417 (if (version< emacs-version "27") 418 (/ vscroll (float (frame-char-height))) 419 vscroll))))))) 420 421 (defun pdf-util-scroll-to-edges (edges &optional eager-p) 422 "Scroll window such that image EDGES are visible. 423 424 Scroll as little as necessary. Unless EAGER-P is non-nil, in 425 which case scroll as much as possible." 426 427 (let ((vscroll (pdf-util-required-vscroll edges eager-p)) 428 (hscroll (pdf-util-required-hscroll edges eager-p))) 429 (when vscroll 430 (image-set-window-vscroll vscroll)) 431 (when hscroll 432 (image-set-window-hscroll hscroll)))) 433 434 435 436 ;; * ================================================================== * 437 ;; * Temporary files 438 ;; * ================================================================== * 439 440 (defvar pdf-util--base-directory nil 441 "Base directory for temporary files.") 442 443 (defvar-local pdf-util--dedicated-directory nil 444 "The relative name of buffer's dedicated directory.") 445 446 (defun pdf-util-dedicated-directory () 447 "Return the name of a existing dedicated directory. 448 449 The directory is exclusive to the current buffer. It will be 450 automatically deleted, if Emacs or the current buffer are 451 killed." 452 (with-file-modes #o0700 453 (unless (and pdf-util--base-directory 454 (file-directory-p 455 pdf-util--base-directory) 456 (not (file-symlink-p 457 pdf-util--base-directory))) 458 (add-hook 'kill-emacs-hook 459 (lambda nil 460 (when (and pdf-util--base-directory 461 (file-directory-p pdf-util--base-directory)) 462 (delete-directory pdf-util--base-directory t)))) 463 (setq pdf-util--base-directory 464 (make-temp-file "pdf-tools-" t))) 465 (unless (and pdf-util--dedicated-directory 466 (file-directory-p pdf-util--dedicated-directory) 467 (not (file-symlink-p 468 pdf-util--base-directory))) 469 (let ((temporary-file-directory 470 pdf-util--base-directory)) 471 (setq pdf-util--dedicated-directory 472 (make-temp-file (convert-standard-filename (pdf-util-temp-prefix)) 473 t)) 474 (add-hook 'kill-buffer-hook #'pdf-util-delete-dedicated-directory 475 nil t))) 476 pdf-util--dedicated-directory)) 477 478 (defun pdf-util-delete-dedicated-directory () 479 "Delete current buffer's dedicated directory." 480 (delete-directory (pdf-util-dedicated-directory) t)) 481 482 (defun pdf-util-expand-file-name (name) 483 "Expand filename against current buffer's dedicated directory." 484 (expand-file-name name (pdf-util-dedicated-directory))) 485 486 (defun pdf-util-temp-prefix () 487 "Create a temp-file prefix for the current buffer" 488 (concat (if buffer-file-name 489 (file-name-nondirectory buffer-file-name) 490 (replace-regexp-in-string "[^[:alnum:]]+" "-" (buffer-name))) 491 "-")) 492 493 (defun pdf-util-make-temp-file (&optional prefix dir-flag suffix) 494 "Create a temporary file in current buffer's dedicated directory. 495 496 See `make-temp-file' for the arguments." 497 (let ((temporary-file-directory (pdf-util-dedicated-directory))) 498 (make-temp-file (convert-standard-filename 499 (or prefix (pdf-util-temp-prefix))) 500 dir-flag suffix))) 501 502 503 ;; * ================================================================== * 504 ;; * Various 505 ;; * ================================================================== * 506 507 (defmacro pdf-util-debug (&rest body) 508 "Execute BODY only if debugging is enabled." 509 (declare (indent 0) (debug t)) 510 `(when (bound-and-true-p pdf-tools-debug) 511 ,@body)) 512 513 (defun pdf-util-pdf-buffer-p (&optional buffer) 514 (and (or (null buffer) 515 (buffer-live-p buffer)) 516 (save-current-buffer 517 (and buffer (set-buffer buffer)) 518 (derived-mode-p 'pdf-view-mode)))) 519 520 (defun pdf-util-assert-pdf-buffer (&optional buffer) 521 (unless (pdf-util-pdf-buffer-p buffer) 522 (error "Buffer is not in PDFView mode"))) 523 524 (defun pdf-util-pdf-window-p (&optional window) 525 (unless (or (null window) 526 (window-live-p window)) 527 (signal 'wrong-type-argument (list 'window-live-p window))) 528 (unless window (setq window (selected-window))) 529 (and (window-live-p window) 530 (with-selected-window window 531 (pdf-util-pdf-buffer-p)))) 532 533 (defun pdf-util-assert-pdf-window (&optional window) 534 (unless (pdf-util-pdf-window-p window) 535 (error "Window's buffer is not in PdfView mode"))) 536 537 (defun pdf-util-munch-file (filename &optional multibyte-p) 538 "Read contents from FILENAME and delete it. 539 540 Return the file's content as a unibyte string, unless MULTIBYTE-P 541 is non-nil." 542 (unwind-protect 543 (with-temp-buffer 544 (set-buffer-multibyte multibyte-p) 545 (insert-file-contents-literally filename) 546 (buffer-substring-no-properties 547 (point-min) 548 (point-max))) 549 (when (and filename 550 (file-exists-p filename)) 551 (delete-file filename)))) 552 553 (defun pdf-util-hexcolor (color) 554 "Return COLOR in hex-format. 555 556 Signal an error, if color is invalid." 557 (if (string-match "\\`#[[:xdigit:]]\\{6\\}\\'" color) 558 color 559 (let ((values (color-values color))) 560 (unless values 561 (signal 'wrong-type-argument (list 'color-defined-p color))) 562 (apply #'format "#%02x%02x%02x" 563 (mapcar (lambda (c) (ash c -8)) 564 values))))) 565 566 (defun pdf-util-highlight-regexp-in-string (regexp string &optional face) 567 "Highlight all occurrences of REGEXP in STRING using FACE. 568 569 FACE defaults to the `match' face. Returns the new fontified 570 string." 571 (with-temp-buffer 572 (save-excursion (insert string)) 573 (while (and (not (eobp)) 574 (re-search-forward regexp nil t)) 575 (if (= (match-beginning 0) 576 (match-end 0)) 577 (forward-char) 578 (put-text-property 579 (match-beginning 0) 580 (point) 581 'face (or face 'match)))) 582 (buffer-string))) 583 584 (autoload 'list-colors-duplicates "facemenu") 585 586 (defun pdf-util-color-completions () 587 "Return a fontified list of defined colors." 588 (let ((color-list (list-colors-duplicates)) 589 colors) 590 (dolist (cl color-list) 591 (dolist (c (reverse cl)) 592 (push (propertize c 'face `(:background ,c)) 593 colors))) 594 (nreverse colors))) 595 596 (defun pdf-util-tooltip-in-window (text x y &optional window) 597 (let* ((we (window-inside-absolute-pixel-edges window)) 598 (dx (round (+ x (nth 0 we)))) 599 (dy (round (+ y (nth 1 we)))) 600 (tooltip-frame-parameters 601 `((left . ,dx) 602 (top . ,dy) 603 ,@tooltip-frame-parameters))) 604 (tooltip-show text))) 605 606 ;; FIXME: Defined in `pdf-view' but we can't require it here because it 607 ;; requires us :-( 608 (defvar pdf-view-midnight-colors) 609 610 (when (and (> emacs-major-version 28) 611 (not (boundp 'x-gtk-use-system-tooltips))) 612 ;; The x-gtk prefix has been dropped Emacs 29 613 (defvaralias 'x-gtk-use-system-tooltips 'use-system-tooltips)) 614 615 (defun pdf-util-tooltip-arrow (image-top &optional timeout) 616 (pdf-util-assert-pdf-window) 617 (when (floatp image-top) 618 (setq image-top 619 (round (* image-top (cdr (pdf-view-image-size)))))) 620 (let* ((x-gtk-use-system-tooltips nil) 621 ;; ^ allow for display text property in tooltip 622 (dx (+ (or (car (window-margins)) 0) 623 (car (window-fringes)))) 624 (dy image-top) 625 (pos (list dx dy dx (+ dy (* 2 (frame-char-height))))) 626 (vscroll 627 (pdf-util-required-vscroll pos)) 628 (tooltip-frame-parameters 629 `((border-width . 0) 630 (internal-border-width . 0) 631 ,@tooltip-frame-parameters)) 632 (tooltip-hide-delay (or timeout 3))) 633 (when vscroll 634 (image-set-window-vscroll vscroll)) 635 (setq dy (max 0 (- dy 636 (cdr (pdf-view-image-offset)) 637 (window-vscroll nil t) 638 (frame-char-height)))) 639 (when (overlay-get (pdf-view-current-overlay) 'before-string) 640 (let* ((e (window-inside-pixel-edges)) 641 (xw (pdf-util-with-edges (e) e-width))) 642 (cl-incf dx (/ (- xw (car (pdf-view-image-size t))) 2)))) 643 (pdf-util-tooltip-in-window 644 (propertize 645 " " 'display (propertize 646 "\u2192" ;;right arrow 647 'display '(height 2) 648 'face `(:foreground 649 "orange red" 650 :background 651 ,(cond 652 ((bound-and-true-p pdf-view-midnight-minor-mode) 653 (cdr pdf-view-midnight-colors)) 654 ((bound-and-true-p pdf-view-themed-minor-mode) 655 (face-background 'default nil)) 656 (t "white"))))) 657 dx dy))) 658 659 (defvar pdf-util--face-colors-cache (make-hash-table)) 660 661 (advice-add 'enable-theme :after #'pdf-util--clear-faces-cache) 662 (defun pdf-util--clear-faces-cache (&rest _) 663 (clrhash pdf-util--face-colors-cache)) 664 665 (defun pdf-util-face-colors (face &optional dark-p) 666 "Return both colors of FACE as a cons. 667 668 Look also in inherited faces. If DARK-P is non-nil, return dark 669 colors, otherwise light." 670 (let* ((bg (if dark-p 'dark 'light)) 671 (spec (list (get face 'face-defface-spec) 672 (get face 'theme-face) 673 (get face 'customized-face))) 674 (cached (gethash face pdf-util--face-colors-cache))) 675 (cl-destructuring-bind (&optional cspec color-alist) 676 cached 677 (or (and color-alist 678 (equal cspec spec) 679 (cdr (assq bg color-alist))) 680 (let* ((this-bg (frame-parameter nil 'background-mode)) 681 (frame-background-mode bg) 682 (f (and (not (eq bg this-bg)) 683 (x-create-frame-with-faces '((visibility . nil)))))) 684 (with-selected-frame (or f (selected-frame)) 685 (unwind-protect 686 (let ((colors 687 (cons (face-attribute face :foreground nil 'default) 688 (face-attribute face :background nil 'default)))) 689 (puthash face `(,(mapcar #'copy-sequence spec) 690 ((,bg . ,colors) ,@color-alist)) 691 pdf-util--face-colors-cache) 692 colors) 693 (when (and f (frame-live-p f)) 694 (delete-frame f))))))))) 695 696 (defun pdf-util-window-attach (awindow &optional window) 697 "Attach AWINDOW to WINDOW. 698 699 This has the following effect. Whenever WINDOW, defaulting to 700 the selected window, stops displaying the buffer it currently 701 displays (e.g., by switching buffers or because it was deleted) 702 AWINDOW is deleted." 703 (unless window (setq window (selected-window))) 704 (let ((buffer (window-buffer window)) 705 (hook (make-symbol "window-attach-hook"))) 706 (fset hook 707 (lambda () 708 (when (or (not (window-live-p window)) 709 (not (eq buffer (window-buffer window)))) 710 (remove-hook 'window-configuration-change-hook 711 hook) 712 ;; Deleting windows inside wcch may cause errors in 713 ;; windows.el . 714 (run-with-timer 715 0 nil (lambda (win) 716 (when (and (window-live-p win) 717 (not (eq win (selected-window)))) 718 (delete-window win))) 719 awindow)))) 720 (add-hook 'window-configuration-change-hook hook))) 721 722 (defun display-buffer-split-below-and-attach (buf alist) 723 "Display buffer action using `pdf-util-window-attach'." 724 (let ((window (selected-window)) 725 (height (cdr (assq 'window-height alist))) 726 newwin) 727 (when height 728 (when (floatp height) 729 (setq height (round (* height (frame-height))))) 730 (setq height (- (max height window-min-height)))) 731 (setq newwin (window--display-buffer 732 buf 733 (split-window-below height) 734 'window alist)) 735 (pdf-util-window-attach newwin window) 736 newwin)) 737 738 (defun pdf-util-goto-position (line &optional column) 739 "Goto LINE and COLUMN in the current buffer. 740 741 COLUMN defaults to 0. Widen the buffer, if the position is 742 outside the current limits." 743 (let ((pos 744 (when (> line 0) 745 (save-excursion 746 (save-restriction 747 (widen) 748 (goto-char 1) 749 (when (= 0 (forward-line (1- line))) 750 (when (and column (> column 0)) 751 (forward-char (1- column))) 752 (point))))))) 753 (when pos 754 (when (or (< pos (point-min)) 755 (> pos (point-max))) 756 (widen)) 757 (goto-char pos)))) 758 759 (defun pdf-util-seq-alignment (seq1 seq2 &optional similarity-fn alignment-type) 760 "Return an alignment of sequences SEQ1 and SEQ2. 761 762 SIMILARITY-FN should be a function. It is called with two 763 arguments: One element from SEQ1 and one from SEQ2. It should 764 return a number determining how similar the elements are, where 765 higher values mean `more similar'. The default returns 1 if the 766 elements are equal, else -1. 767 768 ALIGNMENT-TYPE may be one of the symbols `prefix', `suffix', 769 `infix' or nil. If it is `prefix', trailing elements in SEQ2 may 770 be ignored. For example the alignment of 771 772 \(0 1\) and \(0 1 2\) 773 774 using prefix matching is 0, since the prefixes are equal and the 775 trailing 2 is ignored. The other possible values have similar 776 effects. The default is nil, which means to match the whole 777 sequences. 778 779 Return a cons \(VALUE . ALIGNMENT\), where VALUE says how similar 780 the sequences are and ALIGNMENT is a list of \(E1 . E2\), where 781 E1 is an element from SEQ1 or nil, likewise for E2. If one of 782 them is nil, it means there is gap at this position in the 783 respective sequence." 784 785 (cl-macrolet ((make-matrix (rows columns) 786 `(apply #'vector 787 (cl-loop for i from 1 to ,rows 788 collect (make-vector ,columns nil)))) 789 (mset (matrix row column newelt) 790 `(aset (aref ,matrix ,row) ,column ,newelt)) 791 (mref (matrix row column) 792 `(aref (aref ,matrix ,row) ,column))) 793 (let* ((len1 (length seq1)) 794 (len2 (length seq2)) 795 (d (make-matrix (1+ len1) (1+ len2))) 796 (prefix-p (memq alignment-type '(prefix infix))) 797 (suffix-p (memq alignment-type '(suffix infix))) 798 (similarity-fn (or similarity-fn 799 (lambda (a b) 800 (if (equal a b) 1 -1))))) 801 802 (cl-loop for i from 0 to len1 do 803 (mset d i 0 (- i))) 804 (cl-loop for j from 0 to len2 do 805 (mset d 0 j (if suffix-p 0 (- j)))) 806 807 (cl-loop for i from 1 to len1 do 808 (cl-loop for j from 1 to len2 do 809 (let ((max (max 810 (1- (mref d (1- i) j)) 811 (+ (mref d i (1- j)) 812 (if (and prefix-p (= i len1)) 0 -1)) 813 (+ (mref d (1- i) (1- j)) 814 (funcall similarity-fn 815 (elt seq1 (1- i)) 816 (elt seq2 (1- j))))))) 817 (mset d i j max)))) 818 819 (let ((i len1) 820 (j len2) 821 alignment) 822 (while (or (> i 0) 823 (> j 0)) 824 (cond 825 ((and (> i 0) 826 (= (mref d i j) 827 (1- (mref d (1- i) j)))) 828 (cl-decf i) 829 (push (cons (elt seq1 i) nil) alignment)) 830 ((and (> j 0) 831 (= (mref d i j) 832 (+ (mref d i (1- j)) 833 (if (or (and (= i 0) suffix-p) 834 (and (= i len1) prefix-p)) 835 0 -1)))) 836 (cl-decf j) 837 (push (cons nil (elt seq2 j)) alignment)) 838 (t 839 (cl-assert (and (> i 0) (> j 0)) t) 840 (cl-decf i) 841 (cl-decf j) 842 (push (cons (elt seq1 i) 843 (elt seq2 j)) 844 alignment)))) 845 (cons (mref d len1 len2) alignment))))) 846 847 848 (defun pdf-util-pcre-quote (string) 849 "Escape STRING for use as a PCRE. 850 851 See also `regexp-quote'." 852 853 (let ((to-escape 854 (eval-when-compile (append "\0\\|()[]{}^$*+?." nil))) 855 (chars (append string nil)) 856 escaped) 857 (dolist (ch chars) 858 (when (memq ch to-escape) 859 (push ?\\ escaped)) 860 (push ch escaped)) 861 (apply #'string (nreverse escaped)))) 862 863 (defun pdf-util-frame-ppi () 864 "Return the PPI of the current frame." 865 (condition-case nil 866 (let* ((props (frame-monitor-attributes)) 867 (px (nthcdr 2 (alist-get 'geometry props))) 868 (mm (alist-get 'mm-size props)) 869 (dp (sqrt (+ (expt (nth 0 px) 2) 870 (expt (nth 1 px) 2)))) 871 (di (sqrt (+ (expt (/ (nth 0 mm) 25.4) 2) 872 (expt (/ (nth 1 mm) 25.4) 2))))) 873 (/ dp di)) 874 ;; Calculating frame-ppi failed, return 0 to indicate unknown. 875 ;; This can happen when (frame-monitor-attributes) does not have 876 ;; the right properties (Emacs 26, 27). It leads to the 877 ;; wrong-type-argument error, which is the only one we are 878 ;; catching here. We will catch more errors only if we see them 879 ;; happening. 880 (wrong-type-argument 0))) 881 882 (defvar pdf-view-use-scaling) 883 884 (defun pdf-util-frame-scale-factor () 885 "Return the frame scale factor depending on the image type used for display. 886 When `pdf-view-use-scaling' is non-nil, return the scale factor of the frame 887 if available. If the scale factor isn't available, return 2 if the 888 frame's PPI is larger than 180. Otherwise, return 1." 889 (if pdf-view-use-scaling 890 (or (and (fboundp 'frame-scale-factor) 891 (truncate (frame-scale-factor))) 892 (and (fboundp 'frame-monitor-attributes) 893 (cdr (assq 'backing-scale-factor (frame-monitor-attributes)))) 894 (if (>= (pdf-util-frame-ppi) 180) 895 2 896 1)) 897 1)) 898 899 900 ;; * ================================================================== * 901 ;; * Imagemagick's convert 902 ;; * ================================================================== * 903 904 (defcustom pdf-util-convert-program 905 ;; Avoid using the MS Windows command convert.exe . 906 (unless (memq system-type '(ms-dos windows-nt)) 907 (executable-find "convert")) 908 "Absolute path to the convert program." 909 :group 'pdf-tools 910 :type '(choice (const :tag "Unset" nil) file)) 911 912 (defcustom pdf-util-fast-image-format nil 913 "An image format appropriate for fast displaying. 914 915 This should be a cons (TYPE . EXT) where type is the Emacs 916 `image-type' and EXT the appropriate file extension starting with 917 a dot. If nil, the value is determined automatically. 918 919 Different formats have different properties, with respect to 920 Emacs loading time, convert creation time and the file-size. In 921 general, uncompressed formats are faster, but may need a fair 922 amount of (temporary) disk space." 923 :group 'pdf-tools 924 :type '(choice 925 (const :tag "Determine automatically" nil) 926 (cons symbol string))) 927 928 (defun pdf-util-assert-convert-program () 929 (unless (and pdf-util-convert-program 930 (file-executable-p pdf-util-convert-program)) 931 (error "The pdf-util-convert-program is unset or non-executable"))) 932 933 (defun pdf-util-image-file-size (image-file) 934 "Determine the size of the image in IMAGE-FILE. 935 936 Returns a cons \(WIDTH . HEIGHT\)." 937 (pdf-util-assert-convert-program) 938 (with-temp-buffer 939 (when (save-excursion 940 (= 0 (call-process 941 pdf-util-convert-program 942 nil (current-buffer) nil 943 image-file "-format" "%w %h" "info:"))) 944 (let ((standard-input (current-buffer))) 945 (cons (read) (read)))))) 946 947 (defun pdf-util-convert (in-file out-file &rest spec) 948 "Convert image IN-FILE to OUT-FILE according to SPEC. 949 950 IN-FILE should be the name of a file containing an image. Write 951 the result to OUT-FILE. The extension of this filename usually 952 determines the resulting image-type. 953 954 SPEC is a property list, specifying what the convert program 955 should do with the image. All manipulations operate on a 956 rectangle, see below. 957 958 SPEC may contain the following keys, respectively values. 959 960 `:foreground' Set foreground color for all following operations. 961 962 `:background' Dito, for the background color. 963 964 `:commands' A list of strings representing arguments to convert 965 for image manipulations. It may contain %-escape characters, as 966 follows. 967 968 %f -- Expands to the foreground color. 969 %b -- Expands to the background color. 970 %g -- Expands to the geometry of the current rectangle, i.e. WxH+X+Y. 971 %x -- Expands to the left edge of rectangle. 972 %X -- Expands to the right edge of rectangle. 973 %y -- Expands to the top edge of rectangle. 974 %Y -- Expands to the bottom edge of rectangle. 975 %w -- Expands to the width of rectangle. 976 %h -- Expands to the height of rectangle. 977 978 Keep in mind, that every element of this list is seen by convert 979 as a single argument. 980 981 `:formats' An alist of additional %-escapes. Every element 982 should be a cons \(CHAR . STRING\) or \(CHAR . FUNCTION\). In 983 the first case, all occurrences of %-CHAR in the above commands 984 will be replaced by STRING. In the second case FUNCTION is 985 called with the current rectangle and it should return the 986 replacement string. 987 988 `:apply' A list of rectangles \(\(LEFT TOP RIGHT BOT\) ...\) in 989 IN-FILE coordinates. Each such rectangle triggers one execution 990 of the last commands given earlier in SPEC. E.g. a call like 991 992 (pdf-util-convert 993 image-file out-file 994 :foreground \"black\" 995 :background \"white\" 996 :commands \\='(\"-fill\" \"%f\" \"-draw\" \"rectangle %x,%y,%X,%Y\") 997 :apply \\='((0 0 10 10) (10 10 20 20)) 998 :commands \\='(\"-fill\" \"%b\" \"-draw\" \"rectangle %x,%y,%X,%Y\") 999 :apply \\='((10 0 20 10) (0 10 10 20))) 1000 1001 would draw a 4x4 checkerboard pattern in the left corner of the 1002 image, while leaving the rest of it as it was. 1003 1004 Returns OUT-FILE. 1005 1006 See url `http://www.imagemagick.org/script/convert.php'." 1007 (pdf-util-assert-convert-program) 1008 (let* ((cmds (pdf-util-convert--create-commands spec)) 1009 (status (apply #'call-process 1010 pdf-util-convert-program nil 1011 (get-buffer-create "*pdf-util-convert-output*") 1012 nil 1013 `(,in-file ,@cmds ,out-file)))) 1014 (unless (and (numberp status) (= 0 status)) 1015 (error "The convert program exited with error status: %s" status)) 1016 out-file)) 1017 1018 (defun pdf-util-convert-asynch (in-file out-file &rest spec-and-callback) 1019 "Like `pdf-util-convert', but asynchronous. 1020 1021 If the last argument is a function, it is installed as the 1022 process sentinel. 1023 1024 Returns the convert process." 1025 (pdf-util-assert-convert-program) 1026 (let ((callback (car (last spec-and-callback))) 1027 spec) 1028 (if (functionp callback) 1029 (setq spec (butlast spec-and-callback)) 1030 (setq spec spec-and-callback 1031 callback nil)) 1032 (let* ((cmds (pdf-util-convert--create-commands spec)) 1033 (proc 1034 (apply #'start-process "pdf-util-convert" 1035 (get-buffer-create "*pdf-util-convert-output*") 1036 pdf-util-convert-program 1037 `(,in-file ,@cmds ,out-file)))) 1038 (when callback 1039 (set-process-sentinel proc callback)) 1040 proc))) 1041 1042 (defun pdf-util-convert-page (&rest specs) 1043 "Convert image of current page according to SPECS. 1044 1045 Return the converted PNG image as a string. See also 1046 `pdf-util-convert'." 1047 1048 (pdf-util-assert-pdf-window) 1049 (let ((in-file (make-temp-file "pdf-util-convert" nil ".png")) 1050 (out-file (make-temp-file "pdf-util-convert" nil ".png"))) 1051 (unwind-protect 1052 (let ((image-data 1053 (plist-get (cdr (pdf-view-current-image)) :data))) 1054 (with-temp-file in-file 1055 (set-buffer-multibyte nil) 1056 (set-buffer-file-coding-system 'binary) 1057 (insert image-data)) 1058 (pdf-util-munch-file 1059 (apply #'pdf-util-convert 1060 in-file out-file specs))) 1061 (when (file-exists-p in-file) 1062 (delete-file in-file)) 1063 (when (file-exists-p out-file) 1064 (delete-file out-file))))) 1065 1066 1067 (defun pdf-util-convert--create-commands (spec) 1068 (let ((fg "red") 1069 (bg "red") 1070 formats result cmds s) 1071 (while (setq s (pop spec)) 1072 (unless spec 1073 (error "Missing value in convert spec:%s" (cons s spec))) 1074 (cl-case s 1075 (:foreground 1076 (setq fg (pop spec))) 1077 (:background 1078 (setq bg (pop spec))) 1079 (:commands 1080 (setq cmds (pop spec))) 1081 (:formats 1082 (setq formats (append formats (pop spec) nil))) 1083 (:apply 1084 (dolist (m (pop spec)) 1085 (pdf-util-with-edges (m) 1086 (let ((alist (append 1087 (mapcar (lambda (f) 1088 (cons (car f) 1089 (if (stringp (cdr f)) 1090 (cdr f) 1091 (funcall (cdr f) m)))) 1092 formats) 1093 `((?g . ,(format "%dx%d+%d+%d" 1094 m-width m-height 1095 m-left m-top)) 1096 (?x . ,m-left) 1097 (?X . ,m-right) 1098 (?y . ,m-top) 1099 (?Y . ,m-bot) 1100 (?w . ,(- m-right m-left)) 1101 (?h . ,(- m-bot m-top)) 1102 (?f . ,fg) 1103 (?b . ,bg))))) 1104 (dolist (fmt cmds) 1105 (push (format-spec fmt alist) result)))))))) 1106 (nreverse result))) 1107 1108 ;; FIXME: Check code below and document. 1109 1110 (defun pdf-util-edges-p (obj &optional relative-p) 1111 "Return non-nil, if OBJ look like edges. 1112 1113 If RELATIVE-P is non-nil, also check that all values <= 1." 1114 1115 (and (consp obj) 1116 (ignore-errors (= 4 (length obj))) 1117 (cl-every (lambda (x) 1118 (and (numberp x) 1119 (>= x 0) 1120 (or (null relative-p) 1121 (<= x 1)))) 1122 obj))) 1123 1124 (defun pdf-util-edges-empty-p (edges) 1125 "Return non-nil, if EDGES area is empty." 1126 (pdf-util-with-edges (edges) 1127 (or (<= edges-width 0) 1128 (<= edges-height 0)))) 1129 1130 (defun pdf-util-edges-inside-p (edges pos &optional epsilon) 1131 (pdf-util-edges-contained-p 1132 edges 1133 (list (car pos) (cdr pos) (car pos) (cdr pos)) 1134 epsilon)) 1135 1136 (defun pdf-util-edges-contained-p (edges contained &optional epsilon) 1137 (unless epsilon (setq epsilon 0)) 1138 (pdf-util-with-edges (edges contained) 1139 (and (<= (- edges-left epsilon) 1140 contained-left) 1141 (>= (+ edges-right epsilon) 1142 contained-right) 1143 (<= (- edges-top epsilon) 1144 contained-top) 1145 (>= (+ edges-bot epsilon) 1146 contained-bot)))) 1147 1148 (defun pdf-util-edges-intersection (e1 e2) 1149 (pdf-util-with-edges (edges1 e1 e2) 1150 (let ((left (max e1-left e2-left)) 1151 (top (max e1-top e2-top)) 1152 (right (min e1-right e2-right)) 1153 (bot (min e1-bot e2-bot))) 1154 (when (and (<= left right) 1155 (<= top bot)) 1156 (list left top right bot))))) 1157 1158 (defun pdf-util-edges-union (&rest edges) 1159 (if (null (cdr edges)) 1160 (car edges) 1161 (list (apply #'min (mapcar #'car edges)) 1162 (apply #'min (mapcar #'cadr edges)) 1163 (apply #'max (mapcar #'cl-caddr edges)) 1164 (apply #'max (mapcar #'cl-cadddr edges))))) 1165 1166 (defun pdf-util-edges-intersection-area (e1 e2) 1167 (let ((inters (pdf-util-edges-intersection e1 e2))) 1168 (if (null inters) 1169 0 1170 (pdf-util-with-edges (inters) 1171 (* inters-width inters-height))))) 1172 1173 (defun pdf-util-read-image-position (prompt) 1174 "Read a image position using prompt. 1175 1176 Return the event position object." 1177 (save-selected-window 1178 (let ((ev (pdf-util-read-click-event 1179 (propertize prompt 'face 'minibuffer-prompt))) 1180 (buffer (current-buffer))) 1181 (unless (mouse-event-p ev) 1182 (error "Not a mouse event")) 1183 (let ((posn (event-start ev))) 1184 (unless (and (eq (window-buffer 1185 (posn-window posn)) 1186 buffer) 1187 (eq 'image (car-safe (posn-object posn)))) 1188 (error "Invalid image position")) 1189 posn)))) 1190 1191 (defun pdf-util-read-click-event (&optional prompt seconds) 1192 (let ((down (read-event prompt seconds))) 1193 (unless (and (mouse-event-p down) 1194 (equal (event-modifiers down) 1195 '(down))) 1196 (error "No a mouse click event")) 1197 (let ((up (read-event prompt seconds))) 1198 (unless (and (mouse-event-p up) 1199 (equal (event-modifiers up) 1200 '(click))) 1201 (error "No a mouse click event")) 1202 up))) 1203 1204 (defun pdf-util-image-map-mouse-event-proxy (event) 1205 "Set POS-OR-AREA in EVENT to 1 and unread it." 1206 (interactive "e") 1207 (setcar (cdr (cadr event)) 1) 1208 (setq unread-command-events (list event))) 1209 1210 (defun pdf-util-image-map-divert-mouse-clicks (id &optional buttons) 1211 (dolist (kind '("" "down-" "drag-")) 1212 (dolist (b (or buttons '(2 3 4 5 6))) 1213 (local-set-key 1214 (vector id (intern (format "%smouse-%d" kind b))) 1215 'pdf-util-image-map-mouse-event-proxy)))) 1216 1217 (defmacro pdf-util-do-events (event-resolution-unread-p condition &rest body) 1218 "Read EVENTs while CONDITION executing BODY. 1219 1220 Process at most 1/RESOLUTION events per second. If UNREAD-p is 1221 non-nil, unread the final non-processed event. 1222 1223 \(FN (EVENT RESOLUTION &optional UNREAD-p) CONDITION &rest BODY\)" 1224 (declare (indent 2) (debug ((symbolp form &optional form) form body))) 1225 (cl-destructuring-bind (event resolution &optional unread-p) 1226 event-resolution-unread-p 1227 (let ((*seconds (make-symbol "seconds")) 1228 (*timestamp (make-symbol "timestamp")) 1229 (*clock (make-symbol "clock")) 1230 (*unread-p (make-symbol "unread-p")) 1231 (*resolution (make-symbol "resolution"))) 1232 `(let* ((,*unread-p ,unread-p) 1233 (,*resolution ,resolution) 1234 (,*seconds 0) 1235 (,*timestamp (float-time)) 1236 (,*clock (lambda (&optional secs) 1237 (when secs 1238 (setq ,*seconds secs 1239 ,*timestamp (float-time))) 1240 (- (+ ,*timestamp ,*seconds) 1241 (float-time)))) 1242 (,event (read-event))) 1243 (while ,condition 1244 (when (<= (funcall ,*clock) 0) 1245 (progn ,@body) 1246 (setq ,event nil) 1247 (funcall ,*clock ,*resolution)) 1248 (setq ,event 1249 (or (read-event nil nil 1250 (and ,event 1251 (max 0 (funcall ,*clock)))) 1252 ,event))) 1253 (when (and ,*unread-p ,event) 1254 (setq unread-command-events 1255 (append unread-command-events 1256 (list ,event)))))))) 1257 1258 (defmacro pdf-util-track-mouse-dragging (event-resolution &rest body) 1259 "Read mouse movement events executing BODY. 1260 1261 See also `pdf-util-do-events'. 1262 1263 This macro should be used inside a command bound to a down-mouse 1264 event. It evaluates to t, if at least one event was processed in 1265 BODY, otherwise nil. In the latter case, the only event (usually 1266 a mouse click event) is unread. 1267 1268 \(FN (EVENT RESOLUTION) &rest BODY\)" 1269 (declare (indent 1) (debug ((symbolp form) body))) 1270 (let ((ran-once-p (make-symbol "ran-once-p"))) 1271 `(let (,ran-once-p) 1272 (track-mouse 1273 (pdf-util-do-events (,@event-resolution t) 1274 (mouse-movement-p ,(car event-resolution)) 1275 (setq ,ran-once-p t) 1276 ,@body)) 1277 (when (and ,ran-once-p 1278 unread-command-events) 1279 (setq unread-command-events 1280 (butlast unread-command-events))) 1281 ,ran-once-p))) 1282 1283 (defun pdf-util-remove-duplicates (list) 1284 "Remove duplicates from LIST stably using `equal'." 1285 (let ((ht (make-hash-table :test 'equal)) 1286 result) 1287 (dolist (elt list (nreverse result)) 1288 (unless (gethash elt ht) 1289 (push elt result) 1290 (puthash elt t ht))))) 1291 1292 (provide 'pdf-util) 1293 1294 ;;; pdf-util.el ends here