emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/djvu 071c8ab168: Release djvu.el v1.1.2


From: Roland Winkler
Subject: [elpa] externals/djvu 071c8ab168: Release djvu.el v1.1.2
Date: Sat, 16 Apr 2022 22:54:32 -0400 (EDT)

branch: externals/djvu
commit 071c8ab168588897475899c46eaa16e70141db8c
Author: Roland Winkler <winkler@gnu.org>
Commit: Roland Winkler <winkler@gnu.org>

    Release djvu.el v1.1.2
---
 djvu.el | 1066 ++++++++++++++++++++++++++++++++++++++-------------------------
 1 file changed, 644 insertions(+), 422 deletions(-)

diff --git a/djvu.el b/djvu.el
index 99154d03f4..85ddfa9138 100644
--- a/djvu.el
+++ b/djvu.el
@@ -1,10 +1,10 @@
 ;;; djvu.el --- Edit and view Djvu files via djvused -*- lexical-binding: t -*-
 
-;; Copyright (C) 2011-2020  Free Software Foundation, Inc.
+;; Copyright (C) 2011-2022  Free Software Foundation, Inc.
 
 ;; Author: Roland Winkler <winkler@gnu.org>
 ;; Keywords: files, wp
-;; Version: 1.1.1
+;; Version: 1.1.2
 
 ;; 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
@@ -139,6 +139,18 @@
 
 ;;; News:
 
+;; v1.1.2:
+;; - Support changing the mode of a buffer visiting a Djvu document.
+;;
+;; - Support `doc-view-toggle-display' with `major-mode-suspend'.
+;;
+;; - Selecting the background color "transparent" removes the
+;;   background color attribute.
+;;
+;; - New options `djvu-image-zoom' and `djvu-ascenders-re'.
+;;
+;; - Bug fixes.
+;;
 ;; v1.1.1:
 ;; - Support text and image scrolling similar to `doc-view-mode'.
 ;;   New option `djvu-continuous'.
@@ -204,20 +216,20 @@
 
 ;;; Djvu internals (see Sec. 8.3.4.2.3.1 of djvu3spec.djvu)
 ;;
-;; Supported area attributes             rect  oval  poly  line  text
+;; Supported area attributes             rect  text  oval  line  poly
 ;; (none)/(xor)/(border c)                X     X     X     X     X
 ;; (shadow_* t)                           X
-;; (border_avis)                          X     X     X
+;; (border_avis)                          X           X           X
 ;; (hilite color) / (opacity o)           X
 ;; (arrow) / (width w) / (lineclr c)                        X
-;; (backclr c) / (textclr c) / (pushpin)                          X
+;; (backclr c) / (textclr c) / (pushpin)        X
 ;;
 ;; c = #RRGGBB   t = thickness (1..32)
 ;; o = opacity = 0..200 (yes)
 ;;
 ;; zones: page, column, region, para, line, word, and char
+;; areas: rect, text, oval, line, and poly
 
-(require 'button)
 (require 'image-mode)
 (eval-when-compile
   (require 'cl-lib))
@@ -263,8 +275,9 @@
     ("magenta" . "#FF00FF") ; 5
     ("purple"  . "#7F60FF") ; 6
     ("cyan"    . "#00FFFF") ; 7
-    ("white"   . "#FFFFFF") ; 8
-    ("black"   . "#000000")); 9
+    ("pink"    . "#FF6060") ; 8
+    ("white"   . "#FFFFFF") ; 9
+    ("black"   . "#000000")); 10
   "Alist of colors for highlighting."
   :group 'djvu
   :type '(repeat (cons (string) (string))))
@@ -362,6 +375,11 @@ Used by `djvu-region-string'."
   :group 'djvu
   :type 'boolean)
 
+(defcustom djvu-image-zoom 1.2
+  "Zoom factor for images."
+  :group 'djvu
+  :type 'number)
+
 (defcustom djvu-descenders-re "[(),;Qgjpqy]" ; some fonts also `J' and `f'
   ;; https://en.wikipedia.org/wiki/Descender
   "Regexp matching any descending characters or nil.
@@ -374,8 +392,41 @@ slightly below the baseline."
   :group 'djvu
   :type '(choice regexp (const nil)))
 
+(defcustom djvu-ascenders-re "[^-,.;:acegm-su-z\s]"
+  "Regexp matching ascending characters or nil, see `djvu-descenders-re'."
+  :group 'djvu
+  :type '(choice regexp (const nil)))
+
 ;; Internal variables
 
+(defvar djvu-coords-re
+  (format "\\(?2:%s\\)"
+          (mapconcat (lambda (i) (format "\\(?%d:-?[0-9]+\\)" i))
+                     '(3 4 5 6) "[\s\t]+"))
+  "Regexp matching the coordinates of Djvu areas and zones.
+Substring 2: coordinates, 3-6: individual coordinates.")
+
+(defvar djvu-coord-xy-re
+  (mapconcat (lambda (i) (format "\\(?%d:-?[0-9]+\\)" i))
+             '(1 2) "[\s\t]+")
+  "Regexp matching pair of xy coordinates of Djvu maparea poly.
+Substrings 1-2: individual coordinates.")
+
+(defvar djvu-area-re
+  (format "(\\(?1:%s\\)[\s\t]+%s[)\s\t\n]"
+          (regexp-opt '("rect" "oval" "text" "line" "poly"))
+          djvu-coords-re)
+  "Regexp matching a Djvu area.
+Substring 1: area type, 2: coordinates, 3-6: individual coordinates.")
+
+(defvar djvu-zone-re
+  (format "[\s\t]*(\\(?1:%s\\)[\s\t]+%s[\s\t\n]+" ; omit closing `)'
+          (regexp-opt '("page" "column" "region" "para" "line"
+                        "word" "char"))
+           djvu-coords-re)
+  "Regexp matching the beginning of a Djvu text zone.
+Substring 1: zone type, 2: coordinates, 3-6: individual coordinates.")
+
 (defvar djvu-test nil
   "If non-nil do not process / delete djvused scripts.  Useful for testing.")
 ;; (setq djvu-test t) (setq djvu-test nil)
@@ -408,6 +459,31 @@ Bind this with `let' to select one of these schemes.")
 
 (defvar djvu-image-mode) ; fully defined by `define-minor-mode' (buffer-local)
 
+(defvar djvu-init nil
+  "Non-nil during initialization of Djview mode.")
+
+(defvar djvu-color-attributes '(border hilite lineclr backclr textclr)
+  "List of color attributes known to Djvu.  See djvused(1).")
+
+(defvar djvu-color-re
+  (concat "(" (regexp-opt (mapcar #'symbol-name djvu-color-attributes) t)
+          "[ \t\n]+\\(%s\\(%s[[:xdigit:]][[:xdigit:]]"
+          "[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\\)%s\\)[ \t\n]*)")
+  "Format string to create a regular expression matching color attributes.")
+
+(defvar djvu-beg-object-re
+  (concat "^[\s\t]*(" (regexp-opt '("background" "zoom" "mode" "align"
+                                    "maparea" "metadata" "xmp" "bookmarks")
+                                  t)
+          "\\>")
+  "Regexp matching the beginning of a Djvu object.  See djvused(1).")
+
+(defvar djvu-last-search-re nil
+  "Last regexp used by `djvu-re-search-forward'.")
+
+(defvar djvu-modified nil
+  "Let-bound in `djvu-mouse-drag-track-area'.")
+
 ;; See `ediff-defvar-local'
 (defmacro djvu-defvar-local (var &optional val doc)
   "Define VAR as a permanent-local variable, and return VAR."
@@ -487,6 +563,12 @@ Used in `djvu-image-mode' when we cannot go to this 
position.")
   "Image of current page of a Djvu document.
 This is a list (PAGE-NUM MAGNIFICATION IMAGE).")
 
+(defvar-local djvu-doc-image-hscroll 0
+  "Number of columns by which a page image is scrolled from left margin.")
+
+(defvar-local djvu-doc-image-vscroll 0
+  "Amount by which a page image is scrolled vertically.")
+
 ;;; Helper functions and macros
 
 ;; For each Djvu document we have six buffers associated with this document
@@ -639,12 +721,10 @@ Preserve FILE if `djvu-test' is non-nil."
 (defun djvu-scroll-up-command (&optional arg)
   "Scroll text upward ARG lines; or near full screen if no ARG.
 At the bottom of the page, when `djvu-continuous' is non-nil
-or prefix ARG is nil, go to the next page.
-Prefix ARG may take the same values as arg ARG of `scroll-up-command'.
-For historical reasons, this includes the range of values
-of `current-prefix-arg'."
+go to the next page.
+Prefix ARG may take the same values as arg ARG of `scroll-up-command'."
   (interactive "^P") ; same as `scroll-up-command'
-  (if (and (or djvu-continuous (not arg))
+  (if (and djvu-continuous
            (= (window-end) (point-max))
            (< (djvu-ref page) (djvu-ref pagemax)))
       (djvu-next-page 1)
@@ -655,12 +735,10 @@ of `current-prefix-arg'."
 (defun djvu-scroll-down-command (&optional arg)
   "Scroll text downward ARG lines; or near full screen if no ARG.
 At the top of the page, when `djvu-continuous' is non-nil
-or prefix ARG is nil, go to the previous page.
-Prefix ARG may take the same values as arg ARG of `scroll-down-command'.
-For historical reasons, this includes the range of values
-of `current-prefix-arg'."
+go to the previous page.
+Prefix ARG may take the same values as arg ARG of `scroll-down-command'."
   (interactive "^P") ; same as `scroll-down-command'
-  (if (and (or djvu-continuous (not arg))
+  (if (and djvu-continuous
            (= (point-min) (window-start))
            (< 1 (djvu-ref page)))
       (progn
@@ -725,11 +803,6 @@ go to the previous page."
     (djvu-goto-page (car history-forward))
     (djvu-set history-forward (cdr history-forward))))
 
-(defun djvu-set-color-highlight (color)
-  "Set color for highlighting based on `djvu-color-alist'."
-  (interactive (list (completing-read "Color: " djvu-color-alist nil t)))
-  (setq djvu-color-highlight color))
-
 (defun djvu-kill-view (&optional doc all)
   "Kill most recent Djview process for DOC.
 If ALL is non-nil, kill all Djview processes."
@@ -751,7 +824,7 @@ This relies on `djvu-kill-doc-all' for doing the real work."
   (interactive)
   ;; `djvu-kill-doc-all' will try to save our work and kill all djview
   ;; processes.
-  (mapc 'kill-buffer (djvu-buffers doc)))
+  (mapc #'kill-buffer (djvu-buffers doc)))
 
 (defvar djvu-in-kill-doc nil
   "Non-nil if we are running `djvu-kill-doc-all'.")
@@ -769,14 +842,30 @@ so that killing the current buffer kills all buffers 
visiting `djvu-doc'."
       (condition-case nil
           (let ((doc djvu-doc))
             (setq buffers (djvu-buffers doc))
-            (unless (memq nil (mapcar 'buffer-live-p buffers))
+            (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)))))
+      (mapc #'kill-buffer (delq (current-buffer) buffers)))))
+
+(defun djvu-change-major-mode ()
+  "Clean up Djvu mode buffers and hooks.
+Djvu mode puts this into `change-major-mode-hook'."
+  (unless djvu-init
+    (djvu-kill-doc-all)
+    ;; These local variables are permanent local
+    (kill-local-variable 'kill-buffer-hook)
+    (kill-local-variable 'djvu-doc)
+    (kill-local-variable 'revert-buffer-function)
+    (kill-local-variable 'write-file-functions)
+    (let ((inhibit-read-only t)
+          (buffer-undo-list t))
+      (insert-file-contents-literally buffer-file-name t nil nil t))
+    (setq buffer-undo-list nil
+          buffer-read-only (not (file-writable-p buffer-file-name)))))
 
 (defun djvu-save (&optional doc query)
   "Save Djvu DOC."
@@ -802,7 +891,8 @@ so that killing the current buffer kills all buffers 
visiting `djvu-doc'."
       (if (and annot-modified (not text-modified))
           (djvu-init-read (djvu-read-text doc) doc))
       (djvu-all-buffers doc
-        (set-buffer-modified-p nil)))))
+        (set-buffer-modified-p nil))))
+  t) ; for `write-file-function'
 
 (defun djvu-modified ()
   "Mark Djvu Read and Outline buffers as modified if necessary.
@@ -891,15 +981,6 @@ the purpose of calling djvused is to update the Djvu file."
               (djvu-all-buffers doc
                 (setq buffer-file-number file-number)))))))))
 
-(defvar djvu-color-attributes '(border hilite lineclr backclr textclr)
-  "List of color attributes known to Djvu.")
-
-(defvar djvu-color-re
-  (concat "(" (regexp-opt (mapcar 'symbol-name djvu-color-attributes) t)
-          "[ \t\n]+\\(%s\\(%s[[:xdigit:]][[:xdigit:]]"
-          "[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\\)%s\\)[ \t\n]*)")
-  "Format string to create a regular expression matching color attributes.")
-
 ;; The Emacs lisp reader gets confused by the Djvu color syntax with
 ;; symbols '#000000.  So we temporarily convert these symbols to strings.
 (defun djvu-convert-hash (&optional reverse)
@@ -961,17 +1042,32 @@ If INITIAL-INPUT is non-nil use string from REGION as 
initial input."
 (defun djvu-interactive-color (color)
   "Return color specification for use in interactive calls.
 The color is the Nth element of `djvu-color-alist'.
-Here N is `current-prefix-arg' if this is a number.
+Here N is `current-prefix-arg' if this is a non-negative number.
 N is 1 - `current-prefix-arg' / 4 if the prefix is a cons,
 that is, `C-u' yields N = 0.
-Arg COLOR defines the default when there is no prefix arg."
+Arg COLOR defines the default when there is no prefix arg.
+Return nil if `current-prefix-arg' is a negative number.
+See also `djvu-interactive-color-read'."
   (let ((colnum (or (and (consp current-prefix-arg)
                          (1- (/ (car current-prefix-arg) 4)))
                     (and (integerp current-prefix-arg)
                          current-prefix-arg))))
-    (if (and colnum (>= colnum (length djvu-color-alist)))
-        (user-error "Color undefined"))
-    (if colnum (car (nth colnum djvu-color-alist)) color)))
+    (cond ((not colnum) color) ; use default
+          ((>= colnum (length djvu-color-alist))
+           (user-error "Color undefined"))
+          ((<= 0 colnum)
+           (car (nth colnum djvu-color-alist))))))
+
+(defun djvu-interactive-color-read ()
+  "Read color interactively.
+The return value is the car of an element of `djvu-color-alist'
+or nil if the user selects \"transparent\".
+See also `djvu-interactive-color'."
+  (let ((color (completing-read "New Color: "
+                                (cons '("transparent") djvu-color-alist )
+                                nil t)))
+     (unless (string= color "transparent")
+       color)))
 
 (defun djvu-page-url (&optional page dir doc)
   "For Djvu DOC return the internal url for PAGE.
@@ -981,9 +1077,9 @@ This is the inverse of `djvu-url-page'."
                       (cdr (assq page (djvu-ref page-id doc)))
                     page))))
 
-(defun djvu-interactive-url (&optional color)
+(defun djvu-interactive-url (color)
   "Return URL specification for use in interactive calls."
-  (let ((fmt (format "(%s) URL: " (or color djvu-color-url)))
+  (let ((fmt (format "(%s) URL: " (or color "no color")))
         val)
     (while (not val)
       (setq val (read-string fmt))
@@ -1007,28 +1103,29 @@ Return the new rgb color string.
 If BACKGROUND is nil, use `djvu-color-background'.
 If OPACITY is nil, use `djvu-opacity'.
 If INVERT is non-nil apply inverse transformation."
-  (let* ((color (if (string-match "\\`#" color) color
-                  (cdr (assoc color djvu-color-alist))))
-         (background (if (and background (string-match "\\`#" background))
-                         background
-                       (cdr (assoc (or background djvu-color-background)
-                                   djvu-color-alist))))
-         (a (/ (float (or opacity djvu-opacity)) 200)) ; foreground
-         (b (- 1 a))) ; background
-    (if invert
+  (when color
+    (let* ((color (if (string-match "\\`#" color) color
+                    (cdr (assoc color djvu-color-alist))))
+           (background (if (and background (string-match "\\`#" background))
+                           background
+                         (cdr (assoc (or background djvu-color-background)
+                                     djvu-color-alist))))
+           (a (/ (float (or opacity djvu-opacity)) 200)) ; foreground
+           (b (- 1 a))) ; background
+      (if invert
+          (cl-flet ((mix (beg end)
+                         (max 0 (min #xFF
+                                     (round (/ (- (djvu-substring-number color 
beg end 16)
+                                                  (* b (djvu-substring-number 
background beg end 16)))
+                                               a))))))
+            (format "#%02X%02X%02X"
+                    (mix 1 3) (mix 3 5) (mix 5 7)))
         (cl-flet ((mix (beg end)
                        (max 0 (min #xFF
-                       (round (/ (- (djvu-substring-number color beg end 16)
-                                    (* b (djvu-substring-number background beg 
end 16)))
-                                 a))))))
+                                   (round (+ (* a (djvu-substring-number color 
beg end 16))
+                                             (* b (djvu-substring-number 
background beg end 16))))))))
           (format "#%02X%02X%02X"
-                  (mix 1 3) (mix 3 5) (mix 5 7)))
-      (cl-flet ((mix (beg end)
-                     (max 0 (min #xFF
-                     (round (+ (* a (djvu-substring-number color beg end 16))
-                               (* b (djvu-substring-number background beg end 
16))))))))
-        (format "#%02X%02X%02X"
-                (mix 1 3) (mix 3 5) (mix 5 7))))))
+                  (mix 1 3) (mix 3 5) (mix 5 7)))))))
 
 ;;; Djvu modes
 
@@ -1146,27 +1243,29 @@ This is a child of `special-mode-map'.")
 
 (define-derived-mode djvu-read-mode special-mode "Djview"
   "Mode for reading Djvu files."
-  ;; The Read buffer is not editable.  So do not create auto-save files.
-  (setq buffer-auto-save-file-name nil ; permanent buffer-local
-        djvu-buffer 'read
-        buffer-undo-list t)
-  (let ((fmt (concat (car (propertized-buffer-identification "%s"))
-                     "  p%d/%d")))
-    (setq mode-line-buffer-identification
-          `(24 (:eval (format ,fmt (buffer-name) (djvu-ref page)
-                              (djvu-ref pagemax))))))
-  (setq-local revert-buffer-function #'djvu-revert-buffer)
-  (setq-local bookmark-make-record-function #'djvu-bookmark-make-record)
-  (if (boundp 'mwheel-scroll-up-function) ; not --without-x build
-      (setq-local mwheel-scroll-up-function
-                  (lambda (&optional n)
-                    (if djvu-image-mode (djvu-image-scroll-up n)
-                      (djvu-scroll-up-command n)))))
-  (if (boundp 'mwheel-scroll-down-function)
-      (setq-local mwheel-scroll-down-function
-                  (lambda (&optional n)
-                    (if djvu-image-mode (djvu-image-scroll-down n)
-                      (djvu-scroll-down-command n))))))
+  (if (not djvu-init)
+      (djvu-init-mode) ; For `doc-view-toggle-display' and `major-mode-suspend'
+    ;; The Read buffer is not editable.  So do not create auto-save files.
+    (setq buffer-auto-save-file-name nil ; permanent buffer-local
+          djvu-buffer 'read
+          buffer-undo-list t)
+    (let ((fmt (concat (car (propertized-buffer-identification "%s"))
+                       "  p%d/%d")))
+      (setq mode-line-buffer-identification
+            `(24 (:eval (format ,fmt (buffer-name) (djvu-ref page)
+                                (djvu-ref pagemax))))))
+    (setq-local revert-buffer-function #'djvu-revert-buffer)
+    (setq-local bookmark-make-record-function #'djvu-bookmark-make-record)
+    (if (boundp 'mwheel-scroll-up-function) ; not --without-x build
+        (setq-local mwheel-scroll-up-function
+                    (lambda (&optional n)
+                      (if djvu-image-mode (djvu-image-scroll-up n)
+                        (djvu-scroll-up-command n)))))
+    (if (boundp 'mwheel-scroll-down-function)
+        (setq-local mwheel-scroll-down-function
+                    (lambda (&optional n)
+                      (if djvu-image-mode (djvu-image-scroll-down n)
+                        (djvu-scroll-down-command n)))))))
 
 (defvar djvu-script-mode-map
   (let ((km (make-sparse-keymap)))
@@ -1192,6 +1291,7 @@ This is a child of `special-mode-map'.")
     (define-key km "\C-c\C-q"    'djvu-quit-window)
     (define-key km "\C-c\C-k"    'djvu-kill-doc)
     (define-key km (kbd "C-c C-S-g") 'djvu-revert-buffer) ; [?\C-c ?\C-\S-g]
+
     km)
   "Keymap for Djvu Script Mode.
 This is a child of `lisp-mode-map'.")
@@ -1238,7 +1338,9 @@ This is a child of `lisp-mode-map'.")
                             "arrow" "width" "lineclr"
                             "backclr" "textclr" "pushpin"
                             "page" "column" "region" "para" "line"
-                            "word" "char") t) ")")
+                            "word" "char")
+                          t)
+              ")")
      1 font-lock-function-name-face)
     ;; url
     (djvu-font-lock-url))
@@ -1247,21 +1349,23 @@ This is a child of `lisp-mode-map'.")
 (define-derived-mode djvu-script-mode lisp-mode "Djvu Script"
   "Mode for editing Djvu scripts.
 The annotations, shared annotations and bookmark buffers use this mode."
-  ;; Fixme: we should create auto-save files for the script buffers.
-  ;; This requires suitable names for the auto-save files that should
-  ;; be derived from `buffer-file-name'.
-  (setq buffer-auto-save-file-name nil ; permanent buffer-local
-        fill-column djvu-fill-column
-        font-lock-defaults '(djvu-font-lock-keywords))
-  (let* ((fmt1 (car (propertized-buffer-identification "%s")))
-         (fmt2 (concat fmt1 "  p%d/%d")))
-    (setq mode-line-buffer-identification
-          `(24 (:eval (if djvu-doc
-                          (format ,fmt2 (buffer-name) (djvu-ref page)
-                                  (djvu-ref pagemax))
-                        (format ,fmt1 (buffer-name)))))))
-  (setq-local revert-buffer-function #'djvu-revert-buffer)
-  (setq-local bookmark-make-record-function #'djvu-bookmark-make-record))
+  (if (not djvu-init)
+      (djvu-init-mode) ; For `doc-view-toggle-display' and `major-mode-suspend'
+    ;; Fixme: we should create auto-save files for the script buffers.
+    ;; This requires suitable names for the auto-save files that should
+    ;; be derived from `buffer-file-name'.
+    (setq buffer-auto-save-file-name nil ; permanent buffer-local
+          fill-column djvu-fill-column
+          font-lock-defaults '(djvu-font-lock-keywords))
+    (let* ((fmt1 (car (propertized-buffer-identification "%s")))
+           (fmt2 (concat fmt1 "  p%d/%d")))
+      (setq mode-line-buffer-identification
+            `(24 (:eval (if djvu-doc
+                            (format ,fmt2 (buffer-name) (djvu-ref page)
+                                    (djvu-ref pagemax))
+                          (format ,fmt1 (buffer-name)))))))
+    (setq-local revert-buffer-function #'djvu-revert-buffer)
+    (setq-local bookmark-make-record-function #'djvu-bookmark-make-record)))
 
 (defvar djvu-outline-mode-map
   (let ((km (make-sparse-keymap)))
@@ -1322,31 +1426,34 @@ This is a child of `special-mode-map'.")
 
 (define-derived-mode djvu-outline-mode special-mode "Djvu OL"
   "Mode for reading the outline of Djvu files."
-  ;; The Outline buffer is not editable.  So do not create auto-save files.
-  (setq buffer-auto-save-file-name nil ; permanent buffer-local
-        djvu-buffer 'outline
-        buffer-undo-list t)
-  (let ((fmt (concat (car (propertized-buffer-identification "%s"))
-                     "  p%d/%d")))
-    (setq mode-line-buffer-identification
-          `(24 (:eval (format ,fmt (buffer-name) (djvu-ref page)
-                              (djvu-ref pagemax))))))
-  (setq-local revert-buffer-function #'djvu-revert-buffer)
-  (setq-local bookmark-make-record-function #'djvu-bookmark-make-record))
+  (if (not djvu-init)
+      (djvu-init-mode) ; For `doc-view-toggle-display' and `major-mode-suspend'
+    ;; The Outline buffer is not editable.  So do not create auto-save files.
+    (setq buffer-auto-save-file-name nil ; permanent buffer-local
+          djvu-buffer 'outline
+          buffer-undo-list t)
+    (let ((fmt (concat (car (propertized-buffer-identification "%s"))
+                       "  p%d/%d")))
+      (setq mode-line-buffer-identification
+            `(24 (:eval (format ,fmt (buffer-name) (djvu-ref page)
+                                (djvu-ref pagemax))))))
+    (setq-local revert-buffer-function #'djvu-revert-buffer)
+    (setq-local bookmark-make-record-function #'djvu-bookmark-make-record)))
 
 ;;; General Setup
 
 ;;;###autoload
-(add-to-list 'auto-mode-alist '("\\.djvu\\'" . djvu-dummy-mode))
+(add-to-list 'auto-mode-alist '("\\.djvu\\'" . djvu-init-mode))
 
 ;;;###autoload
-(defun djvu-dummy-mode ()
-  "Djvu dummy mode for `auto-mode-alist'."
+(defun djvu-init-mode ()
+  "Dummy mode for initializing Djvu mode.
+This can be used as an element for `auto-mode-alist'.
+This can also be used if the current buffer visits a Djvu file
+using some other mode."
+  (interactive)
   (djvu-find-file buffer-file-name nil nil t))
 
-;; FIXME: Add entry for `change-major-mode-hook'.
-;; How should this handle the plethora of buffers per djvu document?
-
 (defun djvu-read-file-name ()
   "Read file name of Djvu file.
 The numeric value of `current-prefix-arg' is the page number."
@@ -1371,26 +1478,33 @@ from 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))
+      (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))
+  (with-temp-buffer
+    (insert-file-contents-literally file nil 0 4)
+    (goto-char (point-min))
+    (unless (looking-at "\\`AT&T") ; magic number for Djvu documents
+      (user-error "`%s' not a Djvu document" 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)))))
+         (doc (if (equal buffer-file-truename file-truename)
+                  (current-buffer)
+                (find-buffer-visiting file-truename)))
+         (old-bufs (and doc (buffer-local-value 'djvu-doc doc)
+                        (mapcar #'buffer-live-p (djvu-buffers doc))))
+         (djvu-init t))
     ;; 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)
+      (with-current-buffer doc
+        (djvu-kill-doc-all))
+      (setq old-bufs nil)
       (message "Killing dangling Djvu buffers...Done")
       (sit-for 2))
     ;; Do nothing if we are already visiting FILE such that all buffers
@@ -1420,18 +1534,22 @@ from file."
                          (concat buf-basename
                                  (nth n djvu-buffer-name-extensions))
                          dir))))
-          (if old-buf
+          (if doc
               ;; 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
+              ;; buffer.  This applies also if we switch from some other
+              ;; mode to Djview mode.
+              (with-current-buffer doc
                 (let ((inhibit-read-only t)
                       (buffer-undo-list t))
                   (erase-buffer))
                 (setq buffer-file-coding-system 'prefer-utf-8)
-                (setq doc old-buf))
+                ;; `rename-buffer' obeys uniquify.
+                (rename-buffer (concat buf-basename
+                                       (nth 0 djvu-buffer-name-extensions))
+                               t))
+            ;; We need this when mimicking `find-file'
+            ;; so that FILE does not yet have a buffer.
             (setq doc (fun 0)))
           (djvu-set read-buf doc doc)
           (djvu-set text-buf (fun 1) doc)
@@ -1469,21 +1587,26 @@ from file."
         (setq djvu-buffer 'bookmarks
               header-line-format '(:eval (djvu-header-line "bookmarks"))))
       (djvu-all-buffers doc
+        ;; permanent buffer-local variables
         (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
+              default-directory dir)
+        ;; other buffer-local stuff
+        (setq 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)
+              ;; buffers of one document?
+              ;; Or do we need a `djvu-read-only-mode'?
+              buffer-read-only read-only)
+        (setq-local write-file-functions #'djvu-save)
         (set-visited-file-modtime)
-        (add-hook 'post-command-hook 'djvu-modified nil t)
-        (add-hook 'kill-buffer-hook 'djvu-kill-doc-all nil t))
+        (add-hook 'post-command-hook #'djvu-modified nil t)
+        (add-hook 'kill-buffer-hook #'djvu-kill-doc-all nil t)
+        (add-hook 'change-major-mode-hook #'djvu-change-major-mode nil t))
 
       (with-temp-buffer
         (djvu-djvused doc t "-e"
@@ -1548,6 +1671,7 @@ from file."
           (let ((object (read (current-buffer))))
             (with-current-buffer (djvu-ref bookmarks-buf doc)
               (let (buffer-read-only)
+                (erase-buffer)
                 (insert "(bookmarks")
                 (djvu-insert-bookmarks (cdr object) " ")
                 (insert ")\n")
@@ -1685,7 +1809,7 @@ into an undefined state."
                   (if page-id
                       (format "#%d" (car page-id))
                     (djvu-unresolve-url url))))
-               (t url))) ; some other URL
+               (t "#1"))) ; some other URL (possibly empty string)
         (t ; check whether URL can be resolved
          (cond ((string-match "\\`#[0-9]+\\'" url)
                 (if (assq (djvu-substring-number url 1)
@@ -1821,15 +1945,10 @@ If prefix NEW is non-nil, always create a new Djview 
process."
 
 ;;; Djvu Text mode
 
-(defvar djvu-last-search-re nil
-  "Last regexp used by `djvu-re-search-forward'.")
-
 (defun djvu-re-search-forward (regexp)
   "Search forward for match for REGEXP.
-
 Search case-sensitivity is determined by the value of the variable
 `case-fold-search', which see.
-
 The command `djvu-re-search-forward-continue' continues to search forward."
   (interactive "sSearch (regexp): ")
   (setq djvu-last-search-re regexp)
@@ -1996,7 +2115,7 @@ This command operates on the text buffer."
       (narrow-to-region beg end)
       (mapc (lambda (zone)
               (goto-char (point-min))
-              (let ((re (format ")[\n\t\s]+(%s -?[0-9]+ -?[0-9]+ -?[0-9]+ 
-?[0-9]+" zone)))
+              (let ((re (format ")[\n\t\s]+(%s %s" zone djvu-coords-re)))
                 (while (re-search-forward re nil t)
                   (replace-match ""))))
             '("column" "region" "para" "line"))
@@ -2025,9 +2144,10 @@ This command operates on the text buffer."
   "Insert OBJECT into Djvu text buffer recursively using indentation INDENT."
   (when object
     (insert indent "("
-            (mapconcat 'prin1-to-string
+            (mapconcat #'prin1-to-string
                        (list (nth 0 object) (nth 1 object) (nth 2 object)
-                             (nth 3 object) (nth 4 object)) " "))
+                             (nth 3 object) (nth 4 object))
+                       " "))
     (let ((tail (nthcdr 5 object))
           (indent (concat indent " ")))
       (if (stringp (car tail))
@@ -2037,14 +2157,6 @@ This command operates on the text buffer."
           (djvu-insert-text elt indent))
         (insert ")")))))
 
-(defvar djvu-zone-re
-  (concat "[ \t]*(\\("
-          (regexp-opt '("page" "column" "region" "para" "line"
-                        "word" "char"))
-          "\\)[ \t]+\\(\\(-?[0-9]+\\)[ \t]+\\(-?[0-9]+\\)"
-             "[ \t]+\\(-?[0-9]+\\)[ \t]+\\(-?[0-9]+\\)\\)[ \t\n]+")
-  "Regexp matching the beginning of a Djvu text zone.")
-
 (defun djvu-text-dpos (&optional point doc)
   "Return Djvu position of POINT in Djvu text buffer."
   (with-current-buffer (djvu-ref text-buf doc)
@@ -2056,7 +2168,7 @@ This command operates on the text buffer."
                         (bobp)))
           (forward-line -1))
         (if zone
-            (mapcar 'djvu-match-number '(3 4 5 6)))))))
+            (mapcar #'djvu-match-number '(3 4 5 6)))))))
 
 (defun djvu-read-text (&optional doc)
   "Read text of a Djvu document from text buffer."
@@ -2109,28 +2221,44 @@ If SCRIPT is non-nil, dump the text buffer into the 
djvused script file SCRIPT."
           (write-region nil nil script t 0))))) ; append to SCRIPT
 
 (defun djvu-text-zone (object depth zones)
-  "Evaluate zones for text OBJECT recursively."
-  (cond ((stringp (nth 5 object))
-         (aset zones depth (vector (nth 1 object) (nth 2 object)
-                                   (nth 3 object) (nth 4 object))))
-        (object
-         (let ((depth1 (1+ depth))
-               zone)
-           (aset zones depth nil)
-           (dolist (elt (nthcdr 5 object))
-             (djvu-text-zone elt depth1 zones)
-             (if (setq zone (aref zones depth))
-                 (let ((zone1 (aref zones depth1)))
-                   (aset zone 0 (min (aref zone 0) (aref zone1 0)))
-                   (aset zone 1 (min (aref zone 1) (aref zone1 1)))
-                   (aset zone 2 (max (aref zone 2) (aref zone1 2)))
-                   (aset zone 3 (max (aref zone 3) (aref zone1 3))))
-               (aset zones depth (copy-sequence (aref zones depth1)))))
-           (if (setq zone (aref zones depth))
-               (setcdr object (apply 'list (aref zone 0) (aref zone 1)
-                                     (aref zone 2) (aref zone 3)
-                                     (nthcdr 5 object)))
-             (error "No zone??"))))))
+  "Evaluate ZONES for text OBJECT recursively.
+This rearranges the tail of OBJECT destructively.
+Branches of OBJECT that point to empty strings are removed."
+  (if (stringp (nth 5 object))
+      ;; We set ZONES only if we have something nontrivial
+      (cond ((not (equal "" (nth 5 object)))
+             (aset zones depth (vector (nth 1 object) (nth 2 object)
+                                       (nth 3 object) (nth 4 object))))
+            ((zerop depth)
+             (setcdr object (list 0 0 0 0 ""))))
+    (let ((depth1 (1+ depth))
+          zone remove)
+      (aset zones depth nil)
+      (dolist (elt (nthcdr 5 object))
+        (aset zones depth1 nil)
+        (djvu-text-zone elt depth1 zones)
+        (let ((zone1 (aref zones depth1)))
+          (cond ((not zone1)
+                 ;; ELT has no ZONE1 because it points to an empty string.
+                 (push elt remove))
+                ((setq zone (aref zones depth))
+                 (aset zone 0 (min (aref zone 0) (aref zone1 0)))
+                 (aset zone 1 (min (aref zone 1) (aref zone1 1)))
+                 (aset zone 2 (max (aref zone 2) (aref zone1 2)))
+                 (aset zone 3 (max (aref zone 3) (aref zone1 3))))
+                (t (aset zones depth zone1)))))
+      (if remove
+          (let ((tail (nthcdr 4 object)))
+            (dolist (elt remove)
+              ;; `delq' removes all occurences of ELT from TAIL.
+              (setcdr tail (delq elt (cdr tail))))))
+      (cond ((setq zone (aref zones depth))
+             (setcdr object (append (mapcar #'identity zone)
+                                    (nthcdr 5 object))))
+            ((zerop depth)
+             (setcdr object (list 0 0 0 0 "")))
+            (t
+             (setcdr object nil))))))
 
 (defun djvu-script-buffer (buffer)
   "Return buffer for djvu script.
@@ -2155,6 +2283,7 @@ You get what you want."
     ;; Put this in a separate buffer!
     (with-current-buffer buffer
       (let ((buffer-undo-list t)
+            (djvu-init t)
             buffer-read-only)
         (djvu-script-mode)
         (erase-buffer)
@@ -2210,7 +2339,7 @@ BUFFER defaults to `djvu-script-buffer'.  If BUFFER is t, 
use current buffer."
         (djvu-goto-read dpos)))
     (set-buffer-modified-p nil)
     (setq buffer-read-only t)
-    (djvu-image)))
+    (if djvu-image-mode (djvu-image))))
 
 (defun djvu-insert-read (object)
   "Display text OBJECT recursively."
@@ -2260,7 +2389,11 @@ BUFFER defaults to `djvu-script-buffer'.  If BUFFER is 
t, use current buffer."
   (when djvu-last-rect
     (let ((beg (nth 0 djvu-last-rect))
           (end (nth 1 djvu-last-rect))
-          (face `(face (:background ,(nth 5 djvu-last-rect))
+          (face `(face (:background ,(nth 5 djvu-last-rect)
+                        ;; `make-button' puts a `button' overlay
+                        ;; that overrides :foreground.
+                        :foreground ,(readable-foreground-color
+                                      (nth 5 djvu-last-rect)))
                        help-echo ,(nth 4 djvu-last-rect))))
       (if (or (eq t djvu-read-prop-newline)
               (and (numberp djvu-read-prop-newline)
@@ -2335,7 +2468,7 @@ Otherwise, do nothing and return nil."
          (goto-char (point-min))
          (or (re-search-forward (format "\\<%s\\>[ \t\n]+%s\\([ \t\n]+\"\\)?"
                                         object
-                                        (mapconcat 'number-to-string dpos
+                                        (mapconcat #'number-to-string dpos
                                                    "[ \t\n]+"))
                                 nil t)
              ;; try again, using the mean value of DPOS
@@ -2348,7 +2481,7 @@ Otherwise, do nothing and return nil."
          ;; The latter always succeeds.
          (let* ((re (format "\\<%s\\>[ \t\n]+%s\\([ \t\n]+\"\\)?"
                             object
-                            (mapconcat 'identity
+                            (mapconcat #'identity
                                        (make-list 4 "\\([[:digit:]]+\\)")
                                        "[ \t\n]+")))
                 (x (nth 0 dpos)) (y (nth 1 dpos))
@@ -2427,10 +2560,6 @@ Return corresponding buffer position."
 
 ;;; Djvu Annotation mode
 
-(defvar djvu-annot-re
-  (concat "(" (regexp-opt '("background" "zoom" "mode" "align"
-                            "maparea" "metadata" "xmp") t) "\\>"))
-
 (defun djvu-init-annot (buf doc &optional shared)
   "Initialize Annotations buffer BUF of Djvu document DOC.
 SHARED should be non-nil for a Shared Annotations buffer."
@@ -2438,17 +2567,18 @@ SHARED should be non-nil for a Shared Annotations 
buffer."
   (goto-char (point-min))
   (let (object alist)
     (while (progn (skip-chars-forward " \t\n") (not (eobp)))
-      (if (looking-at djvu-annot-re)
+      (beginning-of-line)
+      (if (looking-at djvu-beg-object-re)
           (push (read (current-buffer)) object)
         (error "Unknown annotation `%s'" (buffer-substring-no-properties
                                           (point) (line-end-position)))))
 
-    ;; To simplify the editing of annotations, identify rect mapareas
-    ;; sharing the same text string.
     (dolist (elt object)
       (if (not (eq 'maparea (car elt)))
           (push elt alist)
-        (cond ((memq (car (nth 3 elt)) '(rect oval)) ; rect and oval
+        ;; To simplify the editing of annotations, identify rect mapareas
+        ;; sharing the same text string.
+        (cond ((eq 'rect (car (nth 3 elt))) ; rect
                (let ((area (djvu-area (nth 3 elt)))
                      e)
                  ;; Remove area destructively.
@@ -2461,7 +2591,7 @@ SHARED should be non-nil for a Shared Annotations buffer."
                          (not (setq e (assoc elt alist))))
                      (push (cons elt (list area)) alist)
                    (setcdr e (cons area (cdr e))))))
-              ((eq 'text (car (nth 3 elt))) ; text
+              ((memq (car (nth 3 elt)) '(text oval)) ; mapareas text, oval
                (setcar (nthcdr 3 elt) (djvu-area (nth 3 elt)))
                (push elt alist))
               (t (push elt alist)))))
@@ -2484,8 +2614,8 @@ SHARED should be non-nil for a Shared Annotations buffer."
                  (let ((c (car elt)))
                    (insert (format "(maparea %S\n %S\n ("
                                    (djvu-resolve-url (nth 1 c) doc) (nth 2 c))
-                           (mapconcat 'prin1-to-string (cdr elt) "\n  ") ")\n 
" ; rect and oval
-                           (mapconcat 'prin1-to-string (nthcdr 3 c) " ") ; rest
+                           (mapconcat #'prin1-to-string (cdr elt) "\n  ") ")\n 
" ; rect and oval
+                           (mapconcat #'prin1-to-string (nthcdr 3 c) " ") ; 
rest
                            ")")))
                 ((eq 'metadata (car elt)) ; metadata
                  (insert "(metadata")
@@ -2494,9 +2624,9 @@ SHARED should be non-nil for a Shared Annotations buffer."
                  (insert ")"))
                 ((not (eq 'maparea (car elt))) ; no maparea
                  (prin1 elt))
-                ((memq (car (nth 3 elt)) '(text line)) ; maparea text, line
+                ((memq (car (nth 3 elt)) '(text oval line poly)) ; maparea 
text, oval, line, poly
                  (insert (format "(maparea %S\n %S\n " (nth 1 elt) (nth 2 elt))
-                         (mapconcat 'prin1-to-string (nthcdr 3 elt) " ") ; rest
+                         (mapconcat #'prin1-to-string (nthcdr 3 elt) " ") ; 
rest
                          ")"))
                 (t (error "Djvu maparea %s undefined" (car (nth 3 elt)))))
           (insert "\n\n"))
@@ -2593,8 +2723,8 @@ If URL is an internal url, go to that page."
   (let ((dpos (djvu-mean-dpos (djvu-read-dpos)))
         (pagesize (djvu-ref pagesize))
         (color (djvu-interactive-color djvu-color-highlight)))
-    (list nil (read-string (format "(%s) Text: " color)
-                          nil nil nil djvu-inherit-input-method)
+    (list nil (read-string (format "(%s) Text: " (or color "no color"))
+                           nil nil nil djvu-inherit-input-method)
           (list (nth 0 dpos) (nth 1 dpos)
                 (+ (nth 0 dpos) (/ (car pagesize) 2))
                 (+ (nth 1 dpos) (/ (cdr pagesize) 30)))
@@ -2634,8 +2764,8 @@ is usually easier to use."
                     (or url "") (if comment (djvu-fill comment) ""))
             (apply 'format "(text %d %d %d %d)" area)
             (format " (%s)" (or border 'none))
-            (djvu-insert-color "backclr" backclr)
-            (djvu-insert-color "textclr" textclr)
+            (if backclr (djvu-insert-color "backclr" backclr) "")
+            (if textclr (djvu-insert-color "textclr" textclr) "")
             (if pushpin " (pushpin)" "")
             ")\n\n")
     (undo-boundary)))
@@ -2733,7 +2863,8 @@ With prefix LEFT mark left of beginning of line."
      (let* ((color (djvu-interactive-color djvu-color-url))
             (url (djvu-interactive-url color))
             (comment (djvu-read-string
-                      (format "(%s, %s) Annotation: " url color)
+                      (format "(%s, %s) Annotation: "
+                              url (or color "no color"))
                       region)))
        (list (car region) (cdr region) url comment color djvu-opacity 'xor))))
   (djvu-rect-region beg end url comment color opacity border))
@@ -2743,7 +2874,8 @@ With prefix LEFT mark left of beginning of line."
   (interactive
    (djvu-with-region region
      (let* ((color (djvu-interactive-color djvu-color-highlight))
-            (comment (djvu-read-string (format "(%s) Annotation: " color)
+            (comment (djvu-read-string (format "(%s) Annotation: "
+                                               (or color "no color"))
                                        region)))
        (list (car region) (cdr region) nil comment color djvu-opacity 'none))))
 
@@ -2834,15 +2966,15 @@ each defining a rect area for djvused."
     (unless (and djvu-rect-area-nodups
                  (save-excursion
                    (goto-char (point-min))
-                   (re-search-forward (mapconcat 'identity rects "[ \t\n]*")
+                   (re-search-forward (mapconcat #'identity rects "[ \t\n]*")
                                       nil t)))
       (goto-char (point-max))
       (insert (format "(maparea %S\n %S\n ("
                       (or url "") (if comment (djvu-fill comment) ""))
-              (mapconcat 'identity rects "\n  ")
+              (mapconcat #'identity rects "\n  ")
               ")\n"
               (djvu-insert-color "hilite" color)
-              (if opacity (format " (opacity %s)" opacity) "")
+              (if (and color opacity) (format " (opacity %s)" opacity) "")
               (format " (%s)" (or border 'none))
               ")\n\n")
       (undo-boundary))))
@@ -2863,8 +2995,8 @@ This value of `fill-column' defaults to 
`djvu-fill-column'."
   "Toggle between Mapareas rect and text."
   (interactive)
   (let ((bounds (djvu-object-bounds))
-        (rect-re "(rect \\(-?[0-9]+ -?[0-9]+ -?[0-9]+ -?[0-9]+\\))")
-        (text-re "(text \\(-?[0-9]+ -?[0-9]+ -?[0-9]+ -?[0-9]+\\))")
+        (rect-re (format "(rect[\s\t]+%s)" djvu-coords-re))
+        (text-re (format "(text[\s\t]+%s)" djvu-coords-re))
         (color-re (format djvu-color-re "#" "" "")))
     (if (not bounds)
         (user-error "No object to update")
@@ -2876,7 +3008,7 @@ This value of `fill-column' defaults to 
`djvu-fill-column'."
           (cond ((re-search-forward rect-re nil t) ; Maparea rect
                  (if (save-match-data (re-search-forward rect-re nil t))
                      (user-error "Only single rect can be converted to text"))
-                 (replace-match (format "text %s" (match-string 1)))
+                 (replace-match (format "text %s" (match-string 2)))
                  (goto-char (point-min))
                  (let ((opacity
                         (if (re-search-forward " *(opacity \\([0-9]+\\))" nil 
t)
@@ -2900,7 +3032,7 @@ This value of `fill-column' defaults to 
`djvu-fill-column'."
                  ;; to duplicate the job.
                  (let ((opacity (save-match-data
                                   (read-number "Opacity: " djvu-opacity))))
-                   (replace-match (format "((rect %s))" (match-string 1)))
+                   (replace-match (format "((rect %s))" (match-string 2)))
                    (goto-char (point-min))
                    ;; Loop over matches of COLOR-RE as this general regexp
                    ;; also matches elements that so far we do not care about.
@@ -2915,13 +3047,6 @@ This value of `fill-column' defaults to 
`djvu-fill-column'."
                 (t
                  (user-error "Nothing to toggle"))))))))
 
-(defvar djvu-area-re
-  (format "(%s \\(%s\\))"
-          (regexp-opt '("rect" "oval" "text") t)
-          (mapconcat (lambda (_) "\\(-?[0-9]+\\)") '(1 2 3 4) " "))
-  "Regexp matching a Djvu area.
-Substring 1: area type, 2: coordinates, 3-6: individual coordinates.")
-
 (defun djvu-resize-internal (step)
   "Resize Djvu mapareas rect and text by STEP."
   (interactive "nStep: ")
@@ -2933,6 +3058,8 @@ Substring 1: area type, 2: coordinates, 3-6: individual 
coordinates.")
           (narrow-to-region (car bounds) (cdr bounds))
           (goto-char (point-min))
           (while (re-search-forward djvu-area-re nil t)
+            (if (string= "poly" (match-string 1))
+                (user-error "Cannot resize maparea poly"))
             (replace-match (format "%d %d %d %d"
                                    (- (djvu-match-number 3) step)
                                    (- (djvu-match-number 4) step)
@@ -2940,14 +3067,15 @@ Substring 1: area type, 2: coordinates, 3-6: individual 
coordinates.")
                                    (+ (djvu-match-number 6) step))
                              nil nil nil 2)))))))
 
-(defun djvu-shift-internal (shiftx shifty &optional all)
+(defun djvu-shift-internal (shiftx shifty &optional all scale)
   "Shift Djvu mapareas rect and text by SHIFTX and SHIFTY.
 With prefix ALL non-nil shift all mapareas of current page."
   (interactive
-   (let ((shift (mapcar 'string-to-number
+   (let ((shift (mapcar #'string-to-number
                         (split-string (read-string "Shiftx, shifty: ")
                                       "[\t\s\n,;]+" t "[\t\s\n]"))))
      (list (nth 0 shift) (nth 1 shift) current-prefix-arg)))
+  (unless (numberp scale) (setq scale 1))
   (save-excursion
     (save-restriction
       (unless all
@@ -2961,11 +3089,17 @@ With prefix ALL non-nil shift all mapareas of current 
page."
       ;; Cut off visible areas, drop invisble areas (with warning?)
       (while (re-search-forward djvu-area-re nil t)
         (replace-match (format "%d %d %d %d"
-                               (+ (djvu-match-number 3) shiftx)
-                               (+ (djvu-match-number 4) shifty)
-                               (+ (djvu-match-number 5) shiftx)
-                               (+ (djvu-match-number 6) shifty))
-                       nil nil nil 2)))))
+                               (+ (* (djvu-match-number 3) scale) shiftx)
+                               (+ (* (djvu-match-number 4) scale) shifty)
+                               (+ (* (djvu-match-number 5) scale) shiftx)
+                               (+ (* (djvu-match-number 6) scale) shifty))
+                       nil nil nil 2)
+        (if (string= "poly" (match-string 1))
+            (while (progn (skip-chars-forward "\s\t\n")
+                          (looking-at djvu-coord-xy-re))
+              (replace-match (format "%d %d"
+                                     (+ (* (djvu-match-number 1) scale) shiftx)
+                                     (+ (* (djvu-match-number 2) scale) 
shifty)))))))))
 
 (defun djvu-remove-linebreaks-internal ()
   "Remove linebreaks in Maparea string.
@@ -3047,6 +3181,7 @@ or maximum among the Nth elements of all arrays CI."
   ;; Assume that BEG has PROP.
   (let* ((zone (copy-sequence (get-text-property beg prop)))
          (max (aref zone 1))
+         (min (aref zone 3))
          (pnt beg)
          val)
     (while (and (/= pnt end)
@@ -3056,24 +3191,35 @@ or maximum among the Nth elements of all arrays CI."
         (aset zone 1 (min (aref zone 1) (aref val 1)))
         (setq max (max max (aref val 1))) ; descending words
         (aset zone 2 (max (aref zone 2) (aref val 2)))
-        (aset zone 3 (max (aref zone 3) (aref val 3)))))
+        (aset zone 3 (max (aref zone 3) (aref val 3)))
+        (setq min (min min (aref val 3))))) ; ascending words
 
     ;; The following is rather heuristic.  Suggestions for better
     ;; solutions welcome, though probably not worth the effort.
     ;; Set `djvu-descenders-re' to nil if you do not like this.
-    (if (and djvu-descenders-re
-             (eq prop 'word)
-             ;; descending words
-             (> 0.10 (/ (- max (aref zone 1))
-                       (float (- (aref zone 3) (aref zone 1)))))
-             (let ((string (buffer-substring-no-properties beg end)))
-               (not (or (= 1 (length string)) ; single-character string
-                        ;; all-uppercase string
-                        (string= string (upcase string))
-                        ;; descender characters
-                        (string-match djvu-descenders-re string)))))
-        (aset zone 1 (- (aref zone 1)
-                        (round (* 0.20 (- (aref zone 3) (aref zone 1)))))))
+    (if (and (or djvu-descenders-re djvu-ascenders-re)
+             (eq prop 'word))
+        (let* ((string (buffer-substring-no-properties beg end))
+               (long (< 1 (length string))) ; not a single-character string
+               case-fold-search)
+          (if (and long djvu-descenders-re
+                   ;; descending words
+                   (> 0.10 (/ (- max (aref zone 1))
+                              (float (- (aref zone 3) (aref zone 1)))))
+                   ;; all-uppercase string
+                   (not (string= string (upcase string)))
+                   ;; descender characters
+                   (not (string-match djvu-descenders-re string)))
+              (aset zone 1 (- (aref zone 1)
+                              (round (* 0.20 (- (aref zone 3) (aref zone 
1)))))))
+          (if (and long djvu-ascenders-re
+                   ;; ascending words
+                   (> 0.10 (/ (- (aref zone 3) min)
+                              (float (- (aref zone 3) (aref zone 1)))))
+                   ;; ascender characters
+                   (not (string-match djvu-ascenders-re string)))
+              (aset zone 3 (+ (aref zone 3)
+                              (round (* 0.20 (- (aref zone 3) (aref zone 
1)))))))))
     zone))
 
 (defun djvu-region-count (beg end prop)
@@ -3097,7 +3243,8 @@ or maximum among the Nth elements of all arrays CI."
           (djvu-convert-hash)
           (goto-char (point-min))
           (while (progn (skip-chars-forward " \t\n") (not (eobp)))
-            (if (looking-at djvu-annot-re)
+            (beginning-of-line)
+            (if (looking-at djvu-beg-object-re)
                 (condition-case nil
                     (push (read (current-buffer)) object)
                   (error (error "Syntax error in annotations")))
@@ -3132,14 +3279,15 @@ file SCRIPT.  DOC defaults to the current Djvu 
document."
                  (prin1 elt)
                  (insert "\n"))
                 ((or (not (eq 'maparea (car elt))) ; not maparea
-                     (eq 'line (car (nth 3 elt)))) ; maparea line
+                     (memq (car (nth 3 elt)) '(line poly))) ; maparea line, 
poly
                  (prin1 elt)
                  (insert "\n"))
-                ((consp (car (nth 3 elt))) ; maparea rect and oval
+                ((consp (car (nth 3 elt))) ; maparea rect
                  (dolist (area (nth 3 elt))
                    (insert (prin1-to-string
                             (apply 'list (car elt) (nth 1 elt) (nth 2 elt)
-                                   (djvu-area area t) (nthcdr 4 elt))) "\n"))
+                                   (djvu-area area t) (nthcdr 4 elt)))
+                           "\n"))
                  (setq id (1+ id))
                  (push (djvu-rect-elt
                         ;; `djvu-rect-elt' expects that the rect areas are at
@@ -3149,11 +3297,12 @@ file SCRIPT.  DOC defaults to the current Djvu 
document."
                               (nth 3 elt))
                         id)
                        rect-list))
-                ((eq 'text (car (nth 3 elt))) ; maparea text
+                ((memq (car (nth 3 elt)) '(text oval)) ; maparea text, oval
                  (insert (prin1-to-string
                           (apply 'list (car elt) (nth 1 elt) (nth 2 elt)
                                  (djvu-area (nth 3 elt) t)
-                                 (nthcdr 4 elt))) "\n"))
+                                 (nthcdr 4 elt)))
+                         "\n"))
                 (t (error "Djvu maparea %s undefined" (car (nth 3 elt))))))
         (insert ".\n")
         (djvu-convert-hash t)
@@ -3181,6 +3330,7 @@ You get what you want."
     ;; Put this in a separate buffer!
     (with-current-buffer buffer
       (let ((buffer-undo-list t)
+            (djvu-init t)
             buffer-read-only)
         (djvu-script-mode)
         (erase-buffer)
@@ -3212,19 +3362,13 @@ You get what you want."
             (let* ((object (djvu-object bounds))
                    (area (nth 3 object)))
               (if (eq (car object) 'maparea)
-                  (cond ((memq (car area) '(text line))
+                  (cond ((memq (car area) '(text oval line poly))
                          (cdr (nth 3 object)))
-                        ((consp area) ; maparea rect and oval
+                        ((consp area) ; maparea rect
                          (cdar area))))))))))
 
 ;;; Manipulate annotations
 
-(defvar djvu-beg-object-re
-  (concat "^[\s\t]*(" (regexp-opt '("background" "zoom" "mode" "align"
-                                    "maparea" "metadata" "bookmarks")
-                                  t))
-  "Regexp matching the beginning of Djvu annotation object.")
-
 (defun djvu-object-bounds ()
   "Return bounds (BEG . END) of Djvu object that contains or follows point.
 Return nil if no such object can be found."
@@ -3258,7 +3402,7 @@ Return nil if no such object can be found."
 
 (defun djvu-update-color (color)
   "Update color attribute of Djvu maparea to COLOR."
-  (interactive (list (completing-read "New Color: " djvu-color-alist nil t)))
+  (interactive (list (djvu-interactive-color-read)))
   (let ((dpos (djvu-dpos))
         (doc djvu-doc))
     (with-current-buffer (djvu-ref annot-buf doc)
@@ -3271,19 +3415,30 @@ Return nil if no such object can be found."
 If no such attribute exists insert a new one.
 Prefix arg OPACITY is the opacity to use."
   (interactive
-   (list (completing-read "New Color: " djvu-color-alist nil t)
-         (if current-prefix-arg
-             (read-number "Opacity: "))))
+   (let ((color (djvu-interactive-color-read)))
+     (list color
+           (if (and color current-prefix-arg)
+               (read-number "Opacity: ")))))
   (let ((bounds (djvu-object-bounds))
         (opacity (or opacity djvu-opacity)))
     (if bounds
         (save-excursion
           (goto-char (car bounds))
-          (cond ((re-search-forward
+          (cond ((not color)
+                 ;; remove color and opacity attributes
+                 (when (re-search-forward
+                        (format djvu-color-re "#" "" "") (cdr bounds) t)
+                   (replace-match "")
+                   (if (looking-at "[\s\t\n]+") (replace-match "")))
+                 (goto-char (car bounds))
+                 (when (re-search-forward "(opacity [0-9]+)" (cdr bounds) t)
+                   (replace-match "")
+                   (if (looking-at "[\s\t\n]+") (replace-match ""))))
+                ((re-search-forward
                   (format djvu-color-re "#" "" "") (cdr bounds) t)
                  ;; update existing color attribute
                  (let ((attr (match-string 1)))
-                   (cond ((member attr '("hilite" "lineclr"))
+                   (cond ((member attr '("hilite" "lineclr" "border"))
                           (replace-match (cdr (assoc color djvu-color-alist))
                                          nil nil nil 2))
                          ((string= attr "backclr")
@@ -3301,6 +3456,14 @@ Prefix arg OPACITY is the opacity to use."
                            (goto-char (car bounds))
                            (re-search-forward "(opacity [0-9]+)" (cdr bounds) 
t))
                    (insert (format " (opacity %d)" opacity))))
+                ((re-search-forward "(line" (cdr bounds) t)
+                 (goto-char (1- (cdr bounds)))
+                 (insert (format " (lineclr %s)"
+                                 (cdr (assoc color djvu-color-alist)))))
+                ((re-search-forward "(poly" (cdr bounds) t)
+                 (goto-char (1- (cdr bounds)))
+                 (insert (format " (border %s) (border_avis)"
+                                 (cdr (assoc color djvu-color-alist)))))
                 ((re-search-forward "(text" (cdr bounds) t)
                  (goto-char (1- (cdr bounds)))
                  (insert (format " (backclr %s)"
@@ -3340,7 +3503,7 @@ Prefix arg OPACITY is the opacity to use."
     ;; Remove duplicate attribute
     (setq url (or (delete-dups (delete "" url)) '("")))
     (if (nth 1 url) (user-error "Cannot merge multiple URLs"))
-    (setq text (mapconcat 'identity (nreverse (delete "" text)) "\n"))
+    (setq text (mapconcat #'identity (nreverse (delete "" text)) "\n"))
     (setq hilite (delete-dups hilite))
     (if (nth 1 hilite) (user-error "Cannot merge multiple hilites"))
     (setq opacity (delete-dups opacity))
@@ -3352,7 +3515,7 @@ Prefix arg OPACITY is the opacity to use."
     (goto-char beg)
     (delete-region beg end)
     (insert (format "(maparea %S\n %S\n (" (car url) text)
-            (mapconcat 'prin1-to-string (nreverse rect) "\n  ") ")\n"
+            (mapconcat #'prin1-to-string (nreverse rect) "\n  ") ")\n"
             (if hilite (format " (hilite %s)" (car hilite)) "")
             (if opacity (format " (opacity %s)" (car opacity)) "")
             (format " (%s)" (car border))
@@ -3509,6 +3672,58 @@ Return nil if OBJECT does not have internal URLs."
       (djvu-insert-bookmarks (nthcdr 2 elt) indent1)
       (insert ")"))))
 
+(defun djvu-read-bookmarks (&optional doc)
+  "Read bookmarks of a Djvu document from bookmarks buffer."
+  (let (object)
+    (with-current-buffer (djvu-ref bookmarks-buf doc)
+      (save-excursion
+        (save-restriction
+          (widen)
+          (goto-char (point-min))
+          (unless (eobp)
+            (condition-case nil
+                (setq object (read (current-buffer)))
+              (error (error "Syntax error in bookmarks"))))
+          (skip-chars-forward " \t\n")
+          ;; We should have swallowed all bookmarks.
+          (unless (eobp)
+            (error "Syntax error in bookmarks (position %s)" (point))))))
+    (if (and object (not (eq 'bookmarks (car object))))
+        (error "Malformed bookmarks"))
+    object))
+
+(defun djvu-reformat-bookmarks (&optional doc)
+  "Reformat Bookmarks buffer for Djvu document DOC."
+  (interactive)
+  (with-current-buffer (djvu-ref bookmarks-buf doc)
+    (let ((pnt (point))
+          (object (djvu-read-bookmarks doc)))
+      (erase-buffer)
+      (insert "(bookmarks")
+      (djvu-insert-bookmarks (cdr object) " ")
+      (insert ")\n")
+      (goto-char pnt))))
+
+(defun djvu-save-bookmarks (script &optional doc)
+  "Save bookmarks of a Djvu document.
+This dumps the content of DOC's bookmarks buffer into the djvused script
+file SCRIPT. DOC defaults to the current Djvu document."
+  (unless doc (setq doc djvu-doc))
+  (let ((object (djvu-read-bookmarks doc)))
+    (with-temp-buffer
+      (setq buffer-file-coding-system 'utf-8)
+      (insert "set-outline\n")
+      (when object
+        (insert "(bookmarks")
+        (let ((djvu-doc doc)) ; DOC should definitely be initialized above
+          (djvu-insert-bookmarks (cdr object) " "))
+        (insert ")\n"))
+      (insert ".\n")
+      (write-region nil nil script t 0)) ; append to SCRIPT
+    (djvu-init-outline (cdr object) doc)))
+
+;;; Djvu Outline mode
+
 (defun djvu-init-outline (object &optional doc)
   (with-current-buffer (djvu-ref outline-buf doc)
     (let (buffer-read-only)
@@ -3562,63 +3777,16 @@ PNT defaults to position of point."
       (forward-line))
     (goto-char pnt)))
 
-(defun djvu-read-bookmarks (&optional doc)
-  "Read bookmarks of a Djvu document from bookmarks buffer."
-  (let (object)
-    (with-current-buffer (djvu-ref bookmarks-buf doc)
-      (save-excursion
-        (save-restriction
-          (widen)
-          (goto-char (point-min))
-          (unless (eobp)
-            (condition-case nil
-                (setq object (read (current-buffer)))
-              (error (error "Syntax error in bookmarks"))))
-          (skip-chars-forward " \t\n")
-          ;; We should have swallowed all bookmarks.
-          (unless (eobp)
-            (error "Syntax error in bookmarks (end of buffer)")))))
-    (if (and object (not (eq 'bookmarks (car object))))
-        (error "Malformed bookmarks"))
-    object))
-
-(defun djvu-reformat-bookmarks (&optional doc)
-  "Reformat Bookmarks buffer for Djvu document DOC."
-  (interactive)
-  (with-current-buffer (djvu-ref bookmarks-buf doc)
-    (let ((pnt (point))
-          (object (djvu-read-bookmarks doc)))
-      (erase-buffer)
-      (insert "(bookmarks")
-      (djvu-insert-bookmarks (cdr object) " ")
-      (insert ")\n")
-      (goto-char pnt))))
-
-(defun djvu-save-bookmarks (script &optional doc)
-  "Save bookmarks of a Djvu document.
-This dumps the content of DOC's bookmarks buffer into the djvused script
-file SCRIPT. DOC defaults to the current Djvu document."
-  (unless doc (setq doc djvu-doc))
-  (let ((object (djvu-read-bookmarks doc)))
-    (with-temp-buffer
-      (setq buffer-file-coding-system 'utf-8)
-      (insert "set-outline\n")
-      (when object
-        (insert "(bookmarks")
-        (let ((djvu-doc doc)) ; DOC should definitely be initialized above
-          (djvu-insert-bookmarks (cdr object) " "))
-        (insert ")\n"))
-      (insert ".\n")
-      (write-region nil nil script t 0)) ; append to SCRIPT
-    (djvu-init-outline (cdr object) doc)))
-
 ;;; Image minor mode
 
 (defmacro djvu-with-event-buffer (event &rest body)
   "With buffer of EVENT current, evaluate BODY."
   (declare (indent 1))
   ;; Fixme: abort if `minibufferp' returns non-nil?
-  `(with-current-buffer (window-buffer (posn-window (event-start ,event)))
+  `(with-current-buffer
+       (window-buffer (let ((win (posn-window (event-start ,event))))
+                        (if (windowp win) win
+                          (user-error "Event not over window"))))
      ,@body))
 
 (defun djvu-image-toggle ()
@@ -3634,17 +3802,25 @@ file SCRIPT. DOC defaults to the current Djvu document."
   "Image display of current page."
   :lighter "Image"
 
+  ;; Keybindings for motion commands adopted from `image-mode-map'
   :keymap '((" "              . djvu-image-scroll-up)
             ([?\S-\ ]         . djvu-image-scroll-down)
             ("\C-?"           . djvu-image-scroll-down)
-            ("\C-n"           . djvu-image-next-line)
-            ([down]           . djvu-image-next-line)
-            ("\C-p"           . djvu-image-previous-line)
-            ([up]             . djvu-image-previous-line)
-            ([remap forward-char]  . image-forward-hscroll)
-            ([remap backward-char] . image-backward-hscroll)
-            ([remap right-char]    . image-forward-hscroll)
-            ([remap left-char]     . image-backward-hscroll)
+            ([remap scroll-up-command]      . djvu-image-scroll-up)
+            ([remap scroll-down-command]    . djvu-image-scroll-down)
+            ([remap next-line]              . djvu-image-next-line)
+            ([remap previous-line]          . djvu-image-previous-line)
+            ([remap forward-char]           . image-forward-hscroll)
+            ([remap backward-char]          . image-backward-hscroll)
+            ([remap right-char]             . image-forward-hscroll)
+            ([remap left-char]              . image-backward-hscroll)
+            ([remap move-beginning-of-line] . image-bol)
+            ([remap move-end-of-line]       . image-eol)
+            ([remap beginning-of-buffer]    . image-bob)
+            ([remap end-of-buffer]          . image-eob)
+            ;;
+            ("+" . djvu-image-zoom-in)
+            ("-" . djvu-image-zoom-out)
 
             ([drag-mouse-1]   . djvu-mouse-rect-area)
             ([S-drag-mouse-1] . djvu-mouse-text-area)
@@ -3666,26 +3842,43 @@ file SCRIPT. DOC defaults to the current Djvu document."
             ([M-drag-mouse-1] . djvu-mouse-word-area)
             ([M-down-mouse-1] . djvu-mouse-drag-track-area)
             ([drag-mouse-3]   . djvu-mouse-word-area) ; substitute
-            ([down-mouse-3]   . djvu-mouse-drag-track-area) ; substitute
-            ;;
-            ("+" . 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)))))
+            ([down-mouse-3]   . djvu-mouse-drag-track-area)) ; substitute
+
+  (image-mode-setup-winprops)
+
+  (let* ((display (get-text-property (point-min) 'display))
+         (enable (and djvu-image-mode (not display)))
+         (disable (and (not djvu-image-mode) display)))
+    (cond (enable
+           ;; Remember DPOS if we enable `djvu-image-mode'.
+           (djvu-set read-pos (let (djvu-image-mode)
+                                (djvu-read-dpos)))
+           ;; Don't scroll unless the user specifically asked for it.
+           (setq-local auto-hscroll-mode nil))
+          (disable
+           ;; Remember scrolling when we leave image mode
+           (djvu-set image-vscroll (djvu-image-vscroll))
+           (djvu-set image-hscroll (window-hscroll))
+           (set-window-hscroll (selected-window) 0)))
+
+    (if (or enable disable) (djvu-image))
+
+    (cond (enable
+           ;; Code adopted from `image-bol'.
+           (let* ((image-size (image-display-size
+                               (image-get-display-property)))
+                  (img-width  (ceiling (car image-size)))
+                  (img-height (ceiling (cdr image-size)))
+                  (edges (window-inside-edges))
+                  (win-width  (- (nth 2 edges) (nth 0 edges)))
+                  (win-height (- (nth 3 edges) (nth 1 edges))))
+             (image-set-window-vscroll (min (djvu-ref image-vscroll)
+                                            (max 0 (- img-height win-height))))
+             (image-set-window-hscroll (min (djvu-ref image-hscroll)
+                                            (max 0 (- img-width win-width))))))
+          (disable
+           ;; Go to DPOS if we disable `djvu-image-mode'.
+           (djvu-goto-read (djvu-ref read-pos))))))
 
 (defun djvu-image (&optional isize)
   "If `djvu-image-mode' is enabled, display image of current Djvu page.
@@ -3695,9 +3888,8 @@ Otherwise remove the image."
   ;; 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))))
+      (let (buffer-read-only)
+        (remove-text-properties (point-min) (point-max) '(display nil)))
     ;; Update image if necessary.
     (if (or (not (eq (djvu-ref page) (car (djvu-ref image))))
             (and isize
@@ -3728,91 +3920,115 @@ Otherwise remove the image."
                                      'pbm t))
                doc)))))
     ;; Display image.
-    (let ((hscroll (window-hscroll))
-          buffer-read-only)
+    (let (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))))
+                         'display (nthcdr 2 (djvu-ref image))))))
 
-;; The following scrolling commands are adapted from `doc-view-mode'.
-;; Up to Emacs 26, the functions `image-scroll-down', `image-scroll-up',
-;; `image-next-line', and `image-previous-line' return multiples of the
-;; character height.  Starting with Emacs 27 (commit 9c66b09950),
-;; these functions return pixel values.
+;; The following scrolling commands are adapted from `image-mode'
+;; and `doc-view-mode'.
+
+(defun djvu-image-vscroll ()
+  "Return the amount by which a page image is scrolled vertically."
+  ;; Up to Emacs 26, the functions `image-scroll-down', `image-scroll-up',
+  ;; `image-next-line', and `image-previous-line' return multiples of the
+  ;; character height.  Starting with Emacs 27, these functions return
+  ;; pixel values.  To be compatible, we must call `window-vscroll'
+  ;; without or with arg PIXELS-P non-nil.
+  (window-vscroll nil (<= 27 (string-to-number emacs-version))))
 
 (defun djvu-image-scroll-up (&optional n)
   "Scroll image of current page upward by N lines.
 At the bottom of the image, when `djvu-continuous' is non-nil
-or prefix N is nil, go to the image of the next page.
-Prefix N may take the same values as arg N of `image-scroll-up'.
-For historical reasons, this includes the range of values
-of `current-prefix-arg'."
+go to the image of the next page.
+Prefix N may take the same values as arg N of `image-scroll-up'."
   (interactive "P") ; same as `image-scroll-up'
-  (if (and (= (window-vscroll nil (<= 27 (string-to-number emacs-version)))
-              (image-scroll-up n))
-           (or djvu-continuous (not n))
+  (if (and (= (djvu-image-vscroll) (image-scroll-up n))
+           djvu-continuous
            (< (djvu-ref page) (djvu-ref pagemax)))
       (let ((hscroll (window-hscroll)))
         (djvu-next-page 1)
         (image-bob)
         (image-bol 1)
-        (set-window-hscroll (selected-window) hscroll))))
+        (image-set-window-hscroll hscroll))))
 
 (defun djvu-image-scroll-down (&optional n)
   "Scroll image of current page downward N lines.
 At the top of the image, when `djvu-continuous' is non-nil
-or prefix N is nil, go to the image of the previous page.
-Prefix N may take the same values as arg N of `image-scroll-down'.
-For historical reasons, this includes the range of values
-of `current-prefix-arg'."
+go to the image of the previous page.
+Prefix N may take the same values as arg N of `image-scroll-down'."
   (interactive "P") ; same as `image-scroll-down'
-  (if (and (= (window-vscroll nil (<= 27 (string-to-number emacs-version)))
-              (image-scroll-down n))
-           (or djvu-continuous (not n))
+  (if (and (= (djvu-image-vscroll) (image-scroll-down n))
+           djvu-continuous
            (< 1 (djvu-ref page)))
       (let ((hscroll (window-hscroll)))
         (djvu-prev-page 1)
         (image-eob)
         (image-bol 1)
-        (set-window-hscroll (selected-window) hscroll))))
+        (image-set-window-hscroll hscroll))))
 
 (defun djvu-image-next-line (&optional n)
   "Scroll image of current page upward by N lines.
 At the bottom of the image, when `djvu-continuous' is non-nil,
 go to the image of the next page."
   (interactive "p")
-  (if (and (= (window-vscroll nil (<= 27 (string-to-number emacs-version)))
-              (image-next-line n))
+  (if (and (= (djvu-image-vscroll) (image-next-line n))
            djvu-continuous
            (< (djvu-ref page) (djvu-ref pagemax)))
       (let ((hscroll (window-hscroll)))
         (djvu-next-page 1)
         (image-bob)
         (image-bol 1)
-        (set-window-hscroll (selected-window) hscroll))))
+        (image-set-window-hscroll hscroll))))
 
 (defun djvu-image-previous-line (&optional n)
   "Scroll image of current page downward N lines.
 At the top of the image, when `djvu-continuous' is non-nil,
 go to the image of the previous page."
   (interactive "p")
-  (if (and (= (window-vscroll nil (<= 27 (string-to-number emacs-version)))
-              (image-previous-line n))
+  (if (and (= (djvu-image-vscroll) (image-previous-line n))
            djvu-continuous
            (< 1 (djvu-ref page)))
       (let ((hscroll (window-hscroll)))
         (djvu-prev-page 1)
         (image-eob)
         (image-bol 1)
-        (set-window-hscroll (selected-window) hscroll))))
+        (image-set-window-hscroll hscroll))))
+
+(defun djvu-image-zoom-in (&optional zoom)
+  (interactive)
+  ;; FIXME: this preserves the upper left corner of the image.
+  ;; If possible, we should preserve the center of the image.
+  (let ((hscroll (window-hscroll))
+        (vscroll (djvu-image-vscroll))
+        (zoom (or zoom djvu-image-zoom)))
+    (djvu-image (round (* (nth 1 (djvu-ref image)) zoom)))
+    (image-set-window-hscroll (round (* hscroll zoom)))
+    (image-set-window-vscroll (round (* vscroll zoom)))))
+
+(defun djvu-image-zoom-out (&optional zoom)
+  (interactive)
+  (let ((hscroll (window-hscroll))
+        (vscroll (djvu-image-vscroll))
+        (zoom (or zoom djvu-image-zoom)))
+    (djvu-image (round (/ (nth 1 (djvu-ref image)) zoom)))
+    (image-set-window-hscroll (round (/ hscroll zoom)))
+    (image-set-window-vscroll (round (/ vscroll zoom)))))
+
+;; Image-based editing commands
 
 (defun djvu-mouse-drag-track-area (start-event &optional line)
   "Track drag over image."
   (interactive "e")
   ;; Inspired by `mouse-drag-track'.
-  (setq track-mouse t)
-  (set-transient-map
+  (let ((old-track-mouse track-mouse))
+    ;; Disable `djvu-modified' during tracking,
+    ;; but we just remember the current modified flag.
+    ;; This makes the code much faster!
+    (remove-hook 'post-command-hook #'djvu-modified t)
+    (setq djvu-modified (buffer-modified-p))
+    (setq track-mouse 'drag-tracking)
+    (set-transient-map
      (let ((map (make-sparse-keymap)))
        (define-key map [mouse-movement]
          (lambda (event) (interactive "e")
@@ -3823,7 +4039,9 @@ go to the image of the previous page."
                               line))))
        map)
      t (lambda ()
-         (setq track-mouse nil))))
+         (add-hook 'post-command-hook #'djvu-modified nil t)
+         (setq djvu-modified nil)
+         (setq track-mouse old-track-mouse)))))
 
 (defun djvu-image-rect (&optional event line)
   "For PPM image specified via EVENT mark rectangle by inverting bits."
@@ -3884,26 +4102,20 @@ go to the image of the previous page."
                                               width))))))
                     (invert i (+ i 3)))
                   (setq x (+ x step)))))))
-        (with-silent-modifications
+        (let (buffer-read-only)
           (put-text-property
            (point-min) (point-max) 'display
            (create-image image 'pbm t)))
+        (restore-buffer-modified-p djvu-modified)
         (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))))
+    (let ((old-image (get-text-property (point-min) 'display))
+          buffer-read-only)
+      ;; The modified flag is set by `djvu-modified' in `post-command-hook'.
+      (put-text-property (point-min) (point-max)
+                         'display (nthcdr 2 (djvu-ref image)))
       (image-flush old-image))))
 
-(defun djvu-image-zoom-in ()
-  (interactive)
-  (djvu-image (round (* (nth 1 (djvu-ref image)) 1.2))))
-
-(defun djvu-image-zoom-out ()
-  (interactive)
-  (djvu-image (round (/ (nth 1 (djvu-ref image)) 1.2))))
-
 (defun djvu-event-to-area (event &optional dir)
   "Convert mouse EVENT to Djvu area coordinates."
   (let* ((e-start (event-start event))
@@ -3938,13 +4150,15 @@ go to the image of the previous page."
   (interactive "e")
   ;; Mouse events ignore prefix args?
   (djvu-with-event-buffer event
-    (djvu-image-rect event)
-    (let ((color (djvu-interactive-color djvu-color-highlight)))
-      (djvu-rect-area nil (read-string (format "(%s) Highlight: " color)
-                                      nil nil nil djvu-inherit-input-method)
-                      (list (djvu-event-to-area event))
-                      color djvu-opacity 'none))
-    (djvu-image-rect)))
+    (unwind-protect
+        (let ((color (djvu-interactive-color djvu-color-highlight))
+              ;; Do RECTS first as this may throw a user error.
+              (rects (list (djvu-event-to-area event))))
+          (djvu-rect-area nil (read-string (format "(%s) Highlight: "
+                                                   (or color "no color"))
+                                           nil nil nil 
djvu-inherit-input-method)
+                          rects color djvu-opacity 'none))
+      (djvu-image-rect))))
 
 (defun djvu-mouse-text-area (event)
   (interactive "e")
@@ -3957,14 +4171,15 @@ go to the image of the previous page."
 (defun djvu-mouse-text-area-internal (event prompt &optional pushpin)
   ;; Mouse events ignore prefix args?
   (djvu-with-event-buffer event
-    (djvu-image-rect event)
-    (let ((color (djvu-interactive-color djvu-color-highlight)))
-      (djvu-text-area nil (read-string (format "(%s) %s: " color prompt)
-                                      nil nil nil djvu-inherit-input-method)
-                      (djvu-event-to-area event) nil
-                      (djvu-color-background color)
-                      nil pushpin))
-    (djvu-image-rect)))
+    (unwind-protect
+        (let ((color (djvu-interactive-color djvu-color-highlight))
+              ;; Do AREA first as this may throw a user error.
+              (area (djvu-event-to-area event)))
+          (djvu-text-area nil (read-string (format "(%s) %s: "
+                                                   (or color "no color") 
prompt)
+                                           nil nil nil 
djvu-inherit-input-method)
+                          area nil (djvu-color-background color) nil pushpin))
+      (djvu-image-rect))))
 
 (defun djvu-mouse-line-area (event)
   (interactive "e")
@@ -3980,17 +4195,21 @@ go to the image of the previous page."
 
 (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)))))
-      (djvu-line-area nil text line nil nil djvu-line-width djvu-color-line))))
+    (unwind-protect
+        ;; Do LINE first as this may throw a user error.
+        (let* ((line (djvu-event-to-area event dir))
+               (color (djvu-interactive-color djvu-color-line))
+               (text (read-string (format "(%s) Line: " (or color "no 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)))))
+          (djvu-line-area nil text line nil nil
+                          djvu-line-width djvu-color-line))
+      (djvu-image-rect))))
 
 (defun djvu-line-area (url text line &optional border arrow width lineclr)
   ;; Record position where annotation was made.
@@ -4038,11 +4257,15 @@ go to the image of the previous page."
 (defun djvu-mouse-word-area (event)
   "Insert word."
   (interactive "e")
-  (with-current-buffer (djvu-with-event-buffer event
-                         (djvu-ref text-buf))
-    (djvu-text-line-area (read-string "Text: " nil nil nil
-                                      djvu-inherit-input-method)
-                         (djvu-bound-area (djvu-event-to-area event)))))
+  (djvu-with-event-buffer event
+    (unwind-protect
+        ;; Do AREA first as this may throw a user error.
+        (let ((area (djvu-event-to-area event)))
+          (with-current-buffer (djvu-ref text-buf)
+            (djvu-text-line-area (read-string "Text: " nil nil nil
+                                              djvu-inherit-input-method)
+                                 area)))
+      (djvu-image-rect))))
 
 ;;; Miscellaneous commands
 
@@ -4106,17 +4329,16 @@ If the width of a page exceeds WIDTH, increase the page 
resolution DPI
 accordingly."
   (interactive "nWidth: \nnWidth: %s, dpi: ")
   (unless doc (setq doc djvu-doc))
-  (let ((count 0) job)
+  (let (job)
     (with-temp-buffer
       (djvu-djvused doc t "-e" "size")
       (goto-char (point-min))
       (let ((page 0))
         (while (looking-at "width=\\([[:digit:]]+\\)")
           (setq page (1+ page))
-          (let ((w (djvu-match-number 1)))
-            (when (< width w)
-              (push (cons page (/ (* w dpi) width)) job)
-              (setq count (1+ count))))
+          (let ((d (/ (* (djvu-match-number 1) dpi) width)))
+            (if (< dpi d)
+                (push (cons page d) job)))
           (forward-line))))
     (if (not job)
         (message "Nothing to unify")
@@ -4126,7 +4348,7 @@ accordingly."
                                          (car elt) (cdr elt)))
                                job "; ")
                     "-s")
-      (message "%s pages updated: %s" count
+      (message "%s pages updated: %s" (length job)
                (mapconcat (lambda (elt) (format "%d" (car elt)))
                           (nreverse job) ", ")))))
 



reply via email to

[Prev in Thread] Current Thread [Next in Thread]