config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

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