https://github.com/dalanicolai/djvu3
Raw File
Tip revision: 32e8a8bae9f9a62a512f598fae0af58470ec382f authored by Daniel Nicolai on 16 September 2021, 11:58:48 UTC
Merge pull request #2 from c1-g/main
Tip revision: 32e8a8b
djvu3.el
;;; djvu3.el --- Extend djvu.el

;; Copyright (C) 2021 Free Software Foundation, Inc.


;; Author: D. L. Nicolai <dalanicolaih@gmail.com>
;; Version: 1.0
;; Package-Requires: ((svg "1.1"))
;; Keywords: documents
;; URL: https://github.com/dalanicolai/djvu+

;; This file is not part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; This package provides some extensions to djvu.el. In particular it implements
;; display of annotations with convenient keyboard function and fast djvu-occur
;; search functionality, and continue at last viewed page when revisiting a
;; file.

;; DONE check `djvu2.el' for arrow-keybindings in djvu-image-mode map
;; DONE try to implement image invert (see `djvu-toggle-invert')
;; DONE update `djvu-mouse-rect-area', `djvu-mouse-text-area-internal',
;;`djvu-mouse-line-area-arrow' and `djvu-mouse-line-area-internal' functions
;; (see `djvu2.el`)
;; DONE add clickable links by adding `:map' keyword to `create-image' as follows:
;; :map '(((rect . ((0 . 0) . (100 . 100))) area4 (:pointer hand)))


(require 'djvu)
(require 'svg)
(require 'tablist)
(require 'transient)

;;; EXTEND SVG WITH MARKERS (INCL. ARROWHEADS)

(defun svg-marker (svg id width height &optional color reverse)
  "Add a gradient with ID to SVG.
TYPE is `linear' or `radial'.
STOPS is a list of percentage/color pairs."
  (svg--def
   svg
   (apply
    'dom-node
    'marker 
    `((id . ,id)
      (viewBox . "0 0 10 10")
      (refX . 5)
      (refY . 5)
      ,(pcase id
         ("arrow" `(markerWidth . ,width))
         ("dot" `(markerWidth . ,width)))
      ,(pcase id
         ("arrow" `(markerHeight . ,height))
         ("dot" `(markerHeight . ,height)))
      ,(pcase id
         ;; ("arrow" '(orient . auto-start-reverse))))
         ("arrow" (if reverse
                      '(orient . auto)
                    '(orient . auto-start-reverse)))))
    (pcase id
      ("arrow" (list (dom-node 'path `((d . "M 0 0 L 10 5 L 0 10 z")
                                       (fill . ,(or color "black"))))))
      ("dot" (list (dom-node 'circle `((cx . 5)
                                       (cy . 5)
                                       (r . 5)
                                       (fill . ,(or color "black"))))))))))

;;; START OF DJVU-EXTENSION

(defcustom djvu-restore-filename (if (and (fboundp 'dotspacemacs-directory)
                                          dotspacemacs-directory)
                                     (concat dotspacemacs-directory ".djvu-view-restore")
                                   ".djvu-view-restore")
  "Filename to save the last known pdf position."
  :group 'djvu
  :type 'string)

(defvar-local djvu-invert nil
  "If non-nil invert djvu image colors (dark-mode).")

(defvar-local djvu-doc-hotspots nil
  "Hotspots (i.e. cliackable links) of current page of a Djvu document.
This is a list.")

;;; Annotations

(defun djvu-annots-listify (doc)
"Create list with annotations.
DOC is a djvu-read-buffer object.
The list is created by wrapping the annotations in the annotation
buffer in a list after converting the color hex-symbols into
strings."
  (interactive)
  (let ((buffer (get-buffer-create "*annot-list*")))
    (with-current-buffer (djvu-ref annot-buf doc)
      (copy-to-buffer "*annot-list*" (point-min) (point-max)))
    (with-current-buffer buffer
      (while (re-search-forward " \\(#[[:alnum:]]+\\)" nil t)
        (replace-match " \"\\1\""))
      (goto-char (point-min))
      (insert "(")
      (goto-char (point-max))
      (insert ")")
      (emacs-lisp-mode)
      (indent-region (point-min) (point-max))
      (read (buffer-string)))))

;; Another fast function for listify (for use when annot-buffer is not available
;; .e.g. in DocView mode)

;; (defun djvu-annots-listify (doc)
;;   "Return list of page's annotations.
;; Code written to be usable in djvu- and in doc-view-mode."
;;   (interactive)
;;   (let ((file doc))
;;     (with-temp-buffer
;;       (shell-command (format "djvused %s -e 'select %s; print-ant'"
;;                              (shell-quote-argument file)
;;                              page)
;;                      (current-buffer))
;;       (goto-char (point-min))
;;       (while (re-search-forward " #\\([[:alnum:]]+\\)" nil t)
;;         (replace-match " \"\\1\""))
;;       (goto-char (point-min))
;;       (insert "(")
;;       (goto-char (point-max))
;;       (insert ")")
;;       (read (buffer-string)))))

(makunbound 'djvu-image-mode-map)
(define-minor-mode djvu-image-mode
  "Image display of current page."
  :lighter "Image"
  :keymap '(([down-mouse-1]   . djvu-mouse-drag-track-area)
            ([S-down-mouse-1] . djvu-mouse-drag-track-area)
            ([C-down-mouse-1] . djvu-mouse-drag-track-area)
            ([down-mouse-2]   . (lambda (event) (interactive "e")
                                  (djvu-mouse-drag-track-area event t)))
            ([S-down-mouse-2] . (lambda (event) (interactive "e")
                                  (djvu-mouse-drag-track-area event 'horiz)))
            ([C-down-mouse-2] . (lambda (event) (interactive "e")
                                  (djvu-mouse-drag-track-area event 'vert)))
            ([C-S-down-mouse-2] . (lambda (event) (interactive "e")
                                    (djvu-mouse-drag-track-area event 'arrow)))
            ([drag-mouse-1]   . djvu-mouse-rect-area)
            ([S-drag-mouse-1] . djvu-mouse-text-area)
            ([C-drag-mouse-1] . djvu-mouse-text-area-pushpin)
            ([drag-mouse-2]   . djvu-mouse-line-area)
            ([S-drag-mouse-2] . djvu-mouse-line-area-horiz)
            ([C-drag-mouse-2] . djvu-mouse-line-area-vert)
            ([C-S-drag-mouse-2] . djvu-mouse-line-area-arrow)
            ;; FIXME: The following binding has no effect.  Why??
            ([M-down-mouse-1] . djvu-mouse-drag-track-area)
            ([M-drag-mouse-1] . djvu-mouse-word-area)
            ([down-mouse-3]   . djvu-mouse-drag-track-area) ; substitute
            ([drag-mouse-3]   . djvu-mouse-word-area) ; substitute
            ;;
            ("C-c m" . djvu-invert)
            ("+" . djvu-image-zoom-in)
            ("-" . djvu-image-zoom-out))

  ;; Adopted from `doc-view-mode'
  (image-mode-setup-winprops) ; record current scroll settings
  ;; Don't scroll unless the user specifically asked for it.
  (setq-local auto-hscroll-mode nil)

  (if (and djvu-image-mode
           (not (get-text-property (point-min) 'display)))
      ;; Remember DPOS if we enable `djvu-image-mode'.
      (djvu-set read-pos (let (djvu-image-mode)
                           (djvu-read-dpos))))
  (let ((tmp (and (not djvu-image-mode)
                  (get-text-property (point-min) 'display))))
    (djvu-image)
    ;; Go to DPOS if we disable `djvu-image-mode'.
    (if tmp (djvu-goto-read (djvu-ref read-pos)))))

(defun djvu-event-to-area (event &optional dir)
  "Convert mouse EVENT to Djvu area coordinates."
  (let* ((e-start (event-start event))
         (e-end   (event-end   event))
         (_ (unless (and (posn-image e-start) (posn-image e-end))
              (user-error "Area not over image")))
         (start (posn-object-x-y e-start))
         (end   (posn-object-x-y e-end))
         (x1 (car start)) (y1 (cdr start)) (x2 (car end)) (y2 (cdr end))
         (size (posn-object-width-height e-start))
         (_ (if (equal size '(0 . 0))
                (error "See Emacs bug#18839 (GNU Emacs 24.4)")))
         (width  (/ (float (car (djvu-ref pagesize))) (car size)))
         (height (/ (float (cdr (djvu-ref pagesize))) (cdr size)))
         (area
          (list (round (* (if (memq dir '(vert free arrow))
                              x1 (min x1 x2))
                          width))
                (round (* (- (cdr size) (if (memq dir '(horiz free arrow))
                                            y1 (max y1 y2)))
                          height))
                (round (* (if (memq dir '(vert free arrow))
                              x2 (max x1 x2))
                          width))
                (round (* (- (cdr size) (if  (memq dir '(horiz free arrow))
                                            y2 (min y1 y2)))
                          height)))))
    (djvu-set read-pos (djvu-mean-dpos area))
    area))

(defun djvu-mouse-line-area-arrow (event)
  (interactive "e")
  (djvu-mouse-line-area-internal event 'arrow))

(defun djvu-mouse-line-area-internal (event &optional dir)
  (djvu-with-event-buffer event
    (let* ((line (djvu-event-to-area event dir))
           (color (djvu-interactive-color djvu-color-line))
           (text (read-string (format "(%s) Line: " color)
                              nil nil nil djvu-inherit-input-method)))
      (cond ((eq dir 'horiz)
             (setq line (list (nth 0 line) (nth 1 line)
                              (nth 2 line) (nth 1 line))))
            ((eq dir 'vert)
             (setq line (list (nth 0 line) (nth 1 line)
                              (nth 0 line) (nth 3 line)))))
      (if (eq dir 'arrow)
          (djvu-line-area nil text line nil t djvu-line-width djvu-color-line)
        (djvu-line-area nil text line nil nil djvu-line-width djvu-color-line))
      (djvu-set image nil)
      (djvu-image nil t))))

;; Extended version of `djvu-image' from original djvu.el. Implements display of
;; annotations with svg.el and embedding the ppm image in an svg image. Also
;; adds update flag to allow for immediate update after creating annotation.

(defun djvu-image (&optional isize update match)
  "If `djvu-image-mode' is enabled, display image of current Djvu page.
Otherwise remove the image."
  ;; Strange!  `djvu-image' modifies the buffer (its text properties).
  ;; Nonetheless, we end up with an unmodified buffer.  This holds,
  ;; in particular, for the "bare" calls of `djvu-image' by
  ;; `djvu-image-zoom-in' and `djvu-image-zoom-out'.
  (if (not djvu-image-mode)
      (if (get-text-property (point-min) 'display)
          (let (buffer-read-only)
            (remove-text-properties (point-min) (point-max) '(display nil))))
    ;; Update image if necessary.
      (when (or update (not (eq (djvu-ref page) (car (djvu-ref image))))
                (and isize
                     (not (eq isize (nth 1 (djvu-ref image))))))
        (let ((isize (or isize
                         (nth 1 (djvu-ref image))
                         djvu-image-size))
              (doc djvu-doc)
              (inhibit-quit t)
              (invert djvu-invert))
          (with-temp-buffer
            (set-buffer-multibyte nil)
            (let* ((coding-system-for-read 'raw-text)
                   ;; For a rectangular image, ISIZE does not give us
                   ;; the actual size of the image, but (max width height)
                   ;; will be equal to ISIZE.
                   (status (call-process "ddjvu" nil t nil
                                         (format "-size=%dx%d" isize isize)
                                         "-format=ppm"
                                         (format "-page=%d" (djvu-ref page doc))
                                         (djvu-ref file doc))))
              (unless (zerop status)
                (error "Ddjvu error %s" status))
              (when invert
                (call-process-region (point-min) (point-max) "pnminvert" t t))
              ;; (goto-char (point-min))
              ;; (forward-line 2)
              ;; (let ((maxval (print (thing-at-point 'number))))
              ;;   (forward-line 1)
              ;;   (while (not (eobp))
              ;;     (let ((char (char-after)))
              ;;       (delete-char 1)
              ;;       (insert-byte (- maxval char) 1)))))
              (let* ((ppm (create-image (buffer-substring-no-properties
                                         (point-min) (point-max))
                                        'pbm t))
                     (size (image-size ppm t))
                     (scaling-factor (/ isize (float (cdr (djvu-ref pagesize doc)))))
                     (svg (svg-create (car size) (cdr size)))
                     url-data)
                (svg-marker svg "arrow" 8 8 "black" t)
                (svg-embed svg (image-property ppm :data) "image/x-portable-bitmap" t
                           :width (format "%spx" (car size)) :height (format "%spx" (cdr size))
                           :x "0px" :y "0px")

                ;; Draw annotations
                (dolist (annot (djvu-annots-listify doc))
                  (when annot
                    (pcase (car annot)
                      ('background (message "Viewer are color should be %s (not (yet) implemented)" (car annot)))
                      ('zoom (message "Zoom value should be %s (not (yet) implemented)" (car annot)))
                      ('mode (message "Mode value should be %s (not (yet) implemented)" (car annot)))
                      ('align (message "Horizontal annot vertical align should be %s %s (not (yet) implemented)"
                                       (nth 1 annot) (nth 2 annot)))
                      ('maparea
                       (let* ((url (nth 1 annot))
                              (comment (nth 2 annot))
                              (area (if (listp (car (nth 3 annot)))
                                        (nth 3 annot)
                                      (list (nth 3 annot)))))
                         (dolist (a area)
                           (let* ((coords (mapcar (lambda (x) (* x scaling-factor)) (cdr a)))
                                  ;; y coord is defined from bottom up, while svg is top down
                                  ;; therefore we must add ywidth to y0 (hence we assign ywidth before y0)
                                  (xwidth (nth 2 coords))
                                  (ywidth (nth 3 coords))
                                  (x0 (nth 0 coords))
                                  (y0 (- isize (nth 1 coords)))
                                  (x1 (nth 2 coords))
                                  (y1 (- isize (nth 3 coords)))
                                  (options (nthcdr 4 annot))
                                  (svg-command-data (pcase (car a)
                                                      ('rect (cons 'svg-rectangle
                                                                   (list x0 y1 (- x1 x0) (- y0 y1))))
                                                      ;; transformation for area from direct djvused annots
                                                      ;; ('rect (list x0 (- y0 ywidth) xwidth ywidth))
                                                      ('oval (cons 'svg-ellipse
                                                                   (list (/ (+ x0 x1) 2)
                                                                         (/ (+ y0 y1) 2)
                                                                         (/ (- x1 x0) 2)
                                                                         (/ (- y0 y1) 2))))
                                                      ;; ('poly 'svg-polygon)
                                                      ('text (cons 'svg-text
                                                                   (list comment
                                                                         :x x0
                                                                         :y y0
                                                                         :font-size (apply 'min (list (/ (- x1 x0) (length comment) 0.5)
                                                                                                      (- y0 y1))))))
                                                      ('line (cons 'svg-line (list x0 y0 x1 y1))))))
                             (apply (car svg-command-data)
                                    svg
                                    (append
                                     (cdr svg-command-data)
                                     (if-let (x (car (alist-get 'opacity options)))
                                         (list :opacity (/ x 100.0))
                                       (pcase (car a)
                                         ((or 'text 'line) (list :opacity 1.0))
                                         (_ (list :opacity 0.3))))
                                     (when-let (x (car (alist-get 'hilite options)))
                                       (list :fill-color (car (rassoc (format "%s" x) djvu-color-alist))))
                                     (when-let (x (car (alist-get 'width options)))
                                       (list :stroke-width x))
                                     (if-let (x (car (alist-get 'lineclr options)))
                                         (list :stroke-color (car (rassoc (format "%s" x) djvu-color-alist)))
                                       (when (equal (car a) 'line)
                                         (list :stroke-color "black")))
                                     (when-let (x (car (alist-get 'textclr options)))
                                       (list :fill x))
                                     (when (assoc 'arrow options)
                                       (list :marker-end "url(#arrow)"))
                                     ))
                             (when (not (= (length url) 0))
                               (push (list (cons 'rect
                                                 (cons (cons (truncate x0) (truncate y1))
                                                       (cons (truncate x1) (truncate y0))))
                                           (intern (mapconcat 'number-to-string
                                                              (mapcar 'truncate (list x0 y1 x1 y0)) "-"))
                                           (list 'pointer 'hand 'help-echo url))
                                     url-data))
                             ))))
                      )))
                (when match
                  (let* ((coords (mapcar (lambda (x) (* x scaling-factor)) match))
                         (x0 (nth 0 coords))
                         (y0 (- isize (nth 1 coords)))
                         (x1 (nth 2 coords))
                         (y1 (- isize (nth 3 coords))))
                    (apply 'svg-rectangle svg (append
                                               (list x0 y1 (- x1 x0) (- y0 y1))
                                               (list :fill-color "green")
                                               (list :opacity 0.5)))))
                ;; (let* ((url (nth 1 annot))
                ;;        (comment (nth 2 annot))
                ;;        (area (mapcar (lambda (x) (* x scaling-factor))  (cdr (nth 3 annot))))
                ;;        ;; area y coord is defined from bottom up, while svg is top down
                ;;        ;; therefore we must add ywidth to y0 (hence we assign ywidth before y0)
                ;;        (xwidth (nth 2 area))
                ;;        (ywidth (nth 3 area))
                ;;        (x0 (nth 0 area))
                ;;        (y0 (- isize (+ (nth 1 area) ywidth)))
                ;;        (options (nthcdr 4 annot)))
                ;;   (svg-rectangle svg x0 y0 xwidth ywidth :fill "red" :opacity "0.5" :stroke "red")))))
                (djvu-set image
                          (append (list (djvu-ref page doc) isize)
                                  ;; Images are lists
                                  (svg-image svg
                                             :map url-data
                                             ;; :map '(((rect . ((0 . 0) . (100 . 100))) area4 (:pointer hand)))
                                             ))
                          doc)
                (djvu-set hotspots url-data doc)
                (djvu-ref hotspots doc)
                )))))
    ;; Display image.
    (let ((hscroll (window-hscroll))
          buffer-read-only)
      (if (= (point-min) (point-max)) (insert " "))
      (put-text-property (point-min) (point-max)
                         'display (nthcdr 2 (djvu-ref image)))
      (set-window-hscroll (selected-window) hscroll))
    (dolist (x (djvu-ref hotspots))
      (local-set-key
       (vector (nth 1 x) 'mouse-1)
       (lambda (event)
         (interactive "@e")
         (let ((hs-list (djvu-ref hotspots)))
           (while (not (eq (posn-area (nth 1 event)) (nth 1 (car hs-list))))
             (setq hs-list (cdr hs-list)))
           (djvu-goto-page (string-to-number (substring (plist-get (nth 2 (car hs-list)) 'help-echo) 1)))))))))

;; Shorter verion of `djvu-image-rect' of original function in djvu.el. Possibly
;; cut off to much.

;; THIS FUNCTION IS NOT USED IN DJVU3
(defun djvu-image-rect (&optional event line)
  (print "This message from `djvu-image-rect' can be neglected. How to unbind in djvu.el defined keymap?"))
;;   "For PPM image specified via EVENT mark rectangle by inverting bits."
;;   ;; FIXME: Can the following be implemented more efficiently in the
;;   ;; image display code?  Could this be useful for other packages, too?
;;   (if event
;;       (let* ((e-start (event-start event))
;;              (e-end   (event-end   event))
;;              (_ (unless (and (posn-image e-start) (posn-image e-end))
;;                   (user-error "Area not over image")))
;;              (start (posn-object-x-y e-start))
;;              (end   (posn-object-x-y e-end))
;;              (x1 (if line (car start)
;;                    (min (car start) (car end))))
;;              (y1 (if line (cdr start)
;;                    (min (cdr start) (cdr end))))
;;              (x2 (if line (car end)
;;                    (max (car start) (car end))))
;;              (y2 (if line (cdr end)
;;                    (max (cdr start) (cdr end))))
;;              (image (copy-sequence (nth 6 (djvu-ref image))))
;;              ))))
    ;;          ;; (_ (unless (string-match "\\`P6\n\\([0-9]+\\) +\\([0-9]+\\)\n\\([0-9]+\\)\n" image)
    ;;          ;;      (error "Not a PPM image")))
    ;;          (width (djvu-match-number 1 image))
    ;;          ; (height (djvu-match-number 2 image))
    ;;          (depth (djvu-match-number 3 image))
    ;;          (i0 (match-end 0))
    ;;          (old-image (get-text-property (point-min) 'display)))
    ;;     (unless (= depth 255)
    ;;       (error "Cannot handle depth %d" depth))
    ;;     (cl-flet ((invert (i imax)
    ;;                       (while (< i imax)
    ;;                         ;; Invert bits
    ;;                         (aset image i (- 255 (aref image i)))
    ;;                         (setq i (1+ i)))))
    ;;       (if (not line)
    ;;           (while (< y1 y2)
    ;;             ;; i = i0 + 3 * (y * width + x)
    ;;             (let ((i (+ i0 (* 3 (+ x1 (* width y1))))))
    ;;               (invert i (+ i (* 3 (- x2 x1)))))
    ;;             (setq y1 (1+ y1)))
    ;;         (cond ((eq line 'horiz) (setq y2 y1))
    ;;               ((eq line 'vert)  (setq x2 x1)))
    ;;         (if (< (abs (- x2 x1)) (abs (- y2 y1)))
    ;;             (let ((dx (/ (- x2 x1) (float (- y2 y1))))
    ;;                   (y y1) (step (cl-signum (- y2 y1))))
    ;;               (while (/= y y2)
    ;;                 ;; x = (y - y1) * dx + x1
    ;;                 (let ((i (+ i0 (* 3 (+ (* y width) x1
    ;;                                        (round (* (- y y1) dx)))))))
    ;;                   (invert i (+ i 3)))
    ;;                 (setq y (+ y step))))
    ;;           (let ((dy (/ (- y2 y1) (float (- x2 x1))))
    ;;                 (x x1) (step (cl-signum (- x2 x1))))
    ;;             (while (/= x x2)
    ;;               ;; y = (x - x1) * dy + y1
    ;;               (let ((i (+ i0 (* 3 (+ x (* (+ y1 (round (* (- x x1) dy)))
    ;;                                           width))))))
    ;;                 (invert i (+ i 3)))
    ;;               (setq x (+ x step)))))))
    ;;     (with-silent-modifications
    ;;       (put-text-property
    ;;        (point-min) (point-max) 'display
    ;;        (create-image image 'pbm t)))
    ;;     (image-flush old-image))
    ;; ;; Restore unmodified image
    ;; (let ((old-image (get-text-property (point-min) 'display)))
    ;;   (with-silent-modifications
    ;;     (put-text-property (point-min) (point-max)
    ;;                        'display (nthcdr 2 (djvu-ref image))))
    ;;   (image-flush old-image))))


;;; Keyboard annotation functionality

(defun djvu-kb-annot-get-matches (pattern)
"Create list with position of matches.
The buffer is searched for PATTERN. The list contains starting
position for keyboard annotation regions."
  (if (search-forward pattern nil t)
      (cons (point) (djvu-kb-annot-get-matches pattern))))

(defun djvu-annot-get-ranges (patt1 patt2)
  "Create alist with starting en ending positions for annotations.
The buffer is searched for the strings PATT1 and PATT2 as
starting and ending positions"
        (let ((start-points (progn (goto-char (point-min))
                                   (djvu-kb-annot-get-matches patt1))))
          (mapcar (lambda (x)
                    (goto-char x)
                    (cons x (djvu-kb-annot-get-matches patt2))) start-points)))

(defun djvu-annot-get-words (beg end &optional last)
"Get words in subregion of annotation.
With LAST is nil/non-nil, the first/last (partial) word is
replaced with the complete word at point BEG/END."
  (let ((word-list (split-string (buffer-substring-no-properties beg end)))
        (full-word (save-excursion
                     (goto-char (if last
                                    end
                                  beg))
                     (thing-at-point 'word t))))
  (when last
    (setq word-list (nreverse word-list)))
  (setq word-list (cons full-word (cdr word-list)))
  (when last
    (setq word-list (nreverse word-list)))
  (string-join word-list " ")))


(defun ivy-annot-collection (ranges)
  "Print start and end strings of annotation.
This function is an adaptation of `djvu-rect-region'. The
function can be used to provide better info in completion frameworks."
    (mapcan (lambda (start-point-list)
              (mapcar (lambda (end)
                        (print end)
                        (let ((beg (1- (car start-point-list))))
                          (unless (get-text-property beg 'word)
                            (user-error "Start position `%s' not a word" beg))
                          (unless (get-text-property (1- end) 'word)
                            (user-error "End position `%s' not a word" end))
                          (let ((lines (djvu-region-count beg end 'line))
                                (paras (djvu-region-count beg end 'para))
                                (regions (djvu-region-count beg end 'region))
                                (columns (djvu-region-count beg end 'column))
                                areas)
                            (unless (and (>= 1 paras) (>= 1 regions) (>= 1 columns))
                              (user-error "Region spans multiple paragraphs"))

                            (if (eq 1 lines)
                                (let ((first-words (djvu-annot-get-words beg end)))
                                  (setq regions (list first-words
                                                      beg end)))

                              (if (eq 2 lines)
                                  (let* ((l1e (djvu-property-end (1+ beg) 'line))
                                         (l2b (djvu-property-beg (1- end) 'line))
                                         (c1 (djvu-scan-zone beg (djvu-property-end (1+ beg) 'line) 'word))
                                         (c2 (djvu-scan-zone (djvu-property-beg (1- end) 'line) end 'word)))
                                    ;; If BEG is beginning of first line, both lines share same left margin.
                                    (if (and (= beg (djvu-property-beg beg 'line))
                                             (djvu-areas-justify t c1 c2))
                                        (djvu-justify-areas 'min 0 c1 c2))
                                    ;; If END is end of second line, both lines share same right margin.
                                    (if (and (= end (djvu-property-end end 'line))
                                             (djvu-areas-justify nil c2 c1))
                                        (djvu-justify-areas 'max 2 c1 c2))
                                    (if (<= (aref c1 0) (aref c2 2))
                                        ;; Lower bound of upper box and upper bound of lower box coincide.
                                        (let ((tmp (/ (+ (aref c1 1) (aref c2 3)) 2)))
                                          (aset c1 1 tmp) (aset c2 3 tmp)))
                                    (let ((first-words (djvu-annot-get-words beg l1e))
                                          (last-words (djvu-annot-get-words l2b end t)))
                                      (list (concat first-words
                                                    " ... "
                                                    last-words)
                                            beg end)))
                                ;; 3 lines
                                (let* ((l1e (djvu-property-end (1+ beg) 'line))
                                       (l2b (djvu-property-beg (1- end) 'line))
                                       (c1  (djvu-scan-zone beg l1e 'word))
                                       (ci  (djvu-scan-zone (1+ l1e) (1- l2b) 'line))
                                       (c2  (djvu-scan-zone l2b end 'word)))
                                  ;; If BEG is beginning of first line, all lines share same left margin.
                                  (cond ((and (= beg (djvu-property-beg beg 'line))
                                              (djvu-areas-justify t c1 ci c2))
                                         (djvu-justify-areas 'min 0 c1 ci c2))
                                        ((djvu-areas-justify t ci c2)
                                         (djvu-justify-areas 'min 0 ci c2)))
                                  ;; If END is end of last line, all lines share same right margin.
                                  (cond ((and (= end (djvu-property-end end 'line))
                                              (djvu-areas-justify nil c2 ci c1))
                                         (djvu-justify-areas 'max 2 c1 ci c2))
                                        ((djvu-areas-justify nil c1 ci)
                                         (djvu-justify-areas 'max 2 c1 ci)))
                                  (let ((tmp1 (/ (+ (aref c1 1) (aref ci 3)) 2))
                                        (tmp2 (/ (+ (aref ci 1) (aref c2 3)) 2)))
                                    ;; Lower bound of upper boxes and upper bound of lower boxes coincide.
                                    (aset c1 1 tmp1) (aset ci 3 tmp1)
                                    (aset ci 1 tmp2) (aset c2 3 tmp2))
                                  (let ((first-words (djvu-annot-get-words beg l1e))
                                        (last-words (djvu-annot-get-words l2b end t)))
                                    (list (concat first-words
                                                  " ... "
                                                  last-words)
                                          beg end)
                                                        ;; (list c1 ci c2)))
                                  )))))))
                      (cdr start-point-list)))
            ranges)
  )

(defun djvu-keyboard-annot (patt1 patt2)
"Djvu keyboard annotation command.
Highlight a region starting with PATT1 and anding with PATT2 in
djvu buffer. If multiple regions get matched then select correct
one using completion framework."
  (interactive "sFrom (start pattern): \nsTo (end pattern is pattern in last word): ")
  (let* ((ranges (djvu-annot-get-ranges patt1 patt2))
         (collection (ivy-annot-collection ranges))
         (region (alist-get (ivy-read "Select region: " collection) collection nil nil 'string=)))
    (djvu-rect-region (car region) (cadr region) "nil" "nil" "yellow" "50"))
  (when djvu-image-mode
    (djvu-image djvu-image-size t)))


;;; Djvu 0ccur
(defun djvu-assert-djvu-buffer ()
  (unless (equal major-mode 'djvu-read-mode)
    (error "Buffer is not in DJView mode")))

(defun djvu-sexp-line-to-string (line-sexp)
  (mapconcat (lambda (x) (car (nthcdr 5 x))) (nthcdr 5 line-sexp) " "))

;; (defun djvu-occur-tablist ()
;;   (let ((pattern (read-string "List lines matching: "))
;;         tablist
;;         (file (djvu-ref file)))
;;     (dotimes (x (djvu-ref pagemax))
;;     ;; (dotimes (x 9)
;;       (let ((page (+ x 1)))
;;         (with-temp-buffer
;;           (insert (shell-command-to-string
;;                    (format "djvused %s -e 'select %s; print-txt'"
;;                            (shell-quote-argument file)
;;                            page
;;                            pattern)))
;;           (goto-char (point-min))
;;           (while (search-forward-regexp (format "  (word .*%s.*\")" pattern) nil t)
;;             (let ((word-sexp (read (match-string 0)))
;;                   (line-sexp (read (thing-at-point 'list))))
;;               (setq tablist (append
;;                              tablist
;;                              (list (list
;;                                     nil
;;                                     (vector
;;                                      (format "%s" page)
;;                                      (let* ((text (djvu-sexp-line-to-string line-sexp))
;;                                             (start (string-match pattern text))
;;                                             (end (match-end 0)))
;;                                        (add-face-text-property
;;                                         start
;;                                         end
;;                                         'match
;;                                         nil
;;                                         text)
;;                                        text)))))))))))
;;     tablist))

(defun djvu-occur-exhaust-words (pattern word-list parent-list)
  (let (results
        (word (car word-list)))
    (while word
      (if (string-match pattern (nth 5 word))
          (setq results (cons (list word parent-list) results)))
      (setq word-list (cdr word-list))
      (setq word (car word-list)))
    results))

(defun djvu-occur-exhaust-lines (pattern line-list)
  (let (results
        (line (car line-list)))
    (while line
      (if (stringp (nth 5 line))
          (when (string-match pattern (nth 5 line))
            (setq results (cons line results)))
        (setq results
              (append (djvu-occur-exhaust-words pattern (nthcdr 5 line) line)
                      results)))
      (setq line-list (cdr line-list))
      (setq line (car line-list)))
    results))

(defun djvu-occur-exhaust-paras (pattern para-list)
  (let (results
        (para (car para-list)))
    (while para
      (if (stringp (nth 5 para))
          (when (string-match pattern (nth 5 para))
            (setq results (cons para results)))
        (if (equal (car (nth 5 para)) 'line)
            (setq results
                  (append (djvu-occur-exhaust-lines pattern (nthcdr 5 para))
                          results))
          (setq results
                (append (djvu-occur-exhaust-words pattern (nthcdr 5 para) para)
                        results))))
      (setq para-list (cdr para-list))
      (setq para (car para-list)))
    results))

(defun djvu-occur-exhaust-columns (pattern column-list)
  (let (results
        (column (car column-list)))
    (while column
      (setq results (append (djvu-occur-exhaust-paras pattern (nthcdr 5 column))
                                 results))
      (setq column-list (cdr column-list))
      (setq column (car column-list)))
    results))

(defun djvu-set-text ()
  (read
   (concat "("
           (shell-command-to-string
            "djvutxt -detail=word 'The Art of Experimental Physics - Preston, Daryl W_.djvu'")
           ")")))

;; (setq djvu-text-pages
;;       (read
;;        (concat "("
;;                (shell-command-to-string
;;                 "djvutxt -detail=word 'The Art of Experimental Physics - Preston, Daryl W_.djvu'")
;;                ")")))

(defun djvu--parse-metadata ()
  (interactive)
  (with-current-buffer (djvu-ref shared-buf djvu-doc)
    (unless (string= (buffer-string) "")
      (goto-char (point-min))
      (mapcar (lambda (field)
                (concat (symbol-name (car field))
                        "="
                        (cadr field)))
              (cdr (read (current-buffer)))))))

(defun djvu--write-metadata (args)
  (interactive (list (transient-args 'djvu-edit-metadata )))
  (let ((temp-meta-file (make-temp-file "meta")))
    (with-temp-file temp-meta-file
      (dolist (x (nreverse args))
        (let ((field-cons (split-string x "=")))
          (insert (car field-cons))
          (insert " ")
          (insert (format "\"%s\"" (cadr field-cons)))
          (insert "\n"))))
    (shell-command (format "djvused %s -e 'set-meta %s; save'"
                           ;; (transient-arg-value "title=" args))
                           (shell-quote-argument (buffer-file-name))
                           (shell-quote-argument temp-meta-file))))
  (sit-for 5)
  (djvu-revert-buffer)
  (djvu-switch-shared))

(transient-define-prefix djvu-edit-metadata ()
  "Set metadata of djvu document."
  ["Fields"
   [("a" "author" "author=")
    ("b" "booktitle" "booktitle=" )
    ("t" "title" "title=")
    ("n" "note" "note=")
    ("y" "year" "year=")]]
   [("w" "Write metadata" djvu--write-metadata)
    ("q" "Quit" transient-quit-one)]
   (interactive)
   (djvu-switch-shared)
   (goto-char (point-min))
   (let ((metadata (search-forward "(metadata" nil t))
         xmp)
     (goto-char (point-min))
     (when (search-forward "(xmp" nil t)
       (setq xmp t))
     (if (or metadata xmp)
         (when (y-or-n-p (format "Document already contains %s. Editing the metadata will erase existing tags. Continue anyway? "
                                 (cond ((and metadata xmp)  "metadata and xmp tags")
                                       (metadata            "metadata tag")
                                       (xmp                 "xmp tag"))))
           (transient-setup 'djvu-edit-metadata nil nil :value (djvu--parse-metadata)))
       (transient-setup 'djvu-edit-metadata nil nil :value (djvu--parse-metadata)))))

;; (defun djvu-occur-extract-pages-text ()
;;   (let ((i 0)
;;         text-pages
;;         (djvu-text-pages (djvu-set-text)))
;;     (dolist (column-list djvu-text-pages text-pages)
;;       (let* ((contents (nth 5 column-list))
;;              (results (cond ((stringp contents)
;;                                   contents)
;;                                  ((equal (car contents) 'line)
;;                                   (djvu-occur-exhaust-lines (nthcdr 5 column-list)))
;;                                  ((equal (car contents) 'column)
;;                                   (djvu-occur-exhaust-columns (nthcdr 5 column-list))))))
;;         (when results
;;           (setq text-pages (cons (cons i results) text-pages)))
;;         (setq i (1+ i))))))

(defun djvu-occur-tablist ()
  (interactive)
  (let ((pattern (read-string "List lines matching: "))
        tablist
        (i 0)
        text-pages
        (djvu-text-pages (read
                          (concat "("
                                  (shell-command-to-string
                                   (format 
                                    "djvutxt -detail=word '%s'" (djvu-ref file)))
                                  ")"))))
    (dolist (column-list djvu-text-pages text-pages)
      (let* ((contents (nth 5 column-list))
             (results (cond ((stringp contents)
                                  contents)
                                 ((equal (car contents) 'line)
                                  (djvu-occur-exhaust-lines pattern (nthcdr 5 column-list)))
                                 ((equal (car contents) 'column)
                                  (djvu-occur-exhaust-columns pattern (nthcdr 5 column-list))))))
        (when results
          (setq text-pages (append
                            (mapcar (lambda (x) (cons i x)) results)
                            text-pages)))
        (setq i (1+ i))))
              (dolist (x (nreverse text-pages))
                (let ((page (+ (car x) 1))
                      (word-sexp (nth 1 x))
                      (line-sexp (nth 2 x)))
                  (setq tablist (append
                                 tablist
                                 (list (list
                                        `(:page ,page :edges ,(butlast (cdr word-sexp)))
                                        (vector
                                         (format "%s" page)
                                         (let* ((text (djvu-sexp-line-to-string line-sexp))
                                                (start (string-match pattern text))
                                                (end (match-end 0)))
                                           (add-face-text-property
                                            start
                                            end
                                            'match
                                            nil
                                            text)
                                           text))))))))
    tablist))

(defun djvu-occur-show-entry (id)
  (interactive (list (tabulated-list-get-id)))
  (with-selected-window (get-buffer-window target-buffer)
    (djvu-goto-page (plist-get id :page) nil (plist-get id :edges))))

(defvar djvu-occur-mode-map
  (let ((kmap (make-sparse-keymap)))
    (set-keymap-parent kmap tablist-mode-map)
    (define-key kmap (kbd "RET") 'tablist-find-entry)
    (define-key kmap (kbd "M-RET") 'djvu-occur-show-entry)
    (define-key kmap (kbd "C-o") 'tablist-find-entry)
    ;;     (define-key kmap (kbd "SPC") 'djvu-occur-view-occurrence)
    ;;     (define-key kmap (kbd "C-c C-f") 'next-error-follow-minor-mode)
    ;;     (define-key kmap (kbd "g") 'djvu-occur-revert-buffer-with-args)
    ;;     (define-key kmap (kbd "K") 'djvu-occur-abort-search)
    ;;     (define-key kmap (kbd "D") 'djvu-occur-tablist-do-delete)
    ;;     (define-key kmap (kbd "x") 'djvu-occur-tablist-do-flagged-delete)
    ;;     (define-key kmap (kbd "A") 'djvu-occur-tablist-gather-documents)
    kmap)
  "The keymap used for `djvu-occur-buffer-mode'.")

(define-derived-mode djvu-occur-mode
  tablist-mode "DJVUOccur"
  "Major mode for browsing djvu search result"
  (setq-local tabulated-list-format [("page" 10 nil) ("text" 80 nil)])
  (setq-local tablist-operations-function
              (lambda (op &rest _)
                (cl-case op
                  (supported-operations '(find-entry))
                  (find-entry (let ((item (tabulated-list-get-id)))
                                (pop-to-buffer target-buffer)
                                (djvu-goto-page (plist-get item :page) nil (plist-get item :edges)))))))
  (tabulated-list-init-header))

(defun djvu-occur ()
  (interactive)
  (djvu-assert-djvu-buffer)
  (let ((djvu-tablist (djvu-occur-tablist))
        (doc-buffer (current-buffer)))
    (pop-to-buffer "djvu-occur")
    (djvu-occur-mode)
    (setq-local target-buffer doc-buffer)
    (setq-local tabulated-list-entries djvu-tablist)
    (tabulated-list-print)))


;;; Invert toggle
(defun djvu-toggle-invert ()
  (interactive)
  (setq djvu-invert (if djvu-invert
                        nil
                      t))
  (djvu-init-page (djvu-ref page))
  (message "Djvu invert %s" (if djvu-invert
                                "enabled"
                              "disabled")))


;;; Djvu imenu

(defun djvu-imenu-create-index ()
  (or imenu--index-alist
      (setq-local imenu--index-alist
                  (with-current-buffer (djvu-ref bookmarks-buf djvu-doc)
                    (goto-char (point-max))
                    (let (alist)
                      (while (re-search-backward "\"#p*\\([0-9]+\\).*\"" nil t)
                        (let ((pagenumber (string-to-number (match-string-no-properties 1))))
                          (re-search-backward "(\"\\(.+\\)\"")
                          (push (cons (match-string-no-properties 1) pagenumber) alist)))
                      alist)))))

(defun djvu-imenu-goto-index (_index-name position)
  "A wrapper of `djvu-goto-page' that is to be used as `imenu-default-goto-function'.
This functions ignores the first argument passed to it."
  (djvu-goto-page position))

(add-hook 'djvu-read-mode-hook (lambda ()
                                 (setq imenu-create-index-function 'djvu-imenu-create-index)
                                 (setq imenu-default-goto-function 'djvu-imenu-goto-index)))


;;; djvu-restore

(defun djvu-restore ()
  (when (member major-mode '(djvu-read-mode djvu-script-mode djvu-outline-mode))
    (let ((page (djvu-restore-get-page)))
      (when page (djvu-goto-page page)))))

(defun djvu-restore-save ()
  (when (member major-mode '(djvu-read-mode djvu-script-mode djvu-outline-mode))
    (djvu-restore-set-page (djvu-ref page))))

(defun djvu-restore-get-alist ()
  (when (file-exists-p djvu-restore-filename)
    (with-temp-buffer
      (insert-file-contents-literally
       djvu-restore-filename)
      (read (buffer-string)))))

(defun djvu-restore-get-page ()
  "Return restore page."
  (let* ((alist (djvu-restore-get-alist)))
    (cdr (assoc (djvu-ref file) alist))))

(defun djvu-restore-set-page (page)
  "Save restore PAGE."
  (let ((alist (djvu-restore-get-alist)))
    (setf (alist-get (djvu-ref file) alist nil nil 'equal) page)
    (with-temp-file djvu-restore-filename
      (insert (let (print-length) (prin1-to-string alist))))))

(defun djvu-kill-doc-all ()
  "Kill all buffers visiting `djvu-doc' except for the current buffer.
This function is added to `kill-buffer-hook' of all buffers visiting `djvu-doc'
so that killing the current buffer kills all buffers visiting `djvu-doc'."
  (djvu-restore-save)
  (unless djvu-in-kill-doc
    (let ((djvu-in-kill-doc t)
          buffers)
      ;; Sometimes we choke on broken djvu files so that many things
      ;; do not work anymore the way they should.  At least, we want to
      ;; be able to kill the relevant buffers.  So do not bail out here.
      (condition-case nil
          (let ((doc djvu-doc))
            (setq buffers (djvu-buffers doc))
            (unless (memq nil (mapcar 'buffer-live-p buffers))
              (djvu-save doc t))
            (djvu-kill-view doc t))
        (error nil))
      ;; A function in `kill-buffer-hook' should not kill the buffer
      ;; for which we called this hook in the first place, so that
      ;; other functions in this hook can do their job, too.
      (mapc 'kill-buffer (delq (current-buffer) buffers)))))

(defun djvu-find-file (file &optional page view noselect noconfirm)
  "Read and edit Djvu FILE on PAGE.  Return Read buffer.
If VIEW is non-nil start external viewer.
If NOSELECT is non-nil visit FILE, but do not make it current.
If NOCONFIRM is non-nil don't ask for confirmation when reverting buffer
from file."
  (interactive (djvu-read-file-name))
  (unless page (setq page 1))
  (setq file (expand-file-name file))
  ;; Djvu mode needs a local file.  If FILE is located on a remote system,
  ;; you can use something like `file-local-copy' to edit FILE.
  (if (file-remote-p file)
    (user-error "Cannot handle remote Djvu file `%s'" file))
  (unless (and (file-regular-p file)
               (file-readable-p file))
    (user-error "Cannot open Djvu file `%s'" file))
  (let* ((inhibit-quit t)
         (buf-basename (file-name-nondirectory file))
         (file-truename (abbreviate-file-name (file-truename file)))
         (file-number (nthcdr 10 (file-attributes file)))
         (dir (file-name-directory file))
         (read-only (not (file-writable-p file)))
         (old-buf (if (equal buffer-file-truename file-truename)
                      (current-buffer)
                    (find-buffer-visiting file-truename)))
         (doc (and old-buf (buffer-local-value 'djvu-doc old-buf)))
         (old-bufs (and doc (mapcar 'buffer-live-p (djvu-buffers doc)))))
    ;; Sanity check.  We should never need this.
    (when (and old-bufs (memq nil old-bufs))
      (message "Killing dangling Djvu buffers...")
      (djvu-kill-doc doc)
      (setq doc nil old-bufs nil)
      (message "Killing dangling Djvu buffers...Done")
      (sit-for 2))
    ;; Do nothing if we are already visiting FILE such that all buffers
    ;; are properly defined and FILE's modtime matches what we expect.
    (unless (and old-bufs
                 (or (and (equal file-number
                                 (buffer-local-value 'buffer-file-number doc))
                          (verify-visited-file-modtime doc))
                     ;; If a file on disk and a Djvu session are out of sync,
                     ;; we can only continue in hairy, limited ways because
                     ;; Emacs does not copy the contents of FILE into a buffer.
                     ;; Instead, we entirely rely on djvused.
                     (not (or noconfirm
                              (yes-or-no-p
                               (format "Revert buffer from file %s? "
                                       (djvu-ref file doc)))))))
      (unless old-bufs
        (cl-flet ((fun (n)
                       ;; Instead of `generate-new-buffer', we take a detour
                       ;; via `create-file-buffer' so that uniquify can do
                       ;; its job, too.  It does not matter that the arg of
                       ;; `create-file-buffer' does not match `buffer-file-name'
                       ;; because `uniquify-buffer-file-name' only cares
                       ;; about DIR.
                       (create-file-buffer ; needed by uniquify
                        (expand-file-name
                         (concat buf-basename
                                 (nth n djvu-buffer-name-extensions))
                         dir))))
          (if old-buf
              ;; This applies if `find-file-noselect' created OLD-BUF
              ;; in order to visit FILE.  Hence recycle OLD-BUF as Read
              ;; buffer so that `find-file-noselect' can do its job.
              ;; FIXME: this ignores `djvu-buffer-name-extensions'
              ;; because renaming OLD-BUF would break `uniquify'.
              (with-current-buffer old-buf
                (let ((inhibit-read-only t)
                      (buffer-undo-list t))
                  (erase-buffer))
                (setq buffer-file-coding-system 'prefer-utf-8)
                (setq doc old-buf))
            (setq doc (fun 0)))
          (djvu-set read-buf doc doc)
          (djvu-set text-buf (fun 1) doc)
          (djvu-set annot-buf (fun 2) doc)
          (djvu-set shared-buf (fun 3) doc)
          (djvu-set bookmarks-buf (fun 4) doc)
          (djvu-set outline-buf (fun 5) doc)))
      ;; Of course, we have
      ;; `djvu-doc-read-buf' = `djvu-doc'
      ;; `djvu-doc-file' = `buffer-file-name'.  Bother?
      ;; It seems Emacs does not like aliases for buffer-local variables.
      (djvu-set file file doc)
      ;; We could set the resolve-url flag heuristically, if the Djvu file
      ;; happens to have bookmarks or internal urls on the current page.
      ;; (djvu-set resolve-url nil doc)

      ;; (Re-)Initialize all buffers.
      (with-current-buffer (djvu-ref read-buf doc)
        (djvu-read-mode))
      (with-current-buffer (djvu-ref outline-buf doc)
        (djvu-outline-mode))
      (with-current-buffer (djvu-ref text-buf doc)
        (djvu-script-mode)
        (setq djvu-buffer 'text))
      (with-current-buffer (djvu-ref annot-buf doc)
        (djvu-script-mode)
        (setq djvu-buffer 'annot
              header-line-format '(:eval (djvu-header-line "page annotations"))))
      (with-current-buffer (djvu-ref shared-buf doc)
        (djvu-script-mode)
        (setq djvu-buffer 'shared
              header-line-format '(:eval (djvu-header-line "shared annotations"))))
      (with-current-buffer (djvu-ref bookmarks-buf doc)
        (djvu-script-mode)
        (setq djvu-buffer 'bookmarks
              header-line-format '(:eval (djvu-header-line "bookmarks"))))
      (djvu-all-buffers doc
        (setq djvu-doc doc ; propagate DOC to all buffers
              buffer-file-name file
              ;; A non-nil value of `buffer-file-truename' enables file-locking,
              ;; see call of `lock_file' in `prepare_to_modify_buffer_1'
              buffer-file-truename file-truename
              buffer-file-number file-number
              buffer-file-read-only read-only
              ;; We assume that all buffers for a Djvu document have the same
              ;; read-only status.  Should we allow different values for the
              ;; buffers of one document?  Or do we need a `djvu-read-only-mode'?
              buffer-read-only read-only
              default-directory dir)
        (set-visited-file-modtime)
        (add-hook 'post-command-hook 'djvu-modified nil t)
        (add-hook 'kill-buffer-hook 'djvu-kill-doc-all nil t))

      (with-temp-buffer
        (djvu-djvused doc t "-e"
                      "create-shared-ant; print-ant; n; ls; print-outline;")
        (goto-char (point-min))

        ;; shared annotations
        (save-restriction
          (narrow-to-region
           (point)
           ;; There is no delimiter in between the output strings
           ;; of multiple djvused commands indicating something like
           ;; the last shared annotation.
           ;; So we simply rely on the fact that annotations have a
           ;; parsable lisp-like syntax surrounded by braces,
           ;; whereas the next djvused command is `n', the output
           ;; of which is a plain number.
           (save-excursion
             (while (progn (skip-chars-forward " \t\n")
                           (looking-at "("))
               (forward-sexp))
             (point)))
          (djvu-init-annot (djvu-ref shared-buf doc) doc t))

        ;; page max
        (djvu-set pagemax (read (current-buffer)) doc)

        ;; page id:
        ;; The output lines of djvused -e "ls;" consists of several parts
        (let ((regexp (concat "\\(?:\\([0-9]+\\)[ \t]+\\)?" ; page number
                              "\\([PIAT]\\)[ \t]+"          ; file identifier
                              "\\([0-9]+\\)[ \t]+"          ; file size
                              ;; We have a problem when parsing the
                              ;; component file name followed by the optional
                              ;; page title: there is no unambiguous separator
                              ;; in between the two.  Note that the component
                              ;; file name may contain whitespace characters
                              ;; and its file name extension is not unique
                              ;; (if present at all).
                              "\\([^=\n]+\\)"
                              "\\(?:[ \t]+T=[^\t\n]+\\)?"  ; title (optional)
                              "$")) ; match a single line
              page-id)
          (while (progn (skip-chars-forward " \t\n")
                        (looking-at regexp))
            (if (match-string 1)
                ;; page-id is an alist with elements (PAGE-NUM . FILE-ID).
                ;; The remainder of the code assumes that djvused sets up
                ;; this association list properly.
                (push (cons (djvu-match-number 1)
                            (match-string 4))
                      page-id))
            (goto-char (match-end 0)))
          (unless (eq (djvu-ref pagemax doc) (length page-id))
            (error "Page id list broken %s - %s"
                   (djvu-ref pagemax doc) (length page-id)))
          (djvu-set page-id (nreverse page-id) doc))

        ;; bookmarks
        (skip-chars-forward " \t\n")
        (when (looking-at "(bookmarks")
          (let ((object (read (current-buffer))))
            (with-current-buffer (djvu-ref bookmarks-buf doc)
              (let (buffer-read-only)
                (insert "(bookmarks")
                (djvu-insert-bookmarks (cdr object) " ")
                (insert ")\n")
                (goto-char (point-min))
                (set-buffer-modified-p nil)
                (setq buffer-undo-list nil)))
            (djvu-init-outline (cdr object) doc))))

      (djvu-init-page page doc))

    (if view (djvu-view doc))
    (unless noselect (switch-to-buffer (djvu-ref read-buf doc)))
    (djvu-ref read-buf doc)
    (djvu-restore)))

(defun djvu-init-page (&optional page doc match)
  "Initialize PAGE for Djvu DOC.
PAGE is re-initialized if we are already viewing it."
  (interactive (list (djvu-read-page)))
  (unless doc (setq doc djvu-doc))
  ;; No need to save if only the bookmarks buffer
  ;; or shared annotations buffer got modified.
  (if (or (buffer-modified-p (djvu-ref text-buf doc))
          (buffer-modified-p (djvu-ref annot-buf doc)))
      (djvu-save doc t))
  ;; We process PAGE unconditionally, even if it equals the page
  ;; currently displayed.  Most often, PAGE equals the current page
  ;; if we want to redisplay PAGE.
  (unless (integerp page)
    (setq page (or (djvu-ref page doc) 1)))
  (if (or (< page 1)
          (< (djvu-ref pagemax doc) page))
      (user-error "Page `%s' out of range" page))

  (let ((inhibit-quit t))
    (if (and (djvu-ref page doc)
             (not (equal page (djvu-ref page doc))))
        (djvu-set history-backward (cons (djvu-ref page doc)
                                         (djvu-ref history-backward doc))
                  doc))
    (djvu-set history-forward nil doc)
    (djvu-set page page doc)
    ;; Fix me: Restore buffer positions if we revisit the same page.
    (djvu-set read-pos nil doc)
    (with-temp-buffer
      (djvu-djvused doc t "-e"
                    (format "select %d; size; print-txt; print-ant;"
                            (djvu-ref page doc)))
      (goto-char (point-min))

      ;; page size
      (skip-chars-forward " \t\n")
      (if (looking-at "width=\\([[:digit:]]+\\)[ \t]+height=\\([[:digit:]]+\\)\\(?:[ \t]+rotation=\\([[:digit:]]+\\)\\)?$")
          (djvu-set pagesize (cons (djvu-match-number 1)
                                   (djvu-match-number 2))
                    doc)
        ;; This may fail if the file list we read previously contained
        ;; thumbnails.  We should really ignore these thumbnails.
        (error "No page size"))

      ;; Raw text:
      ;; This is exactly one object that we can swallow in one bite.
      ;; Hence we do this before we swallow the unknown number of annotations.
      (goto-char (match-end 0))
      (skip-chars-forward " \t\n")
      (let ((object (if (looking-at "(\\(page\\|column\\|region\\|para\\|line\\|word\\|char\\)")
                        (read (current-buffer)))))
        ;; Set up annotations buffer.
        ;; This also initializes `djvu-doc-rect-list' that we need
        ;; for propertizing the read buffer.
        (save-restriction
          (narrow-to-region (point) (point-max))
          (djvu-init-annot (djvu-ref annot-buf doc) doc))

        ;; Set up text buffer
        (djvu-init-text object doc t)

        ;; Set up read buffer
        (djvu-init-read object doc t match)))))

(defun djvu-init-read (object &optional doc reset match)
  (with-current-buffer (djvu-ref read-buf doc)
    (let ((djvu-rect-list (djvu-ref rect-list doc))
          (dpos (unless reset (djvu-read-dpos nil doc)))
          buffer-read-only djvu-last-rect)
      (erase-buffer)
      (djvu-insert-read object)
      (djvu-insert-read-prop)
      (if reset
          (goto-char (point-min))
        (djvu-goto-read dpos)))
    (set-buffer-modified-p nil)
    (setq buffer-read-only t)
    (djvu-image nil t match)))

(provide 'djvu3)

;;; djvu3.el ends here
back to top