(require 'image-mode)
(eval-when-compile
(require 'cl-lib))
(defgroup djvu nil
"Djvu mode."
:group 'wp
:prefix "djvu-")
(defcustom djvu-color-highlight "yellow"
"Default color for highlighting."
:group 'djvu
:type 'string)
(defcustom djvu-color-himark "red"
"Default color for highmarking."
:group 'djvu
:type 'string)
(defcustom djvu-color-url "blue"
"Default color for URLs."
:group 'djvu
:type 'string)
(defcustom djvu-color-background "white"
"Default background."
:group 'djvu
:type 'string)
(defcustom djvu-color-line "black"
"Default line color."
:group 'djvu
:type 'string)
(defcustom djvu-color-alist
'(("red" . "#FF0070") ("green" . "#00FF00") ("blue" . "#6666FF") ("yellow" . "#EEFF00") ("orange" . "#FF7F00") ("magenta" . "#FF00FF") ("purple" . "#7F60FF") ("cyan" . "#00FFFF") ("pink" . "#FF6060") ("white" . "#FFFFFF") ("black" . "#000000")) "Alist of colors for highlighting."
:group 'djvu
:type '(repeat (cons (string) (string))))
(defcustom djvu-line-width 1
"Default line width."
:group 'djvu
:type 'integer)
(defcustom djvu-opacity 50
"Default opacity for Highlighting."
:group 'djvu
:type 'integer)
(defcustom djvu-areas-justify 0.02
"Upper threshold for justifying area coordinates."
:group 'djvu
:type 'number)
(defcustom djvu-fill-column 50
"Fill column for Djvu annotations."
:group 'djvu
:type 'integer)
(defcustom djvu-script-buffer "*djvu*"
"Default buffer for \"raw\" djvused scripts."
:group 'djvu
:type 'string)
(defcustom djvu-buffer-name-extensions
'("" "-t" "-a" "-s" "-b" "-o")
"Extensions for Djvu buffer names.
This is a list with six elements (READ TEXT ANNOT SHARED BOOKMARKS OUTLINE)."
:group 'djvu
:type '(list (string) (string) (string) (string) (string) (string)))
(defcustom djvu-image-size 1024
"Size of internally displayed image. This is MAX (width, height)."
:group 'djvu
:type 'integer)
(defcustom djvu-inherit-input-method t
"If non-nil calls of `read-string' inherit the input method."
:group 'djvu
:type 'boolean)
(defcustom djvu-djview-command "djview"
"Command for the Djvu Viewer."
:group 'djvu
:type 'string)
(defcustom djvu-djview-options nil
"List of command options for the Djvu Viewer."
:group 'djvu
:type '(repeat (string)))
(defcustom djvu-file-name-extension-re (regexp-opt '(".djvu" ".djbz" ".iff"))
"Regular expression for file name extensions in bundled multi-page documents.
These extensions include the period."
:group 'djvu
:type 'regexp)
(defcustom djvu-read-prop-newline 2
"Number of newline characters in Read buffer for consecutive region."
:group 'djvu
:type 'integer)
(defcustom djvu-outline-faces
[font-lock-function-name-face font-lock-variable-name-face
font-lock-keyword-face font-lock-comment-face
font-lock-type-face font-lock-constant-face
font-lock-builtin-face font-lock-string-face]
"Vector of faces for Outline buffer."
:group 'djvu
:type '(sexp))
(defcustom djvu-string-replace-list
'(("-\n+\\([[:lower:]]\\)" . "\\1") ("-\n+" . "-") ("[\n ]+" . " ")) "Replacement list for text strings.
Each element is of the form (REGEXP . REP).
Used by `djvu-region-string'."
:group 'djvu
:type '(repeat (cons (regexp) (string))))
(defcustom djvu-rect-area-nodups nil
"If non-nil `djvu-rect-area' does not create multiple rects for same areas."
:group 'djvu
:type 'boolean)
(defcustom djvu-continuous nil
"When non-nil, scrolling to the page edge advances to next/previous page."
:group 'djvu
:type 'boolean)
(defcustom djvu-image-zoom 1.2
"Zoom factor for images."
:group 'djvu
:type 'number)
(defcustom djvu-descenders-re "[(),;Qgjpqy]" "Regexp matching any descending characters or nil.
With Djvu mode, mapareas of annotations match tight the text they refer to.
This may appear visually awkward if the lower bound of the maparea lines up
with the baseline of the text because the text contains no descenders
from characters such as `g' or `q'. Then, if the text does not match
the regexp `djvu-descenders-re', the annotation area will descend
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)))
(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]+" (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.")
(defvar-local djvu-buffer nil
"Type of Djvu buffer.")
(defvar djvu-rect-list nil
"Expanded rect list for propertizing the Read buffer.
This is a list with elements (COORDS URL TEXT COLOR ID) stored
in `djvu-doc-rect-list'.")
(defvar djvu-last-rect nil
"Last rect used for propertizing the Read buffer.
This is a list (BEG END COORDS URL TEXT COLOR).")
(defvar djvu-resolve-url nil
"Flag for resolving internal URLs.
If `long' replace short page numbers by long FileIDs.
If `short' replace long FileIDs by short page numbers.
If nil do nothing.
Bind this with `let' to select one of these schemes.")
(defvar djvu-bookmark-level nil
"Counter for bookmark level.")
(defvar djvu-image-mode)
(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'.")
(defmacro djvu-defvar-local (var &optional val doc)
"Define VAR as a permanent-local variable, and return VAR."
(declare (doc-string 3))
`(progn
(defvar ,var ,val ,doc)
(make-variable-buffer-local ',var)
(put ',var 'permanent-local t)
,var))
(djvu-defvar-local djvu-doc nil
"The \"ID\" of a Djvu document.
This is actually the Read buffer acting as the master buffer
of the Djvu document. This buffer holds all buffer-local values
of variables for a Djvu document.")
(djvu-defvar-local djvu-doc-file nil
"File name of a Djvu document.")
(djvu-defvar-local djvu-doc-text-buf nil
"Text buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-read-buf nil
"Read buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-annot-buf nil
"Annotation buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-shared-buf nil
"Shared annotation buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-bookmarks-buf nil
"Bookmarks buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-outline-buf nil
"Outline buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-view-proc nil
"List of djview processes for a Djvu document.")
(defvar-local djvu-doc-resolve-url nil
"Resolve URLs of a Djvu document.")
(defvar-local djvu-doc-rect-list nil
"Rect list of a Djvu document.")
(defvar-local djvu-doc-history-backward nil
"Backward history of a Djvu document.
This is a stack of pages visited previously.")
(defvar-local djvu-doc-history-forward nil
"Forward history of a Djvu document.")
(defvar-local djvu-doc-page nil
"Current page number of a Djvu document.")
(defvar-local djvu-doc-pagemax nil
"Total number of pages of a Djvu document.")
(defvar-local djvu-doc-page-id nil
"Alist of page IDs of a Djvu document.
Each element is a cons pair (PAGE-NUM . FILE-ID).")
(defvar-local djvu-doc-pagesize nil
"Size of current page of a Djvu document.")
(defvar-local djvu-doc-read-pos nil
"The current editing position in the Read buffer (image coordinates).
This is either a list (X Y) or a list or vector (XMIN YMIN XMAX YMAX).
Used in `djvu-image-mode' when we cannot go to this position.")
(defvar-local djvu-doc-image nil
"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.")
(defmacro djvu-set (var val &optional doc)
"Set VAR's value to VAL in Djvu document DOC, and return VAL.
DOC defaults to `djvu-doc'."
(let ((var (intern (format "djvu-doc-%s" var)))
(tmpval (make-symbol "tmpval")))
`(let ((,tmpval ,val))
(with-current-buffer (or ,doc djvu-doc)
(set ',var ,tmpval)))))
(defmacro djvu-ref (var &optional doc)
"Return VAR's value in Djvu document DOC.
DOC defaults to `djvu-doc'."
(let ((var (intern (format "djvu-doc-%s" var))))
`(buffer-local-value ',var (or ,doc djvu-doc))))
(defun djvu-header-line (identifier)
(list (propertize " " 'display '(space :align-to 0))
(format "%s -- %s (p%d)" (buffer-name (djvu-ref read-buf))
identifier (djvu-ref page))))
(defsubst djvu-substring-number (string &optional from to base)
"Parse substring of STRING as a decimal number and return the number.
If BASE, interpret STRING as a number in that base."
(string-to-number (substring-no-properties string from to) base))
(defsubst djvu-match-number (num &optional string base)
"Return string of text matched by last search, as a number.
If BASE, interpret match as a number in that base."
(string-to-number (match-string num string) base))
(defsubst djvu-buffers (&optional doc)
"Return a list of all buffers for DOC."
(list (djvu-ref read-buf doc) (djvu-ref text-buf doc)
(djvu-ref annot-buf doc) (djvu-ref shared-buf doc)
(djvu-ref bookmarks-buf doc) (djvu-ref outline-buf doc)))
(defmacro djvu-all-buffers (doc &rest body)
"Evaluate BODY in all buffers of Djvu DOC."
(declare (indent 1))
`(dolist (buf (djvu-buffers ,doc))
(with-current-buffer buf
,@body)))
(defmacro djvu-with-temp-file (file &rest body)
"Evaluate BODY with temp file FILE deleted at the end.
Preserve FILE if `djvu-test' is non-nil."
(declare (indent 1) (debug (symbolp body)))
`(let ((,file (make-temp-file "djvu-")))
(unwind-protect
(progn ,@body)
(unless djvu-test (delete-file ,file)))))
(defun djvu-switch-read (&optional doc dpos)
"Switch to Djvu Read buffer."
(interactive (list nil (djvu-dpos)))
(switch-to-buffer (djvu-ref read-buf doc))
(djvu-goto-read dpos))
(defun djvu-switch-text (&optional doc dpos)
"Switch to Djvu Text buffer."
(interactive (list nil (djvu-dpos)))
(switch-to-buffer (djvu-ref text-buf doc))
(djvu-goto-dpos 'word dpos))
(defun djvu-switch-annot (&optional doc dpos)
"Switch to Djvu Annotations buffer."
(interactive (list nil (djvu-dpos)))
(switch-to-buffer (djvu-ref annot-buf doc))
(if (djvu-goto-dpos "\\(?:rect\\|text\\)" dpos)
(re-search-backward "\"")))
(defun djvu-switch-shared (&optional doc)
"Switch to Djvu Shared Annotations buffer."
(interactive)
(switch-to-buffer (djvu-ref shared-buf doc)))
(defun djvu-switch-bookmarks (&optional doc page)
"Switch to Djvu Bookmarks buffer."
(interactive (list nil (if (eq djvu-buffer 'outline)
(djvu-outline-page)
(djvu-ref page))))
(switch-to-buffer (djvu-ref bookmarks-buf doc))
(when page
(goto-char (point-min))
(if (looking-at "(bookmarks")
(while (and (< 0 page)
(not (re-search-forward
(format "\"#\\(%d\\|%s\\)\"" page
(cdr (assq page (djvu-ref page-id doc))))
nil t)))
(setq page (1- page))))))
(defun djvu-switch-outline (&optional doc page)
"Switch to Djvu Outline buffer."
(interactive (list nil (if (eq djvu-buffer 'bookmarks)
(djvu-bookmarks-page)
(djvu-ref page))))
(switch-to-buffer (djvu-ref outline-buf doc))
(if page (djvu-goto-outline page)))
(defun djvu-dpos (&optional doc)
"Djvu position in current Djvu buffer."
(cond ((eq djvu-buffer 'read)
(djvu-read-dpos nil doc))
((eq djvu-buffer 'text)
(djvu-text-dpos nil doc))
((eq djvu-buffer 'annot)
(djvu-annot-dpos nil doc))))
(defun djvu-read-page ()
"Read page number interactively."
(let ((str (read-string (format "Page (f, 1-%d, l): " (djvu-ref pagemax)))))
(cond ((string-match "\\`f" str) 1)
((string-match "\\`l" str) (djvu-ref pagemax))
((string-match "\\`[[:digit:]]+\\'" str)
(string-to-number str))
(t (user-error "Page `%s' invalid" str)))))
(defun djvu-next-page (n)
"Go to the next page of this Djvu document."
(interactive "p")
(djvu-goto-page (+ (djvu-ref page) n)))
(defun djvu-prev-page (n)
"Go to the previous page of this Djvu document."
(interactive "p")
(djvu-goto-page (- (djvu-ref page) n)))
(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
go to the next page.
Prefix ARG may take the same values as arg ARG of `scroll-up-command'."
(interactive "^P") (if (and djvu-continuous
(= (window-end) (point-max))
(< (djvu-ref page) (djvu-ref pagemax)))
(djvu-next-page 1)
(condition-case nil (scroll-up-command arg)
(end-of-buffer nil))))
(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
go to the previous page.
Prefix ARG may take the same values as arg ARG of `scroll-down-command'."
(interactive "^P") (if (and djvu-continuous
(= (point-min) (window-start))
(< 1 (djvu-ref page)))
(progn
(djvu-prev-page 1)
(goto-char (point-max))
(beginning-of-line)
(recenter -3))
(condition-case nil (scroll-down-command arg)
(beginning-of-buffer nil))))
(defun djvu-next-line (&optional _arg _try-vscroll)
"Move cursor vertically down ARG lines.
ARG and TRY-VSCROLL have the same meaning as for `next-line'.
At the bottom of the page, when `djvu-continuous' is non-nil,
go to the next page."
(interactive "^p\np") (if (and djvu-continuous
(= (line-end-position) (point-max))
(< (djvu-ref page) (djvu-ref pagemax)))
(djvu-next-page 1)
(call-interactively 'next-line)))
(defun djvu-prev-line (&optional _arg _try-vscroll)
"Move cursor vertically up ARG lines.
ARG and TRY-VSCROLL have the same meaning as for `previous-line'.
At the top of the page, when `djvu-continuous' is non-nil,
go to the previous page."
(interactive "^p\np") (if (and djvu-continuous
(= (point-min) (line-beginning-position))
(< 1 (djvu-ref page)))
(progn
(djvu-prev-page 1)
(goto-char (point-max))
(beginning-of-line)
(recenter -3))
(call-interactively 'previous-line)))
(defun djvu-history-backward ()
"Go backward in the history of visited pages."
(interactive)
(let ((history-backward (djvu-ref history-backward))
(history-forward (cons (djvu-ref page)
(djvu-ref history-forward))))
(unless history-backward
(user-error "This is the first page you looked at"))
(djvu-goto-page (car history-backward))
(djvu-set history-backward (cdr history-backward))
(djvu-set history-forward history-forward)))
(defun djvu-history-forward ()
"Go forward in the history of visited pages."
(interactive)
(let ((history-forward (djvu-ref history-forward)))
(unless history-forward
(user-error "This is the last page you looked at"))
(djvu-goto-page (car history-forward))
(djvu-set history-forward (cdr history-forward))))
(defun djvu-kill-view (&optional doc all)
"Kill most recent Djview process for DOC.
If ALL is non-nil, kill all Djview processes."
(interactive (list nil current-prefix-arg))
(let ((proc-list (djvu-ref view-proc doc)) proc nproc-list)
(while (setq proc (pop proc-list))
(unless (memq (process-status proc) '(exit signal))
(push proc nproc-list)))
(setq proc-list (nreverse nproc-list))
(while (setq proc (pop proc-list))
(quit-process proc)
(djvu-set view-proc proc-list)
(unless all (setq proc-list nil)))))
(defun djvu-kill-doc (&optional doc)
"Kill all buffers visiting DOC.
This relies on `djvu-kill-doc-all' for doing the real work."
(interactive)
(mapc #'kill-buffer (djvu-buffers doc)))
(defvar djvu-in-kill-doc nil
"Non-nil if we are running `djvu-kill-doc-all'.")
(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'."
(unless djvu-in-kill-doc
(let ((djvu-in-kill-doc t)
buffers)
(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))
(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)
(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."
(interactive)
(unless doc (setq doc djvu-doc))
(let ((afile (abbreviate-file-name (djvu-ref file doc)))
(text-modified (buffer-modified-p (djvu-ref text-buf doc)))
(annot-modified (buffer-modified-p (djvu-ref annot-buf doc)))
(shared-modified (buffer-modified-p (djvu-ref shared-buf doc)))
(bookmarks-modified (buffer-modified-p (djvu-ref bookmarks-buf doc))))
(when (and (or text-modified annot-modified shared-modified bookmarks-modified)
(or (and (verify-visited-file-modtime doc)
(or (not query)
(yes-or-no-p (format "Save %s? " afile))))
(yes-or-no-p (format "%s has changed since visited or saved. Save anyway? "
afile))))
(djvu-with-temp-file script
(if annot-modified (djvu-save-annot script doc))
(if shared-modified (djvu-save-annot script doc t))
(if text-modified (djvu-save-text doc script)) (if bookmarks-modified (djvu-save-bookmarks script doc))
(djvu-djvused doc nil "-f" script "-s"))
(if (and annot-modified (not text-modified))
(djvu-init-read (djvu-read-text doc) doc))
(djvu-all-buffers doc
(set-buffer-modified-p nil))))
t)
(defun djvu-modified ()
"Mark Djvu Read and Outline buffers as modified if necessary.
Used in `post-command-hook' of the Djvu Read, Text, Annotations,
Bookmarks and Outline buffers."
(let ((modified (or (buffer-modified-p (djvu-ref bookmarks-buf))
(buffer-modified-p (djvu-ref text-buf))
(buffer-modified-p (djvu-ref annot-buf))
(buffer-modified-p (djvu-ref shared-buf)))))
(with-current-buffer (djvu-ref read-buf)
(set-buffer-modified-p modified))
(with-current-buffer (djvu-ref outline-buf)
(set-buffer-modified-p modified))))
(defun djvu-quit-window (&optional kill doc)
"Quit all windows of Djvu document DOC and bury its buffers.
With prefix KILL non-nil, kill the buffers instead of burying them."
(interactive "P")
(unless doc (setq doc djvu-doc))
(dolist (buf (djvu-buffers doc))
(let ((window (get-buffer-window buf t)))
(cond (window
(let ((prev-buffers (window-prev-buffers window)))
(dolist (b (djvu-buffers doc))
(setq prev-buffers (assq-delete-all b prev-buffers)))
(set-window-prev-buffers window prev-buffers))
(quit-window kill window))
(kill
(kill-buffer buf))
(t
(bury-buffer buf))))))
(defun djvu-djvused (doc buffer &rest args)
"Process Djvu DOC by running the command djvused with ARGS.
BUFFER receives the process output, t means current buffer.
If BUFFER is nil, discard the process output, assuming that
the purpose of calling djvused is to update the Djvu file."
(unless doc (setq doc djvu-doc))
(unless (or buffer (file-writable-p (djvu-ref file doc)))
(user-error "File `%s' not writable"
(abbreviate-file-name (djvu-ref file doc))))
(when (or buffer (not djvu-test))
(unless buffer
(djvu-backup doc))
(let* ((inhibit-quit t)
(coding-system-for-read 'utf-8)
(status (apply 'call-process "djvused" nil buffer nil
"-u" (djvu-ref file doc) args)))
(unless (zerop status)
(error "Djvused error %s (args: %s)" status args))
(unless buffer
(djvu-all-buffers doc
(set-visited-file-modtime))))))
(defun djvu-backup (doc)
"Make a backup of the disk file for Djvu document DOC, if appropriate."
(with-current-buffer doc
(unless buffer-backed-up
(let* ((file (djvu-ref file doc))
(real-file (file-chase-links file))
(val (backup-buffer)))
(when buffer-backed-up
(djvu-all-buffers doc
(setq buffer-backed-up t))
(unless (file-exists-p real-file)
(backup-buffer-copy (nth 2 val) real-file
(nth 0 val) (nth 1 val))
(let ((file-number (nthcdr 10 (file-attributes file))))
(djvu-all-buffers doc
(setq buffer-file-number file-number)))))))))
(defun djvu-convert-hash (&optional reverse)
"Convert color symbols #000000 to strings \"#000000\".
Perform inverse transformation if REVERSE is non-nil."
(if reverse
(let ((re (format djvu-color-re "\"" "#" "\"")))
(goto-char (point-min))
(while (re-search-forward re nil t)
(replace-match (match-string 3) nil nil nil 2)))
(let ((re (format djvu-color-re "#" "" "")))
(goto-char (point-min))
(while (re-search-forward re nil t)
(replace-match (format "\"%s\"" (match-string 2)) nil nil nil 2)))))
(defmacro djvu-with-region (region &rest body)
"Provide REGION while executing BODY, deactivating REGION afterwards.
This is useful for the interactive spec of commands operating on REGION."
(declare (indent 1) (debug (symbolp body)))
`(let ((,region (let (beg end)
(if (use-region-p)
(setq beg (region-beginning)
end (region-end))
(setq beg (point) end beg))
(setq beg (djvu-property-beg beg 'word)
end (djvu-property-end end 'word))
(cons beg end))))
(prog1 (progn ,@body)
(if (and transient-mark-mode mark-active) (deactivate-mark)))))
(defun djvu-region-string (region &optional list)
"Return REGION of current buffer as string. REGION is a cons (BEG . END).
Apply replacements LIST to the buffer substring defined by REGION.
Each element in LIST is a cons (REGEXP . REP).
LIST defaults to `djvu-string-replace-list'. Return the resulting string."
(let ((string (buffer-substring-no-properties (car region)
(cdr region)))
case-fold-search)
(dolist (elt (or list djvu-string-replace-list))
(setq string (replace-regexp-in-string (car elt) (cdr elt) string)))
string))
(defun djvu-read-string (prompt region &optional initial-input)
"Read a string from the minibuffer, prompting with string PROMPT.
REGION is a cons (BEG . END) that defines the default.
If INITIAL-INPUT is non-nil use string from REGION as initial input."
(if initial-input
(read-string prompt (djvu-region-string region)
nil nil djvu-inherit-input-method)
(read-string prompt nil nil (djvu-region-string region)
djvu-inherit-input-method)))
(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 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.
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))))
(cond ((not colnum) color) ((>= 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.
This is the inverse of `djvu-url-page'."
(let ((page (or page (djvu-ref page doc))))
(format "#%s" (if (eq 'long (or dir (djvu-ref resolve-url doc)))
(cdr (assq page (djvu-ref page-id doc)))
page))))
(defun djvu-interactive-url (color)
"Return URL specification for use in interactive calls."
(let ((fmt (format "(%s) URL: " (or color "no color")))
val)
(while (not val)
(setq val (read-string fmt))
(cond ((string-match "\\`#?\\([0-9]+\\)\\'" val)
(setq val (djvu-match-number 1 val))
(if (<= 1 val (djvu-ref pagemax))
(setq val (djvu-page-url val))
(message "Page number %d out of range (1-%d)"
val (djvu-ref pagemax))
(sit-for 1)
(setq val nil)))
((not (string-match "\\`[a-z]+://" val))
(message "URL `%s' not recognized" val)
(sit-for 1)
(setq val nil))))
val))
(defun djvu-color-background (color &optional background opacity invert)
"For rgb COLOR and BACKGROUND apply OPACITY.
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."
(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)) (b (- 1 a))) (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 (+ (* 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)))))))
(defvar djvu-read-mode-map
(let ((km (make-sparse-keymap)))
(define-key km [remap scroll-up-command] 'djvu-scroll-up-command)
(define-key km [remap scroll-down-command] 'djvu-scroll-down-command)
(define-key km [remap next-line] 'djvu-next-line)
(define-key km [remap previous-line] 'djvu-prev-line)
(define-key km "i" 'djvu-image-toggle)
(define-key km "v" 'djvu-view)
(define-key km "\C-c\C-v" 'djvu-view)
(define-key km "n" 'djvu-next-page)
(define-key km "p" 'djvu-prev-page)
(define-key km "g" 'djvu-goto-page)
(define-key km "f" 'djvu-history-forward)
(define-key km "r" 'djvu-history-backward) (define-key km "k" 'djvu-kill-doc)
(define-key km "s" 'djvu-save)
(define-key km "\C-x\C-s" 'djvu-save)
(define-key km "q" 'djvu-quit-window)
(define-key km "G" 'djvu-revert-buffer)
(define-key km (kbd "C-c C-S-g") 'djvu-revert-buffer)
(define-key km "t" 'djvu-switch-text)
(define-key km "\C-c\C-t" 'djvu-switch-text)
(define-key km "\C-c\C-s" 'djvu-re-search-forward)
(define-key km "\M-," 'djvu-re-search-forward-continue)
(define-key km "ee" 'djvu-edit-word)
(define-key km "es" 'djvu-split-word)
(define-key km "ew" 'djvu-merge-words)
(define-key km "el" 'djvu-merge-lines)
(define-key km "T" 'djvu-text-script)
(define-key km "P" 'djvu-process-script)
(define-key km "a" 'djvu-switch-annot)
(define-key km "\C-c\C-a" 'djvu-switch-annot)
(define-key km "S" 'djvu-switch-shared)
(define-key km (kbd "C-c C-S-S") 'djvu-switch-shared)
(define-key km "h" 'djvu-rect-region) (define-key km "u" 'djvu-rect-region-url)
(define-key km "A" 'djvu-annot-script)
(define-key km "\C-c\C-c" 'djvu-update-color)
(define-key km "\C-c\C-u" 'djvu-update-url)
(define-key km "o" 'djvu-switch-outline)
(define-key km "\C-c\C-o" 'djvu-switch-outline)
(define-key km "b" 'djvu-switch-bookmarks)
(define-key km "\C-c\C-b" 'djvu-switch-bookmarks)
(define-key km "l" 'djvu-mark-line-beg)
(define-key km "B" 'djvu-bookmark)
(define-key km "m" 'djvu-himark)
(define-key km "D" 'djvu-delete-page)
(define-key km "U" 'djvu-resolve-all-urls)
km)
"Keymap for Djvu Read Mode.
This is a child of `special-mode-map'.")
(easy-menu-define
djvu-read-menu djvu-read-mode-map "Djvu Menu"
'("Djvu"
["Djview File" djvu-view t]
["Toggle Image mode" djvu-image-toggle t]
["Go to Page" djvu-goto-page t]
["Save Doc" djvu-save t]
["Revert Doc" djvu-revert-buffer t]
"---"
["Search Regexp Forward" djvu-re-search-forward t]
["Continue Search Re Forward" djvu-re-search-forward-continue t]
"---"
("Operate on text layer"
["Edit Word" djvu-edit-word t]
["Split Word" djvu-split-word t]
["Merge Words" djvu-merge-words t]
["Merge Lines" djvu-merge-lines t]
["Switch to Text" djvu-switch-text t])
("Operate on annotations layer"
["Highlight Region" djvu-rect-region t]
["Page URL over Region" djvu-rect-region-url t]
["Himark Region" djvu-himark t]
["Mark point" djvu-mark-line-beg t]
["Update color" djvu-update-color t]
["Update url" djvu-update-url t]
["Switch to Annotations" djvu-switch-annot t]
["Switch to Shared Annotations" djvu-switch-shared t])
("Operate on bookmarks layer"
["Add Bookmark" djvu-bookmark t]
["Switch to Bookmarks" djvu-switch-bookmarks t]
["Switch to Outline" djvu-switch-outline t])
("Editing multiple pages"
["Resolve internal URLs" djvu-resolve-all-urls t]
["Text as Script" djvu-text-script t]
["Annotations as Script" djvu-annot-script t]
["Process Djvused Script" djvu-process-script t])
("Destructive commands"
["Delete current page" djvu-delete-page t]
["Remove Annot / Bookmarks" djvu-make-clean t])
"---"
["Quit Viewing" djvu-quit-window t]
["Kill Djvu buffers" djvu-kill-doc t]))
(defvar bookmark-make-record-function)
(define-derived-mode djvu-read-mode special-mode "Djview"
"Mode for reading Djvu files."
(if (not djvu-init)
(djvu-init-mode) (setq buffer-auto-save-file-name nil 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) (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)))
(define-key km "\C-c\C-r" 'djvu-switch-read)
(define-key km "\C-c\C-t" 'djvu-switch-text)
(define-key km "\C-c\C-a" 'djvu-switch-annot)
(define-key km (kbd "C-c C-S-S") 'djvu-switch-shared)
(define-key km "\C-c\C-b" 'djvu-switch-bookmarks)
(define-key km "\C-c\C-o" 'djvu-switch-outline)
(define-key km "\C-c\C-g" 'djvu-goto-page)
(define-key km "\C-c\C-p" 'djvu-prev-page)
(define-key km "\C-c\C-n" 'djvu-next-page)
(define-key km "\C-c\C-es" 'djvu-split-word-internal)
(define-key km "\C-c\C-ew" 'djvu-merge-words-internal)
(define-key km "\C-c\C-el" 'djvu-merge-lines-internal)
(define-key km "\C-c\C-m" 'djvu-merge-mapareas)
(define-key km "\C-c\C-c" 'djvu-update-color-internal)
(define-key km "\C-c\C-u" 'djvu-update-url-internal)
(define-key km "\C-c\C-z" 'djvu-resize-internal)
(define-key km "\C-c\C-l" 'djvu-remove-linebreaks-internal)
(define-key km "\C-x\C-s" 'djvu-save)
(define-key km "\C-c\C-v" 'djvu-view)
(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)
km)
"Keymap for Djvu Script Mode.
This is a child of `lisp-mode-map'.")
(easy-menu-define
djvu-annot-menu djvu-script-mode-map "Djvu Menu"
'("Djvu"
["Go to Page" djvu-goto-page t]
["Switch to Read" djvu-switch-read t]
["Switch to Text" djvu-switch-text (not (eq djvu-buffer 'text))]
["Switch to Annotations" djvu-switch-annot (not (eq djvu-buffer 'annot))]
["Switch to Shared Annotations" djvu-switch-shared (not (eq djvu-buffer 'shared))]
["Switch to Bookmarks" djvu-switch-bookmarks t]
["Switch to Outline" djvu-switch-outline t]
["Save Doc" djvu-save t]
["Revert Doc" djvu-revert-buffer t]
"---"
["Merge Mapareas" djvu-merge-mapareas (eq djvu-buffer 'annot)]
["Update Color" djvu-update-color-internal (eq djvu-buffer 'annot)]
["Update URL" djvu-update-url-internal (eq djvu-buffer 'annot)]
["Resize Maparea" djvu-resize-internal (eq djvu-buffer 'annot)]
["Remove Linebreaks" djvu-remove-linebreaks-internal (eq djvu-buffer 'annot)]
"---"
["Split Word" djvu-split-word-internal (eq djvu-buffer 'text)]
["Merge Words" djvu-merge-words-internal (eq djvu-buffer 'text)]
["Merge Lines" djvu-merge-lines-internal (eq djvu-buffer 'text)]
"---"
["Quit Djvu" djvu-quit-window t]
["Kill Djvu buffers" djvu-kill-doc t]))
(defvar djvu-font-lock-keywords
`((,(concat "^[ \t]*("
(regexp-opt '("background" "zoom" "mode" "align"
"maparea" "metadata" "bookmarks" "xmp")
t))
1 font-lock-keyword-face)
(,(concat "\\(?:[ \t]+\\|^\\|(\\)("
(regexp-opt '("url" "rect" "oval" "poly" "text" "line"
"none" "xor" "border" "shadow_in"
"shadow_out" "shadow_ein" "shadow_eout"
"border_avis" "hilite" "opacity"
"arrow" "width" "lineclr"
"backclr" "textclr" "pushpin"
"page" "column" "region" "para" "line"
"word" "char")
t)
")")
1 font-lock-function-name-face)
(djvu-font-lock-url))
"Font lock keywords for Djvu buffers.")
(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."
(if (not djvu-init)
(djvu-init-mode) (setq buffer-auto-save-file-name nil 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)))
(define-key km "v" 'djvu-view-page)
(define-key km "\C-c\C-v" 'djvu-view-page)
(define-key km "n" 'djvu-next-page)
(define-key km "p" 'djvu-prev-page)
(define-key km "g" 'djvu-goto-page)
(define-key km "k" 'djvu-kill-doc)
(define-key km "s" 'djvu-save)
(define-key km "\C-x\C-s" 'djvu-save)
(define-key km "q" 'djvu-quit-window)
(define-key km "G" 'djvu-revert-buffer)
(define-key km (kbd "C-c C-S-g") 'djvu-revert-buffer)
(define-key km "a" 'djvu-switch-annot)
(define-key km "\C-c\C-a" 'djvu-switch-annot)
(define-key km "S" 'djvu-switch-shared)
(define-key km (kbd "C-c C-S-S") 'djvu-switch-shared)
(define-key km "b" 'djvu-switch-bookmarks)
(define-key km "\C-c\C-b" 'djvu-switch-bookmarks)
(define-key km "t" 'djvu-switch-text)
(define-key km "\C-c\C-t" 'djvu-switch-text)
(define-key km "r" 'djvu-switch-read)
(define-key km "\C-c\C-r" 'djvu-switch-read)
km)
"Keymap for Djvu Outline Mode.
This is a child of `special-mode-map'.")
(easy-menu-define
djvu-outline-menu djvu-outline-mode-map "Djvu Menu"
'("Djvu"
["Djview File" djvu-view-page t]
["Go to Page" djvu-goto-page t]
["Save Doc" djvu-save t]
["Revert Doc" djvu-revert-buffer t]
"---"
["Switch to Read" djvu-switch-read t]
["Switch to Text" djvu-switch-text t]
["Switch to Annotations" djvu-switch-annot t]
["Switch to Shared Annotations" djvu-switch-shared t]
["Switch to Bookmarks" djvu-switch-bookmarks t]
"---"
["Quit Viewing" djvu-quit-window t]
["Kill Djvu buffers" djvu-kill-doc t]))
(define-derived-mode djvu-outline-mode special-mode "Djvu OL"
"Mode for reading the outline of Djvu files."
(if (not djvu-init)
(djvu-init-mode) (setq buffer-auto-save-file-name nil 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)))
(add-to-list 'auto-mode-alist '("\\.djvu\\'" . djvu-init-mode))
(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))
(defun djvu-read-file-name ()
"Read file name of Djvu file.
The numeric value of `current-prefix-arg' is the page number."
(let ((page (prefix-numeric-value current-prefix-arg)))
(list (read-file-name "Find Djvu file: " nil nil t nil
(lambda (f)
(or (equal "djvu" (file-name-extension f))
(file-directory-p f))))
page)))
(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))
(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))
(with-temp-buffer
(insert-file-contents-literally file nil 0 4)
(goto-char (point-min))
(unless (looking-at "\\`AT&T") (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)))
(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))
(when (and old-bufs (memq nil old-bufs))
(message "Killing dangling Djvu buffers...")
(with-current-buffer doc
(djvu-kill-doc-all))
(setq old-bufs nil)
(message "Killing dangling Djvu buffers...Done")
(sit-for 2))
(unless (and old-bufs
(or (and (equal file-number
(buffer-local-value 'buffer-file-number doc))
(verify-visited-file-modtime doc))
(not (or noconfirm
(yes-or-no-p
(format "Revert buffer from file %s? "
(djvu-ref file doc)))))))
(unless old-bufs
(cl-flet ((fun (n)
(create-file-buffer (expand-file-name
(concat buf-basename
(nth n djvu-buffer-name-extensions))
dir))))
(if doc
(with-current-buffer doc
(let ((inhibit-read-only t)
(buffer-undo-list t))
(erase-buffer))
(setq buffer-file-coding-system 'prefer-utf-8)
(rename-buffer (concat buf-basename
(nth 0 djvu-buffer-name-extensions))
t))
(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)))
(djvu-set file file doc)
(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 buffer-file-name file
buffer-file-truename file-truename
buffer-file-number file-number
default-directory dir)
(setq buffer-file-read-only read-only
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 'change-major-mode-hook #'djvu-change-major-mode nil t))
(with-temp-buffer
(djvu-djvused doc t "-e"
"create-shared-ant; print-ant; n; ls; print-outline;")
(goto-char (point-min))
(save-restriction
(narrow-to-region
(point)
(save-excursion
(while (progn (skip-chars-forward " \t\n")
(looking-at "("))
(forward-sexp))
(point)))
(djvu-init-annot (djvu-ref shared-buf doc) doc t))
(djvu-set pagemax (read (current-buffer)) doc)
(let ((regexp (concat "\\(?:\\([0-9]+\\)[ \t]+\\)?" "\\([PIAT]\\)[ \t]+" "\\([0-9]+\\)[ \t]+" "\\([^=\n]+\\)"
"\\(?:[ \t]+T=[^\t\n]+\\)?" "$")) page-id)
(while (progn (skip-chars-forward " \t\n")
(looking-at regexp))
(if (match-string 1)
(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))
(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)
(erase-buffer)
(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)))
(defun djvu-revert-buffer (&optional _ignore-auto noconfirm _preserve-modes)
"Revert buffers for the current Djvu document.
Note: Djvu mode never visits the Djvu document in the usual Emacs sense of
copying the contents of the file into a buffer to manipulate it. Instead,
djvu mode completely relies on djvused and the file on disk. Therefore,
we are in trouble if the file on disk happens to be not in sync anymore
with what Djvu mode believes it is. Reverting the buffers for the Djvu
document is usually the only way out."
(interactive)
(unless djvu-doc (user-error "No djvu-doc"))
(with-current-buffer djvu-doc
(setq buffer-file-number nil))
(djvu-find-file (djvu-ref file djvu-doc)
(djvu-ref page djvu-doc) nil nil noconfirm))
(defun djvu-init-page (&optional page doc)
"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))
(if (or (buffer-modified-p (djvu-ref text-buf doc))
(buffer-modified-p (djvu-ref annot-buf doc)))
(djvu-save doc t))
(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)
(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))
(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)
(error "No page size"))
(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)))))
(save-restriction
(narrow-to-region (point) (point-max))
(djvu-init-annot (djvu-ref annot-buf doc) doc))
(djvu-init-text object doc t)
(djvu-init-read object doc t)))))
(defalias 'djvu-goto-page 'djvu-init-page
"Goto PAGE of Djvu document DOC.")
(defsubst djvu-unresolve-url (url)
"Unresolve internal URL.
This issues a warning message if URL cannot be resolved.
Yet it does not throw an error that would put the Djvu document
into an undefined state."
(message "Warning: Page id `%s' broken" url)
(sit-for 1)
url)
(defun djvu-resolve-url (url &optional doc)
"Resolve internal URLs. See variable `djvu-resolve-url'."
(cond ((eq 'long djvu-resolve-url)
(cond ((string-match "\\`#[0-9]+\\'" url)
(let ((page-id (assq (djvu-substring-number url 1)
(djvu-ref page-id doc))))
(if page-id
(format "#%s" (cdr page-id))
(djvu-unresolve-url url))))
((string-match "\\`#" url)
(if (rassoc (substring-no-properties url 1)
(djvu-ref page-id doc))
url
(djvu-unresolve-url url)))
(t url))) ((eq 'short djvu-resolve-url)
(cond ((string-match "\\`#[0-9]+\\'" url)
(if (assq (djvu-substring-number url 1)
(djvu-ref page-id doc))
url
(djvu-unresolve-url url)))
((string-match "\\`#" url)
(let ((page-id (rassoc (substring-no-properties url 1)
(djvu-ref page-id doc))))
(if page-id
(format "#%d" (car page-id))
(djvu-unresolve-url url))))
(t "#1"))) (t (cond ((string-match "\\`#[0-9]+\\'" url)
(if (assq (djvu-substring-number url 1)
(djvu-ref page-id doc))
url
(djvu-unresolve-url url)))
((string-match "\\`#" url)
(if (rassoc (substring-no-properties url 1)
(djvu-ref page-id doc))
url
(djvu-unresolve-url url)))
(t url)))))
(defun djvu-resolve-all-urls (dir &optional doc)
"Resolve all internal URLs in Djvu document DOC."
(interactive
(list (intern (completing-read "Direction: " '((long) (short)) nil t))))
(unless doc (setq doc djvu-doc))
(djvu-save doc t)
(unless (eq dir (djvu-ref resolve-url doc))
(if (djvu-modified) (user-error "Djvu file should be saved"))
(with-temp-buffer
(let ((page-id (djvu-ref page-id doc))
(djvu-resolve-url dir))
(djvu-annot-script doc t)
(goto-char (point-min))
(while (re-search-forward "^(maparea[ \t]+\"#\\(\\([0-9]+\\)\\|[^\"]*[^0-9\"][^\"]*\\)\"" nil t)
(let* ((url (match-string 1))
(num (and (match-string 2)
(djvu-match-number 2)))
repl)
(cond ((eq dir 'long)
(if num
(if (setq repl (cdr (assq num page-id)))
(replace-match repl nil nil nil 1)
(djvu-unresolve-url url))
(unless (rassoc url page-id)
(djvu-unresolve-url url))))
(num
(unless (assq num page-id)
(djvu-unresolve-url url)))
((setq repl (car (rassoc url page-id)))
(replace-match (number-to-string repl) nil nil nil 1))
(t (djvu-unresolve-url url)))))
(djvu-process-script doc t)))
(with-temp-buffer
(djvu-djvused doc t "-e"
(format "select %d; print-ant;" (djvu-ref page doc)))
(djvu-init-annot (djvu-ref annot-buf doc) doc))
(let ((object (djvu-read-bookmarks doc))
(djvu-resolve-url dir))
(when object
(with-current-buffer (djvu-ref bookmarks-buf doc)
(erase-buffer)
(insert "(bookmarks")
(djvu-insert-bookmarks (cdr object) " ")
(insert ")\n"))
(djvu-save doc)))
(djvu-set resolve-url dir doc)))
(defun djvu-area (area &optional back)
"Convert (area xmin ymin width height) to (area xmin ymin xmax ymax).
If BACK is non-nil do inverse transformation."
(if back
(let ((lst (list (nth 0 area) (nth 1 area) (nth 2 area)
(- (nth 3 area) (nth 1 area))
(- (nth 4 area) (nth 2 area)))))
(if (or (> 0 (nth 3 lst)) (> 0 (nth 4 lst)))
(message "Annotation area dimensions %s, %s"
(nth 3 lst) (nth 4 lst)))
lst)
(list (nth 0 area) (nth 1 area) (nth 2 area)
(+ (nth 3 area) (nth 1 area))
(+ (nth 4 area) (nth 2 area)))))
(defun djvu-view (&optional doc new)
"(Re)Start Djview for DOC.
If prefix NEW is non-nil, always create a new Djview process."
(interactive (list nil current-prefix-arg))
(if (not (display-graphic-p))
(message "No graphic display available")
(let* ((doc (or doc djvu-doc)) (dpos (djvu-mean-dpos (djvu-read-dpos nil doc)))
(px (/ (float (nth 0 dpos))
(float (car (djvu-ref pagesize doc)))))
(py (- 1 (/ (float (nth 1 dpos))
(float (cdr (djvu-ref pagesize doc))))))
process-connection-type) (if (or (< px 0) (< 1 px) (< py 0) (< 1 py))
(error "px=%s, py=%s out of range" px py))
(unless new (djvu-kill-view doc))
(let ((process (apply 'start-process
"djview" nil djvu-djview-command
(format "-page=%s" (cdr (assq (djvu-ref page doc)
(djvu-ref page-id doc))))
(format "-showposition=%06f,%06f" px py)
(append djvu-djview-options (list (djvu-ref file doc))))))
(set-process-sentinel
process
`(lambda (process event)
(when (string-match "^\\(?:finished\\|exited\\|quit\\|killed\\|terminated\\)" event)
(if (buffer-live-p ,doc)
(djvu-set view-proc (delq process (djvu-ref view-proc ,doc))
,doc))
(message "%s %s: %s" process
,(abbreviate-file-name (djvu-ref file doc))
event))))
(djvu-set view-proc (cons process (djvu-ref view-proc doc)) doc)))))
(defun djvu-view-page (page &optional doc new)
"(Re)Start Djview on PAGE for DOC.
If prefix NEW is non-nil, always create a new Djview process."
(interactive (list (if (eq 'outline djvu-buffer)
(djvu-outline-page)
(djvu-read-page))
nil current-prefix-arg))
(djvu-goto-page page doc)
(djvu-view doc new))
(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)
(let ((doc djvu-doc))
(while (not (or (re-search-forward regexp nil t)
(eq (djvu-ref page doc) (djvu-ref pagemax doc))))
(djvu-next-page 1))))
(defun djvu-re-search-forward-continue ()
"Continue search forward for match for `djvu-last-search-re'."
(interactive)
(djvu-re-search-forward djvu-last-search-re))
(defun djvu-edit-word (bpos)
"Edit word at buffer position BPOS."
(interactive "d")
(let* ((old (buffer-substring-no-properties
(djvu-property-beg bpos 'word)
(djvu-property-end bpos 'word)))
(new (read-string (format "Replace `%s' with: " old) old
nil nil djvu-inherit-input-method))
(dpos (djvu-read-dpos bpos)))
(with-current-buffer (djvu-ref text-buf)
(unless (and (djvu-goto-dpos 'word dpos)
(progn
(backward-char)
(looking-at (regexp-quote (prin1-to-string old)))))
(error "`%s' not found" old))
(replace-match (prin1-to-string new) t t)))
(djvu-save-text))
(defun djvu-split-word (bpos)
"Split word at buffer position BPOS.
This command operates on the read buffer."
(interactive "d")
(let ((beg (djvu-property-beg bpos 'word))
(dpos (djvu-read-dpos bpos)))
(with-current-buffer (djvu-ref text-buf)
(djvu-split-word-internal (djvu-goto-dpos 'word dpos)
(- bpos beg))))
(djvu-save-text))
(defun djvu-split-word-internal (wpos split)
"Split word at position WPOS at character position SPLIT.
This command operates on the text buffer."
(interactive
(let* ((pnt (point))
(pps (parse-partial-sexp (line-beginning-position) pnt)))
(unless (nth 3 pps) (user-error "Point not inside string"))
(list pnt (1- (- pnt (nth 8 pps))))))
(goto-char wpos)
(beginning-of-line)
(skip-chars-forward " \t")
(setq wpos (point))
(let ((indent (buffer-substring-no-properties
(line-beginning-position) wpos))
word)
(condition-case nil
(progn
(setq word (read (current-buffer)))
(unless (eq 'word (car word)) (error "Invalid")))
(error (error "Syntax error in raw text")))
(if (or (< split 1) (<= (length (nth 5 word)) split))
(error "Nothing to split"))
(delete-region wpos (point))
(let ((frac (round (* (/ (float split) (length (nth 5 word)))
(- (nth 3 word) (nth 1 word))))))
(djvu-insert-text (list 'word (nth 1 word) (nth 2 word)
(+ (nth 1 word) frac) (nth 4 word)
(substring (nth 5 word) 0 split)) "")
(insert "\n" indent)
(djvu-insert-text (list 'word (+ (nth 1 word) frac 1) (nth 2 word)
(nth 3 word) (nth 4 word)
(substring (nth 5 word) split)) ""))))
(defun djvu-merge-words (beg end)
"Merge words between positions BEG and END.
This command operates on the read buffer."
(interactive "r")
(let ((bpos (djvu-read-dpos beg))
(epos (djvu-read-dpos (1- end))))
(with-current-buffer (djvu-ref text-buf)
(djvu-merge-words-internal (djvu-goto-dpos 'word bpos)
(djvu-goto-dpos 'word epos))))
(djvu-save-text))
(defun djvu-merge-words-internal (beg end)
"Merge words between positions BEG and END.
This command operates on the text buffer."
(interactive "r")
(let (words)
(goto-char end)
(if (bolp) (setq end (1- end)))
(goto-char beg)
(beginning-of-line)
(skip-chars-forward " \t")
(setq beg (point))
(while (< (point) end)
(push (read (current-buffer)) words)
(unless (eq 'word (caar words))
(error "Syntax error in raw text")))
(delete-region beg (point))
(let ((object (apply 'list 'word 0 0 0 0 (nreverse words))))
(djvu-text-zone object 0 (make-vector 3 nil))
(setcdr (nthcdr 4 object) (list (mapconcat (lambda (w) (nth 5 w))
(nthcdr 5 object) "")))
(djvu-insert-text object "")))
(undo-boundary))
(defun djvu-merge-lines (beg end)
"Merge lines between positions BEG and END.
This command operates on the read buffer."
(interactive "r")
(let ((bpos (djvu-read-dpos beg))
(epos (djvu-read-dpos (1- end))))
(with-current-buffer (djvu-ref text-buf)
(djvu-merge-lines-internal (djvu-goto-dpos 'word bpos)
(djvu-goto-dpos 'word epos))))
(djvu-save-text))
(defun djvu-merge-lines-internal (beg end)
"Merge lines between positions BEG and END.
This command operates on the text buffer."
(interactive "r")
(goto-char end)
(unless (looking-at "[ \t]*(word ")
(re-search-backward "^[ \t]*(word "))
(forward-sexp)
(setq end (point))
(goto-char beg)
(unless (looking-at "[ \t]*(word ")
(re-search-backward "^[ \t]*(word "))
(skip-chars-forward " \t")
(setq beg (point))
(unless (< beg end) (user-error "Nothing to merge"))
(atomic-change-group
(save-restriction
(narrow-to-region beg end)
(mapc (lambda (zone)
(goto-char (point-min))
(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"))
(goto-char (point-min))
(while (> (point-max) (progn (skip-chars-forward "\n\t\s") (point)))
(if (looking-at "(word ")
(forward-sexp) (error "Syntax error: cannot merge"))))))
(defun djvu-init-text (object &optional doc reset)
"Initialize Text buffer."
(with-current-buffer (djvu-ref text-buf doc)
(let ((dpos (unless reset (djvu-text-dpos nil doc)))
buffer-read-only)
(erase-buffer)
(djvu-insert-text object "")
(insert "\n")
(if (not reset)
(djvu-goto-dpos 'word dpos)
(goto-char (point-min))
(set-buffer-modified-p nil)
(setq buffer-undo-list nil)))))
(defun djvu-insert-text (object indent)
"Insert OBJECT into Djvu text buffer recursively using indentation INDENT."
(when object
(insert indent "("
(mapconcat #'prin1-to-string
(list (nth 0 object) (nth 1 object) (nth 2 object)
(nth 3 object) (nth 4 object))
" "))
(let ((tail (nthcdr 5 object))
(indent (concat indent " ")))
(if (stringp (car tail))
(insert (format " %S)" (car tail)))
(dolist (elt tail)
(insert "\n")
(djvu-insert-text elt indent))
(insert ")")))))
(defun djvu-text-dpos (&optional point doc)
"Return Djvu position of POINT in Djvu text buffer."
(with-current-buffer (djvu-ref text-buf doc)
(save-excursion
(if point (goto-char point))
(beginning-of-line)
(let (zone)
(while (not (or (setq zone (looking-at djvu-zone-re))
(bobp)))
(forward-line -1))
(if zone
(mapcar #'djvu-match-number '(3 4 5 6)))))))
(defun djvu-read-text (&optional doc)
"Read text of a Djvu document from text buffer."
(let (object)
(with-current-buffer (djvu-ref text-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 raw text")))
(skip-chars-forward " \t\n")
(unless (eobp)
(error "Syntax error in raw text (end of buffer)"))))))
object))
(defun djvu-save-text (&optional doc script)
"Save text of the Djvu document DOC. This updates the Read buffer for DOC.
DOC defaults to the current Djvu document.
If SCRIPT is non-nil, dump the text buffer into the djvused script file SCRIPT."
(interactive)
(unless doc (setq doc djvu-doc))
(let ((object1 (djvu-read-text doc))
(object2 (djvu-read-text doc))) (djvu-text-zone object1 0 (make-vector 7 nil))
(unless (equal object1 object2)
(djvu-init-text object1 doc))
(djvu-init-read object1 doc)
(if script
(with-temp-buffer
(setq buffer-file-coding-system 'utf-8)
(insert (format "select %d\nremove-txt\nset-txt\n"
(djvu-ref page doc)))
(djvu-insert-text object1 "")
(insert "\n.\n") (write-region nil nil script t 0)))))
(defun djvu-text-zone (object depth zones)
"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))
(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)
(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)
(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.
t means current buffer, nil means `djvu-script-buffer'."
(if (eq t buffer)
(current-buffer)
(get-buffer-create (or buffer djvu-script-buffer))))
(defun djvu-text-script (&optional doc buffer page display)
"Create djvused script for complete text layer of DOC in BUFFER.
If prefix PAGE is non-nil create instead script for only page PAGE.
BUFFER defaults to `djvu-script-buffer'. If BUFFER is t use current buffer.
You can edit the text script in BUFFER. Afterwards you can re-apply
this script using `djvu-process-script'. This code will not (cannot) check
whether the edited script is meaningful for DOC. Use at your own risk.
You get what you want."
(interactive (list nil nil (if current-prefix-arg (djvu-ref page)) t))
(let ((doc (or doc djvu-doc))
(buffer (djvu-script-buffer buffer)))
(djvu-save doc t)
(with-current-buffer buffer
(let ((buffer-undo-list t)
(djvu-init t)
buffer-read-only)
(djvu-script-mode)
(erase-buffer)
(if page (insert (format "select \"%s\" # page %d\n"
(cdr (assq page (djvu-ref page-id doc)))
page)))
(djvu-djvused doc t "-e" (format "select %s; output-txt;"
(or page "")))
(goto-char (point-min)))
(set-buffer-modified-p nil)
(setq buffer-undo-list nil))
(if display (switch-to-buffer buffer))))
(defun djvu-process-script (&optional doc buffer obey-restrictions)
"For Djvu document DOC apply the djvused script in BUFFER.
Use at your own risk. You get what you want. This code does not (cannot)
check whether the script is meaningful. Unless prefix OBEY-RESTRICTIONS
is non-nil, throw an error if BUFFER is narrowed.
DOC defaults to the current Djvu document.
BUFFER defaults to `djvu-script-buffer'. If BUFFER is t, use current buffer."
(interactive (list nil nil current-prefix-arg))
(let ((doc (or doc djvu-doc)))
(unless doc (user-error "No Djvu doc"))
(djvu-save doc t)
(djvu-with-temp-file script
(with-current-buffer (djvu-script-buffer buffer)
(unless (or obey-restrictions
(equal (save-restriction (widen)
(- (point-max) (point-min)))
(- (point-max) (point-min))))
(user-error "Script buffer narrowed"))
(let ((buffer-file-coding-system 'utf-8))
(write-region nil nil script nil 0)))
(djvu-djvused doc nil "-f" script "-s"))
(djvu-init-page nil doc)))
(defun djvu-init-read (object &optional doc reset)
(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)
(if djvu-image-mode (djvu-image))))
(defun djvu-insert-read (object)
"Display text OBJECT recursively."
(let ((opoint (point))
(tail (nthcdr 5 object)))
(if (stringp (car tail))
(progn
(insert (car tail))
(if djvu-rect-list
(let ( (rect-list (if djvu-last-rect
(cons (nthcdr 2 djvu-last-rect) djvu-rect-list)
djvu-rect-list))
(x (/ (+ (nth 1 object) (nth 3 object)) 2))
(y (/ (+ (nth 2 object) (nth 4 object)) 2))
rect coords found)
(while (setq rect (pop rect-list))
(setq coords (car rect))
(when (and (< (nth 0 coords) x (nth 2 coords))
(< (nth 1 coords) y (nth 3 coords)))
(setq rect-list nil found t)
(if (equal (cdr rect) (nthcdr 3 djvu-last-rect))
(setq djvu-last-rect
(cons (car djvu-last-rect) (cons (point) rect)))
(djvu-insert-read-prop)
(setq djvu-last-rect
(cons opoint (cons (point) rect))))))
(unless found
(djvu-insert-read-prop)))))
(let* ((obj (caar tail))
(sep (cond ((eq 'line obj) "\n")
((eq 'word obj) "\s")
((eq 'char obj) nil)
(t "\n\n")))
elt)
(while (setq elt (pop tail))
(djvu-insert-read elt)
(if (and sep tail) (insert sep)))))
(put-text-property opoint (point) (car object)
(vector (nth 1 object) (nth 2 object)
(nth 3 object) (nth 4 object)))))
(defun djvu-insert-read-prop ()
"Propertize Read buffer according to annotations."
(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)
: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)
(save-excursion
(goto-char beg)
(re-search-forward "\n+" end t djvu-read-prop-newline))))
(add-text-properties beg end face)
(save-excursion
(goto-char beg)
(while (re-search-forward "[^\n]+" end t)
(add-text-properties (match-beginning 0) (match-end 0) face))))
(unless (equal "" (nth 3 djvu-last-rect))
(make-button beg end :type 'djvu-url
'help-echo (format "mouse-2, RET: url `%s'%s"
(nth 3 djvu-last-rect)
(if (equal "" (nth 4 djvu-last-rect))
""
(format "\n%s" (nth 4 djvu-last-rect))))
'djvu-args (list (nth 3 djvu-last-rect)))))
(setq djvu-last-rect nil)))
(defun djvu-read-dpos (&optional point doc)
"Return Djvu position of POINT in Djvu Read buffer.
This is either a list (XMIN YMIN XMAX YMAX) or (X Y)."
(with-current-buffer (djvu-ref read-buf doc)
(cond ((and djvu-image-mode
(djvu-ref read-pos doc)))
((= (point-min) (point-max))
(list (/ (car (djvu-ref pagesize doc)) 2)
(/ (cdr (djvu-ref pagesize doc)) 2)))
(t
(unless point
(setq point (point)))
(or (get-text-property point 'word)
(and (< 1 point)
(get-text-property (1- point) 'word))
(let ((pos (previous-single-property-change
point 'word)))
(and pos (get-text-property (1- pos) 'word)))
(list (/ (car (djvu-ref pagesize doc)) 2)
(/ (cdr (djvu-ref pagesize doc)) 2)))))))
(defun djvu-mean-dpos (dpos)
"For Djvu position DPOS return mean coordinates (X Y).
DPOS is a list or vector (XMIN YMIN XMAX YMAX)."
(if (elt dpos 2)
(list (/ (+ (elt dpos 0) (elt dpos 2)) 2)
(/ (+ (elt dpos 1) (elt dpos 3)) 2))
dpos))
(defsubst djvu-dist (width height)
(+ (* width width) (* height height)))
(defun djvu-goto-dpos (object dpos)
"Go to OBJECT at position DPOS in the text or annotation buffer.
If found, return corresponding buffer position.
Otherwise, do nothing and return nil."
(cond ((not dpos) nil)
((elt dpos 2) (goto-char (point-min))
(or (re-search-forward (format "\\<%s\\>[ \t\n]+%s\\([ \t\n]+\"\\)?"
object
(mapconcat #'number-to-string dpos
"[ \t\n]+"))
nil t)
(djvu-goto-dpos object (djvu-mean-dpos dpos))))
(t (let* ((re (format "\\<%s\\>[ \t\n]+%s\\([ \t\n]+\"\\)?"
object
(mapconcat #'identity
(make-list 4 "\\([[:digit:]]+\\)")
"[ \t\n]+")))
(x (nth 0 dpos)) (y (nth 1 dpos))
(x2 (- (* 2 x))) (y2 (- (* 2 y)))
(good-dist (* 4 (djvu-dist (car (djvu-ref pagesize))
(cdr (djvu-ref pagesize)))))
(good-pnt (point-min))
pnt dist)
(goto-char (point-min))
(while (and (not (zerop good-dist))
(setq pnt (re-search-forward re nil t)))
(let ((xmin (djvu-match-number 1)) (ymin (djvu-match-number 2))
(xmax (djvu-match-number 3)) (ymax (djvu-match-number 4)))
(if (and (<= xmin x xmax) (<= ymin y ymax))
(setq good-dist 0 good-pnt pnt) (setq dist (djvu-dist (+ xmin xmax x2) (+ ymin ymax y2)))
(if (< dist good-dist)
(setq good-pnt pnt good-dist dist))))) (goto-char good-pnt)
(if (/= good-pnt (point-min)) good-pnt)))))
(defun djvu-goto-read (&optional dpos)
"Go to buffer position in Read buffer corresponding to Djvu position DPOS.
Return corresponding buffer position."
(with-current-buffer (djvu-ref read-buf)
(cond (djvu-image-mode
(djvu-set read-pos dpos)
(point-min))
((not dpos) nil)
((elt dpos 2) (let ((pnt (point-min))
(xmin (elt dpos 0)) (ymin (elt dpos 1))
(xmax (elt dpos 2)) (ymax (elt dpos 3))
word done)
(goto-char (point-min))
(while (progn (setq done
(and (setq word (djvu-mean-dpos
(get-text-property pnt 'word)))
(<= xmin (nth 0 word) xmax)
(<= ymin (nth 1 word) ymax)))
(and (not done)
(setq pnt (next-single-property-change pnt 'word)))))
(if done
(goto-char pnt)
(djvu-goto-read (djvu-mean-dpos dpos)))))
(t (let* ((x (nth 0 dpos)) (y (nth 1 dpos))
(x2 (- (* 2 x))) (y2 (- (* 2 y)))
(good-dist (* 4 (djvu-dist (car (djvu-ref pagesize))
(cdr (djvu-ref pagesize)))))
(pnt (point-min)) (good-pnt (point-min))
word dist)
(goto-char (point-min))
(while (progn (when (setq word (get-text-property pnt 'word))
(if (and (<= (aref word 0) x (aref word 2))
(<= (aref word 1) y (aref word 3)))
(setq good-dist 0 good-pnt pnt) (setq dist (djvu-dist (+ (aref word 0) (aref word 2) x2)
(+ (aref word 1) (aref word 3) y2)))
(if (< dist good-dist)
(setq good-pnt pnt good-dist dist)))) (and (not (zerop good-dist))
(setq pnt (next-single-property-change pnt 'word)))))
(goto-char good-pnt)
(if (/= good-pnt (point-min)) good-pnt))))))
(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."
(djvu-convert-hash)
(goto-char (point-min))
(let (object alist)
(while (progn (skip-chars-forward " \t\n") (not (eobp)))
(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)))))
(dolist (elt object)
(if (not (eq 'maparea (car elt)))
(push elt alist)
(cond ((eq 'rect (car (nth 3 elt))) (let ((area (djvu-area (nth 3 elt)))
e)
(setcdr (nthcdr 2 elt) (nthcdr 4 elt))
(if (or (string= "" (nth 2 elt))
(not (setq e (assoc elt alist))))
(push (cons elt (list area)) alist)
(setcdr e (cons area (cdr e))))))
((memq (car (nth 3 elt)) '(text oval)) (setcar (nthcdr 3 elt) (djvu-area (nth 3 elt)))
(push elt alist))
(t (push elt alist)))))
(unless shared
(let ((id 0) rect-list)
(dolist (elt alist)
(when (consp (car elt)) (setq id (1+ id))
(push (djvu-rect-elt elt id) rect-list)))
(djvu-set rect-list (apply 'nconc rect-list) doc)))
(with-current-buffer buf
(let ((standard-output (current-buffer))
buffer-read-only)
(erase-buffer)
(dolist (elt alist)
(cond ((consp (car elt)) (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 " (mapconcat #'prin1-to-string (nthcdr 3 c) " ") ")")))
((eq 'metadata (car elt)) (insert "(metadata")
(dolist (entry (cdr elt))
(insert (format "\n (%s %S)" (car entry) (cadr entry))))
(insert ")"))
((not (eq 'maparea (car elt))) (prin1 elt))
((memq (car (nth 3 elt)) '(text oval line poly)) (insert (format "(maparea %S\n %S\n " (nth 1 elt) (nth 2 elt))
(mapconcat #'prin1-to-string (nthcdr 3 elt) " ") ")"))
(t (error "Djvu maparea %s undefined" (car (nth 3 elt)))))
(insert "\n\n"))
(djvu-convert-hash t))
(goto-char (point-min))
(set-buffer-modified-p nil)
(setq buffer-undo-list nil))))
(defun djvu-rect-elt (rect id)
"For rect RECT define entry for `djvu-rect-list' using ID."
(let* ((maparea (car rect))
(url (nth 1 maparea))
(text (nth 2 maparea))
(color (djvu-color-background
(cond ((nth 1 (assoc 'hilite maparea))) (url djvu-color-url) (t djvu-color-highlight))
nil
(or (nth 1 (assoc 'opacity maparea))
djvu-opacity))))
(mapcar (lambda (area)
(list (cdr area) url text color id))
(cdr rect))))
(defun djvu-font-lock-url (bound)
(let ((pnt (point)) case-fold-search found beg end)
(cond ((memq djvu-buffer '(annot shared))
(beginning-of-line)
(while (and (not found)
(re-search-forward "^[ \t]*(maparea" bound t))
(setq found (and (looking-at "[ \t]*\"")
(progn
(setq beg (match-end 0))
(>= beg pnt))
(<= beg bound)
(progn
(forward-sexp)
(setq end (1- (point))))))))
((eq djvu-buffer 'bookmarks)
(re-search-backward "^[ \t]*(" nil t)
(while (and (not found)
(re-search-forward "^[ \t]*(" bound t))
(setq found (and (looking-at "[ \t]*\"")
(progn
(forward-sexp)
(looking-at "[ \t\n]+\""))
(progn
(setq beg (match-end 0))
(>= beg pnt))
(<= beg bound)
(progn
(forward-sexp)
(setq end (1- (point)))))))))
(when found
(remove-text-properties beg end '(face))
(make-text-button beg end 'type 'djvu-url
'djvu-args (list (buffer-substring-no-properties beg end))))
found))
(defun djvu-button-action (button)
"Call BUTTON's Djvu function."
(apply (button-get button 'djvu-function)
(button-get button 'djvu-args)))
(define-button-type 'djvu-url
'action 'djvu-button-action
'djvu-function 'djvu-url
'help-echo "mouse-2, RET: follow URL")
(defun djvu-url (url)
"Browse URL in Djvu document.
If URL is an internal url, go to that page."
(if (string-match "\\`#" url)
(let* ((page-url (substring url 1))
(page (or (car (rassoc page-url (djvu-ref page-id)))
(and (string-match "\\`[0-9]+\\'" page-url)
(string-to-number page-url)))))
(unless page (error "Invalid internal url `%s'" page-url))
(djvu-goto-page page)
(djvu-switch-read))
(browse-url url)))
(defun djvu-interactive-text-area (&optional border backclr textclr pushpin)
"Interactive spec for `djvu-text-area' and friends."
(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: " (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)))
border
(or backclr (djvu-color-background color))
textclr pushpin)))
(defsubst djvu-insert-color (key color)
(if color
(format " (%s %s)" key
(cond ((string-match "\\`#" color) color)
((cdr (assoc color djvu-color-alist)))
(t (error "Color `%s' undefined" color))))
""))
(defun djvu-bound-area (area &optional doc)
"Restrict AREA to page boundaries."
(unless doc (setq doc djvu-doc))
(list (max 1 (min (elt area 0) (car (djvu-ref pagesize doc))))
(max 1 (min (elt area 1) (cdr (djvu-ref pagesize doc))))
(max 1 (min (elt area 2) (car (djvu-ref pagesize doc))))
(max 1 (min (elt area 3) (cdr (djvu-ref pagesize doc))))))
(defun djvu-text-area (url comment area
&optional border backclr textclr pushpin)
"Using strings URL and COMMENT, highlight AREA.
This defines a text maparea for djvused.
AREA is a list (XMIN YMIN XMAX YMAX).
Interactively, the command `djvu-mouse-text-area' in `djvu-image-mode'
is usually easier to use."
(interactive (djvu-interactive-text-area))
(setq area (djvu-bound-area area))
(with-current-buffer (djvu-ref annot-buf)
(goto-char (point-max))
(insert (format "(maparea %S\n %S\n "
(or url "") (if comment (djvu-fill comment) ""))
(apply 'format "(text %d %d %d %d)" area)
(format " (%s)" (or border 'none))
(if backclr (djvu-insert-color "backclr" backclr) "")
(if textclr (djvu-insert-color "textclr" textclr) "")
(if pushpin " (pushpin)" "")
")\n\n")
(undo-boundary)))
(defun djvu-text-area-pushpin (url comment area
&optional border backclr textclr pushpin)
"Using URL and COMMENT, highlight AREA as pushpin.
This defines a rect area for djvused.
Interactively, the command `djvu-mouse-text-area-pushpin' in `djvu-image-mode'
is usually easier to use."
(interactive (djvu-interactive-text-area nil nil nil t))
(djvu-text-area url comment area border backclr textclr pushpin))
(defun djvu-mark-line-beg (pnt comment &optional left color)
"Mark word at beginning of line.
With prefix LEFT mark left of beginning of line."
(interactive
(let ((prefix current-prefix-arg))
(list (line-beginning-position)
(read-string (format "(%s) %sMarker comment: "
djvu-color-himark
(if prefix "left " ""))
nil nil nil djvu-inherit-input-method)
prefix djvu-color-himark)))
(let* ((zone (get-text-property pnt 'word))
(height (- (aref zone 3) (aref zone 1)))
(xmin (- (aref zone 0) (round (* 2.5 height)))))
(if left
(djvu-text-area nil comment
(list xmin (aref zone 1)
(- (aref zone 0) (/ height 2)) (aref zone 3))
nil
(djvu-color-background color))
(djvu-rect-area nil comment
`((,xmin ,(aref zone 1) ,(aref zone 2) ,(aref zone 3)))
color djvu-opacity))))
(defun djvu-himark (bookmark url beg end comment
&optional level color opacity border)
"Bookmark and highlight the region between BEG and END."
(interactive
(djvu-with-region region
(let ((level (djvu-interactive-bookmark-level)) (bookmark (djvu-read-string "Bookmark: " region t)))
(list bookmark (djvu-ref page) (car region) (cdr region) bookmark level
djvu-color-himark djvu-opacity nil))))
(djvu-bookmark bookmark url level)
(djvu-rect-region beg end nil comment color opacity border))
(defun djvu-update-url (url &optional color opacity border)
"Update URL"
(interactive
(let* ((color (djvu-interactive-color djvu-color-url))
(url (djvu-interactive-url color)))
(list url color djvu-opacity 'xor)))
(let ((dpos (djvu-dpos))
(doc djvu-doc))
(with-current-buffer (djvu-ref annot-buf doc)
(if (djvu-goto-dpos 'rect dpos)
(djvu-update-url-internal url color opacity border)
(user-error "No object to update")))))
(defun djvu-update-url-internal (url &optional color _opacity border)
"Update URL internal."
(interactive
(let* ((color (djvu-interactive-color djvu-color-url))
(url (djvu-interactive-url color)))
(list url color djvu-opacity 'xor)))
(let ((bounds (djvu-object-bounds)))
(if bounds
(save-excursion
(save-restriction
(narrow-to-region (car bounds) (cdr bounds))
(goto-char (point-min))
(if (not (looking-at "(maparea \\(\"[^\"]*\"\\)"))
(user-error "Nothing to update")
(replace-match (format "\"%s\"" url) nil nil nil 1)
(djvu-update-color-internal color)
(goto-char (point-min))
(let ((border (format "(%s)" border)))
(if (re-search-forward "(\\(none\\|xor\\))" nil t)
(replace-match border)
(goto-char (point-max))
(skip-chars-backward " \t\n")
(backward-char) (insert " " border)))))))))
(defun djvu-rect-region-url (beg end url comment &optional color opacity border)
"Put URL over region between BEG and END, adding annotation COMMENT."
(interactive
(djvu-with-region region
(let* ((color (djvu-interactive-color djvu-color-url))
(url (djvu-interactive-url color))
(comment (djvu-read-string
(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))
(defun djvu-rect-region (beg end url comment &optional color opacity border)
"Highlight region between BEG and END, add URL and annotation COMMENT."
(interactive
(djvu-with-region region
(let* ((color (djvu-interactive-color djvu-color-highlight))
(comment (djvu-read-string (format "(%s) Annotation: "
(or color "no color"))
region)))
(list (car region) (cdr region) nil comment color djvu-opacity 'none))))
(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)
(setq areas (list (djvu-scan-zone beg end 'word)))
(if (eq 2 lines)
(let ((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 (and (= beg (djvu-property-beg beg 'line))
(djvu-areas-justify t c1 c2))
(djvu-justify-areas 'min 0 c1 c2))
(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))
(let ((tmp (/ (+ (aref c1 1) (aref c2 3)) 2)))
(aset c1 1 tmp) (aset c2 3 tmp)))
(setq areas (list c1 c2)))
(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)))
(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)))
(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)))
(aset c1 1 tmp1) (aset ci 3 tmp1)
(aset ci 1 tmp2) (aset c2 3 tmp2))
(setq areas (list c1 ci c2)))))
(djvu-rect-area url comment areas color opacity border)))
(defun djvu-merge-areas (areas)
"Try to merge elements in AREAS.
This assumes that element N in AREAS is above element N+1.
If such a pair of elements has the same left and right boundaries,
and the lower boundary of N equals the upper boundary of N+1,
these elements are merged into one."
(let ((areas areas))
(while (nth 1 areas)
(let ((c0 (nth 0 areas)) (c1 (nth 1 areas)))
(if (and (eq (aref c0 0) (aref c1 0))
(eq (aref c0 2) (aref c1 2))
(eq (aref c0 1) (aref c1 3)))
(progn (aset c0 1 (aref c1 1))
(setcdr areas (cddr areas)))
(pop areas)))))
areas)
(defun djvu-rect-area (url comment rects &optional color opacity border)
"Using URL and COMMENT, highlight RECTS.
The elements in the list RECTS are 4-element sequences of coordinates
each defining a rect area for djvused."
(setq rects (mapcar (lambda (rect) (apply 'format "(rect %d %d %d %d)"
(djvu-bound-area rect)))
(djvu-merge-areas rects)))
(with-current-buffer (djvu-ref annot-buf)
(unless (and djvu-rect-area-nodups
(save-excursion
(goto-char (point-min))
(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 ")
")\n"
(djvu-insert-color "hilite" color)
(if (and color opacity) (format " (opacity %s)" opacity) "")
(format " (%s)" (or border 'none))
")\n\n")
(undo-boundary))))
(defun djvu-fill (text)
"Fill string TEXT using `fill-column' of the annotations buffer.
This value of `fill-column' defaults to `djvu-fill-column'."
(let ((fcolumn (with-current-buffer (djvu-ref annot-buf)
fill-column)))
(with-temp-buffer
(insert text)
(let ((fill-column fcolumn))
(fill-region (point-min) (point-max)))
(buffer-substring-no-properties
(point-min) (point-max)))))
(defun djvu-toggle-rect-text-internal ()
"Toggle between Mapareas rect and text."
(interactive)
(let ((bounds (djvu-object-bounds))
(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")
(save-excursion
(save-restriction
(narrow-to-region (car bounds) (cdr bounds))
(goto-char (point-min))
(cond ((re-search-forward rect-re nil t) (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 2)))
(goto-char (point-min))
(let ((opacity
(if (re-search-forward " *(opacity \\([0-9]+\\))" nil t)
(prog1 (djvu-match-number 1)
(replace-match ""))
djvu-opacity)))
(goto-char (point-min))
(while (re-search-forward color-re nil t)
(if (equal (match-string 1) "hilite")
(replace-match
(format "(backclr %s)"
(save-match-data
(djvu-color-background
(match-string 2) nil opacity))))))))
((re-search-forward text-re nil t) (let ((opacity (save-match-data
(read-number "Opacity: " djvu-opacity))))
(replace-match (format "((rect %s))" (match-string 2)))
(goto-char (point-min))
(while (re-search-forward color-re nil t)
(if (equal (match-string 1) "backclr")
(replace-match
(format "(hilite %s) (opacity %d)"
(save-match-data
(djvu-color-background
(match-string 2) nil opacity t))
opacity))))))
(t
(user-error "Nothing to toggle"))))))))
(defun djvu-resize-internal (step)
"Resize Djvu mapareas rect and text by STEP."
(interactive "nStep: ")
(let ((bounds (djvu-object-bounds)))
(if (not bounds)
(user-error "No object to update")
(save-excursion
(save-restriction
(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)
(+ (djvu-match-number 5) step)
(+ (djvu-match-number 6) step))
nil nil nil 2)))))))
(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
(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
(let ((bounds (djvu-object-bounds)))
(if bounds
(narrow-to-region (car bounds) (cdr bounds))
(user-error "No object to update"))))
(goto-char (point-min))
(while (re-search-forward djvu-area-re nil t)
(replace-match (format "%d %d %d %d"
(+ (* (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.
This may come handy for reformatting such strings."
(interactive)
(let ((bounds (djvu-object-bounds)))
(if (not bounds)
(user-error "No object to update")
(save-excursion
(goto-char (car bounds))
(forward-char)
(forward-sexp 2)
(skip-chars-forward "\s\t\n")
(save-restriction
(narrow-to-region (point) (scan-sexps (point) 1))
(while (re-search-forward "\n\\(\n\\)*" nil t)
(unless (match-string 1)
(replace-match " "))))))))
(defun djvu-property-beg (pnt prop)
"Starting from position PNT search backward for beginning of property PROP.
Return position found."
(let ((p1 (get-text-property pnt prop)) pnt-1)
(cond ((and p1 (< (point-min) pnt)
(eq p1 (get-text-property (1- pnt) prop)))
(previous-single-property-change pnt prop nil (point-min)))
(p1 pnt)
((and (< (point-min) pnt)
(setq p1 (get-text-property (setq pnt-1 (1- pnt)) prop)))
(if (and (< (point-min) pnt-1)
(eq p1 (get-text-property (1- pnt-1) prop)))
(previous-single-property-change pnt-1 prop nil (point-min))
pnt-1))
(t (error "Position %s does not have/end property %s" pnt prop)))))
(defun djvu-property-end (pnt prop)
"Starting from position PNT search forward for end of property PROP.
Return position found."
(let ((p1 (get-text-property pnt prop)))
(cond ((and p1 (< pnt (point-max))
(eq p1 (get-text-property (1+ pnt) prop)))
(next-single-property-change pnt prop nil (point-max)))
(p1 (1+ pnt))
((and (< (point-min) pnt)
(get-text-property (1- pnt) prop))
pnt)
(t (error "Position %s does not have/end property %s" pnt prop)))))
(defun djvu-areas-justify (left &rest ci)
"Return non-nil if areas CI shall be justified horizontally.
If LEFT is nil analyze left boundaries of CI, otherwise the right boundaries."
(let ((xl (apply 'min (mapcar (lambda (c) (aref c 0)) ci)))
(xr (apply 'max (mapcar (lambda (c) (aref c 2)) ci))))
(> djvu-areas-justify
(/ (apply 'max (mapcar (lambda (cj)
(abs (float (if left (- (aref cj 0) xl)
(- xr (aref cj 2))))))
ci))
(float (- xr xl))))))
(defun djvu-justify-areas (fun n &rest ci)
"Pass Nth elements of arrays CI to function FUN.
Set these elements to return value of FUN.
If FUN is `min' or `max' these elements are set to the respective minimum
or maximum among the Nth elements of all arrays CI."
(let ((tmp (apply fun (mapcar (lambda (c) (aref c n)) ci))))
(dolist (c ci)
(aset c n tmp))))
(defun djvu-scan-zone (beg end prop)
"Between BEG and END calculate total zone coordinates for 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)
(setq pnt (next-single-property-change pnt prop nil end)))
(when (setq val (get-text-property pnt prop))
(aset zone 0 (min (aref zone 0) (aref val 0)))
(aset zone 1 (min (aref zone 1) (aref val 1)))
(setq max (max max (aref val 1))) (aset zone 2 (max (aref zone 2) (aref val 2)))
(aset zone 3 (max (aref zone 3) (aref val 3)))
(setq min (min min (aref val 3)))))
(if (and (or djvu-descenders-re djvu-ascenders-re)
(eq prop 'word))
(let* ((string (buffer-substring-no-properties beg end))
(long (< 1 (length string))) case-fold-search)
(if (and long djvu-descenders-re
(> 0.10 (/ (- max (aref zone 1))
(float (- (aref zone 3) (aref zone 1)))))
(not (string= string (upcase string)))
(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
(> 0.10 (/ (- (aref zone 3) min)
(float (- (aref zone 3) (aref zone 1)))))
(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)
"Count regions between BEG and END with distinct non-nil values of PROP."
(let ((count 0)
(pnt beg))
(while (and (/= pnt end)
(setq pnt (next-single-property-change pnt prop nil end)))
(if (get-text-property (1- pnt) prop)
(setq count (1+ count))))
count))
(defun djvu-read-annot (buf)
"Read annotations of a Djvu document from annotations buffer."
(let (object)
(with-current-buffer buf
(save-restriction
(widen)
(with-temp-buffer
(insert-buffer-substring-no-properties buf)
(djvu-convert-hash)
(goto-char (point-min))
(while (progn (skip-chars-forward " \t\n") (not (eobp)))
(beginning-of-line)
(if (looking-at djvu-beg-object-re)
(condition-case nil
(push (read (current-buffer)) object)
(error (error "Syntax error in annotations")))
(error "Unknown annotation `%s'" (buffer-substring-no-properties
(point) (line-end-position))))))))
(nreverse object)))
(defun djvu-save-annot (script &optional doc shared)
"Save annotations of the Djvu document DOC.
This dumps the content of DOC's annotations buffer into the djvused script
file SCRIPT. DOC defaults to the current Djvu document."
(unless doc (setq doc djvu-doc))
(let ((object (djvu-read-annot (if shared
(djvu-ref shared-buf doc)
(djvu-ref annot-buf doc)))))
(dolist (elt object)
(if (eq 'maparea (car elt))
(setcar (cdr elt) (djvu-resolve-url (nth 1 elt)))))
(with-temp-buffer
(let ((standard-output (current-buffer))
(buffer-file-coding-system 'utf-8)
(id 0)
rect-list)
(insert (if shared
"create-shared-ant; remove-ant; set-ant\n"
(format "select %d; remove-ant; set-ant\n"
(djvu-ref page doc))))
(dolist (elt object)
(cond ((eq 'metadata (car elt)) (prin1 elt)
(insert "\n"))
((or (not (eq 'maparea (car elt))) (memq (car (nth 3 elt)) '(line poly))) (prin1 elt)
(insert "\n"))
((consp (car (nth 3 elt))) (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"))
(setq id (1+ id))
(push (djvu-rect-elt
(cons (append (list (nth 0 elt) (nth 1 elt) (nth 2 elt))
(nthcdr 4 elt))
(nth 3 elt))
id)
rect-list))
((memq (car (nth 3 elt)) '(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"))
(t (error "Djvu maparea %s undefined" (car (nth 3 elt))))))
(insert ".\n")
(djvu-convert-hash t)
(write-region nil nil script t 0) (unless shared
(djvu-set rect-list (apply 'nconc rect-list) doc))))))
(defun djvu-annot-script (&optional doc buffer page display)
"Create djvused script for complete annotation layer of DOC in BUFFER.
If prefix PAGE is non-nil create instead script for only page PAGE.
BUFFER defaults to `djvu-script-buffer'. If BUFFER is t use current buffer.
You can edit the annotations script in BUFFER. Afterwards you can re-apply
this script using `djvu-process-script'. This code will not (cannot) check
whether the edited script is meaningful for DOC. Use at your own risk.
You get what you want."
(interactive (list nil nil (if current-prefix-arg (djvu-ref page)) t))
(let ((doc (or doc djvu-doc))
(buffer (djvu-script-buffer buffer)))
(djvu-save doc t)
(with-current-buffer buffer
(let ((buffer-undo-list t)
(djvu-init t)
buffer-read-only)
(djvu-script-mode)
(erase-buffer)
(if page (insert (format "select \"%s\" # page %d\n"
(cdr (assq page (djvu-ref page-id doc)))
page)))
(djvu-djvused doc t "-e" (format "select %s; output-ant;"
(or page "")))
(goto-char (point-min))
(while (re-search-forward "^(maparea" nil t)
(forward-sexp) (let ((limit (save-excursion (forward-sexp) (point))))
(while (search-forward "\\n" limit t)
(replace-match "\n"))))
(goto-char (point-min)))
(set-buffer-modified-p nil)
(setq buffer-undo-list nil))
(if display (switch-to-buffer buffer))))
(defun djvu-annot-dpos (&optional point doc)
"Return Djvu position of POINT in Djvu annotations buffer."
(with-current-buffer (djvu-ref annot-buf doc)
(save-excursion
(if point (goto-char point))
(let ((bounds (djvu-object-bounds)))
(if bounds
(let* ((object (djvu-object bounds))
(area (nth 3 object)))
(if (eq (car object) 'maparea)
(cond ((memq (car area) '(text oval line poly))
(cdr (nth 3 object)))
((consp area) (cdar area))))))))))
(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."
(save-excursion
(let ((pnt (point)) found end)
(beginning-of-line)
(while (not (or (setq found (looking-at djvu-beg-object-re))
(bobp)))
(forward-line -1))
(if (and found
(< pnt (setq end (save-excursion (forward-sexp) (point)))))
(cons (point) end)
(setq found nil)
(goto-char pnt)
(while (not (or (setq found (looking-at djvu-beg-object-re))
(eobp)))
(forward-line 1))
(if found
(cons (point) (progn (forward-sexp) (point))))))))
(defun djvu-object (bounds)
"Return Djvu object defined via BOUNDS, a cons cell (BEG . END)."
(let ((string (buffer-substring-no-properties (car bounds) (cdr bounds))))
(with-temp-buffer
(insert string)
(djvu-convert-hash)
(goto-char (point-min))
(read (current-buffer)))))
(defun djvu-update-color (color)
"Update color attribute of Djvu maparea to COLOR."
(interactive (list (djvu-interactive-color-read)))
(let ((dpos (djvu-dpos))
(doc djvu-doc))
(with-current-buffer (djvu-ref annot-buf doc)
(if (djvu-goto-dpos 'rect dpos)
(djvu-update-color-internal color)
(user-error "No object to update")))))
(defun djvu-update-color-internal (color &optional opacity)
"Update color attribute of Djvu maparea to COLOR.
If no such attribute exists insert a new one.
Prefix arg OPACITY is the opacity to use."
(interactive
(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 ((not color)
(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)
(let ((attr (match-string 1)))
(cond ((member attr '("hilite" "lineclr" "border"))
(replace-match (cdr (assoc color djvu-color-alist))
nil nil nil 2))
((string= attr "backclr")
(replace-match (save-match-data
(djvu-color-background color nil opacity))
nil nil nil 2))
(t (message "Color update for attribute `%s' undefined"
attr)))))
((re-search-forward "(rect" (cdr bounds) t)
(goto-char (1- (cdr bounds)))
(insert (format " (hilite %s)"
(cdr (assoc color djvu-color-alist))))
(unless (save-excursion
(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)"
(djvu-color-background color nil opacity))))
(t (message "Do not know how to update color")))))))
(defun djvu-merge-mapareas (beg end)
"Merge Djvu mapareas from BEG to END."
(interactive "r")
(let (bounds url text rect hilite opacity border)
(goto-char beg)
(while (and (< (point) end)
(setq bounds (djvu-object-bounds)))
(if (< (car bounds) beg) (setq beg (car bounds)))
(if (< end (cdr bounds)) (setq end (cdr bounds)))
(let ((maparea (djvu-object bounds)))
(unless (eq 'maparea (car maparea))
(error "Cannot merge `%s'" (car maparea)))
(push (nth 1 maparea) url)
(push (nth 2 maparea) text)
(dolist (elt (nthcdr 3 maparea))
(cond ((consp (car elt)) (mapc (lambda (r) (push r rect)) elt))
((memq (car elt) '(text line))
(user-error "Cannot merge text or line mapareas"))
((eq (car elt) 'hilite)
(push (cadr elt) hilite))
((eq (car elt) 'opacity)
(push (cadr elt) opacity))
((memq (car elt) '(none xor))
(push (car elt) border))
(t
(error "Unknown attribute `%s'" elt)))))
(goto-char (cdr bounds))
(skip-chars-forward "\s\t\n"))
(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 hilite (delete-dups hilite))
(if (nth 1 hilite) (user-error "Cannot merge multiple hilites"))
(setq opacity (delete-dups opacity))
(if (nth 1 opacity) (user-error "Cannot merge multiple opacities"))
(setq border (or (delete-dups (delq 'none border)) '(none)))
(if (nth 1 border) (user-error "Cannot merge multiple borders"))
(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"
(if hilite (format " (hilite %s)" (car hilite)) "")
(if opacity (format " (opacity %s)" (car opacity)) "")
(format " (%s)" (car border))
")\n")
(save-restriction
(narrow-to-region beg (point))
(djvu-convert-hash t))))
(defun djvu-bookmarks-page (&optional pnt doc)
"In Bookmarks buffer return page number at position PNT.
PNT defaults to position of point."
(djvu-url-page
(save-excursion
(if pnt (goto-char pnt))
(beginning-of-line)
(while (not (or (bobp)
(looking-at "^[\t\s]*(\\(\"\\)")))
(forward-line -1))
(when (match-beginning 1)
(goto-char (match-beginning 1))
(forward-sexp)
(read (current-buffer))))
doc))
(defun djvu-url-page (url &optional doc)
"For the internal URL return the corresponding page number.
This is the inverse of `djvu-page-url'.
Return nil if URL is not an internal URL."
(if url
(cond ((string-match "\\`#\\([0-9]+\\)\\'" url)
(djvu-match-number 1 url))
((string-match "\\`#" url)
(car (rassoc (substring-no-properties url 1)
(djvu-ref page-id doc)))))))
(defun djvu-bookmark (text page &optional level)
"Create bookmark"
(interactive
(djvu-with-region region
(list (djvu-read-string "Bookmark: " region t)
(djvu-ref page) (djvu-interactive-bookmark-level))))
(setq text (replace-regexp-in-string "[\n ]+" " " text))
(let (object)
(with-current-buffer (djvu-ref bookmarks-buf)
(goto-char (point-min))
(if (equal (point) (point-max))
(setq object (list 'bookmarks))
(condition-case nil
(setq object (read (current-buffer)))
(error (error "Syntax error in bookmarks"))))
(unless (eq 'bookmarks (car object))
(error "No bookmarks"))
(let* ((djvu-bookmark-level -1)
(object (djvu-splice-bookmark text page (cdr object) level)))
(erase-buffer)
(insert "(bookmarks")
(djvu-insert-bookmarks object " ")
(insert ")\n"))
(goto-char (point-min))
(undo-boundary))))
(defun djvu-interactive-bookmark-level ()
"Return bookmark level for interactive commands.
Value is nil if the command is called without prefix arg.
Value is t (one level down) if called with prefix C-u.
Otherwise the raw prefix arg should be a non-negative integer
specifying the absolute level of the bookmark."
(cond ((consp current-prefix-arg))
((integerp current-prefix-arg)
(abs current-prefix-arg))))
(defun djvu-splice-bookmark (text page object &optional level)
"Splice bookmark (TEXT PAGE) into tree of bookmarks OBJECT.
If LEVEL is t put bookmark one sublevel below the level
of the preceding bookmark.
If LEVEL is a non-negative integer put bookmark on level LEVEL.
This throws a user error if a bookmark subtree at PAGE extends
beyond PAGE so that putting a new bookmark for PAGE past this subtree
would break the page ordering of the bookmarks.
This code assumes that bookmarks are ordered by page number and that
external URLs appear at the beginning of a subtree of bookmarks."
(setq djvu-bookmark-level (1+ djvu-bookmark-level))
(if (or (null object)
(let ((page-url (djvu-url-page (nth 1 (car object)))))
(and page-url (< page page-url))))
(cons (list text (djvu-page-url page)) object)
(let ((object object))
(while object
(if (or (not (djvu-url-page (nth 1 (car object)))) (and (cdr object)
(>= page (djvu-url-page (nth 1 (nth 1 object))))))
(setq object (cdr object)) (if (or (and (not level)
(nth 2 (car object)))
(eq t level)
(and (integerp level)
(< djvu-bookmark-level level)))
(setcar object (cons (nth 0 (car object))
(cons (nth 1 (car object))
(djvu-splice-bookmark
text page (nthcdr 2 (car object))
level))))
(let (page-max)
(and (eq level djvu-bookmark-level)
(setq page-max (djvu-bookmarks-page-max
(nthcdr 2 (car object))))
(< page page-max)
(user-error "Bookmark level %d invalid on page %d: preceding subtree extends to page %d"
level page page-max)))
(setcdr object (cons (list text (djvu-page-url page))
(cdr object))))
(setq object nil))))
object))
(defun djvu-bookmarks-page-max (object)
"Return maximum page number of a bookmark tree OBJECT.
Return nil if OBJECT does not have internal URLs."
(let (page page-max)
(dolist (elt object)
(and (setq page (djvu-url-page (nth 1 elt)))
(setq page-max (if page-max
(max page-max page)
page)))
(and (nth 2 elt)
(setq page (djvu-bookmarks-page-max (nthcdr 2 elt)))
(setq page-max (if page-max
(max page-max page)
page))))
page-max))
(defun djvu-insert-bookmarks (object indent)
"Insert Bookmarks OBJECT recursively."
(let ((indent1 (concat indent " ")))
(dolist (elt object)
(insert (format "\n%s(%S\n%s %S" indent (car elt) indent
(djvu-resolve-url (nth 1 elt))))
(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")
(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)) (djvu-insert-bookmarks (cdr object) " "))
(insert ")\n"))
(insert ".\n")
(write-region nil nil script t 0)) (djvu-init-outline (cdr object) doc)))
(defun djvu-init-outline (object &optional doc)
(with-current-buffer (djvu-ref outline-buf doc)
(let (buffer-read-only)
(erase-buffer)
(djvu-insert-outline object ""))
(set-buffer-modified-p nil)
(setq buffer-read-only t)
(djvu-goto-outline (or (djvu-ref page doc) 1))))
(defun djvu-insert-outline (object indent)
"Insert Outline OBJECT recursively."
(let ((indent1 (concat indent " "))
(djvu-resolve-url 'short))
(dolist (elt object)
(let ((beg (point)))
(insert indent (car elt))
(make-text-button beg (point) 'type 'djvu-url
'face (aref djvu-outline-faces
(% (/ (length indent) 2)
(length djvu-outline-faces)))
'help-echo (format "mouse-2, RET: url `%s'"
(nth 1 elt))
'djvu-args (list (nth 1 elt))))
(insert "\s" (substring (djvu-resolve-url (nth 1 elt)) 1) "\n")
(djvu-insert-outline (nthcdr 2 elt) indent1))))
(defun djvu-outline-page (&optional pnt doc)
"In Outline buffer return page number at position PNT.
PNT defaults to position of point."
(djvu-url-page
(car (get-text-property
(save-excursion
(if pnt (goto-char pnt))
(beginning-of-line)
(skip-chars-forward "\s\t")
(point))
'djvu-args))
doc))
(defun djvu-goto-outline (&optional page doc)
"In Outline buffer go to first bookmark matching PAGE."
(unless page (setq page (djvu-ref page doc)))
(goto-char (point-min))
(let ((pnt (point)) p done)
(while (not (or done (eobp)))
(when (setq p (djvu-outline-page (point) doc))
(if (<= p page)
(setq pnt (point)))
(setq done (= p page)))
(forward-line))
(goto-char pnt)))
(defmacro djvu-with-event-buffer (event &rest body)
"With buffer of EVENT current, evaluate BODY."
(declare (indent 1))
`(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 ()
"Toggle image display of current page."
(interactive)
(if (display-images-p)
(djvu-image-mode (or current-prefix-arg 'toggle))
(message "Cannot display images")))
(define-minor-mode djvu-image-mode
"Image display of current page."
:lighter "Image"
:keymap '((" " . djvu-image-scroll-up)
([?\S-\ ] . djvu-image-scroll-down)
("\C-?" . djvu-image-scroll-down)
([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)
([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)
([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)))
([M-drag-mouse-1] . djvu-mouse-word-area)
([M-down-mouse-1] . djvu-mouse-drag-track-area)
([drag-mouse-3] . djvu-mouse-word-area) ([down-mouse-3] . djvu-mouse-drag-track-area))
(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
(djvu-set read-pos (let (djvu-image-mode)
(djvu-read-dpos)))
(setq-local auto-hscroll-mode nil))
(disable
(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
(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
(djvu-goto-read (djvu-ref read-pos))))))
(defun djvu-image (&optional isize)
"If `djvu-image-mode' is enabled, display image of current Djvu page.
Otherwise remove the image."
(if (not djvu-image-mode)
(let (buffer-read-only)
(remove-text-properties (point-min) (point-max) '(display nil)))
(if (or (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))
(with-temp-buffer
(set-buffer-multibyte nil)
(let* ((coding-system-for-read 'raw-text)
(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))
(djvu-set image
(append (list (djvu-ref page doc) isize)
(create-image (buffer-substring-no-properties
(point-min) (point-max))
'pbm t))
doc)))))
(let (buffer-read-only)
(if (= (point-min) (point-max)) (insert " "))
(put-text-property (point-min) (point-max)
'display (nthcdr 2 (djvu-ref image))))))
(defun djvu-image-vscroll ()
"Return the amount by which a page image is scrolled vertically."
(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
go to the image of the next page.
Prefix N may take the same values as arg N of `image-scroll-up'."
(interactive "P") (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)
(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
go to the image of the previous page.
Prefix N may take the same values as arg N of `image-scroll-down'."
(interactive "P") (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)
(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 (= (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)
(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 (= (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)
(image-set-window-hscroll hscroll))))
(defun djvu-image-zoom-in (&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)))))
(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)))))
(defun djvu-mouse-drag-track-area (start-event &optional line)
"Track drag over image."
(interactive "e")
(let ((old-track-mouse track-mouse))
(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")
(djvu-with-event-buffer event
(djvu-image-rect (list 'down-mouse-1
(event-start start-event)
(event-end event))
line))))
map)
t (lambda ()
(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."
(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))
(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)
(aset image i (- 255 (aref image i)))
(setq i (1+ i)))))
(if (not line)
(while (< y1 y2)
(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)
(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)
(let ((i (+ i0 (* 3 (+ x (* (+ y1 (round (* (- x x1) dy)))
width))))))
(invert i (+ i 3)))
(setq x (+ x step)))))))
(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))
(let ((old-image (get-text-property (point-min) 'display))
buffer-read-only)
(put-text-property (point-min) (point-max)
'display (nthcdr 2 (djvu-ref image)))
(image-flush old-image))))
(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))
x1 (min x1 x2))
width))
(round (* (- (cdr size) (if (memq dir '(horiz free))
y1 (max y1 y2)))
height))
(round (* (if (memq dir '(vert free))
x2 (max x1 x2))
width))
(round (* (- (cdr size) (if (memq dir '(horiz free))
y2 (min y1 y2)))
height)))))
(djvu-set read-pos (djvu-mean-dpos area))
area))
(defun djvu-mouse-rect-area (event)
(interactive "e")
(djvu-with-event-buffer event
(unwind-protect
(let ((color (djvu-interactive-color djvu-color-highlight))
(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")
(djvu-mouse-text-area-internal event "Text"))
(defun djvu-mouse-text-area-pushpin (event)
(interactive "e")
(djvu-mouse-text-area-internal event "Text w/Pushpin" t))
(defun djvu-mouse-text-area-internal (event prompt &optional pushpin)
(djvu-with-event-buffer event
(unwind-protect
(let ((color (djvu-interactive-color djvu-color-highlight))
(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")
(djvu-mouse-line-area-internal event 'free))
(defun djvu-mouse-line-area-horiz (event)
(interactive "e")
(djvu-mouse-line-area-internal event 'horiz))
(defun djvu-mouse-line-area-vert (event)
(interactive "e")
(djvu-mouse-line-area-internal event 'vert))
(defun djvu-mouse-line-area-internal (event &optional dir)
(djvu-with-event-buffer event
(unwind-protect
(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)
(with-current-buffer (djvu-ref annot-buf)
(goto-char (point-max))
(insert (format "(maparea %S\n %S\n "
(or url "") (if text (djvu-fill text) ""))
(apply 'format "(line %d %d %d %d)" (djvu-bound-area line))
(format " (%s)" (or border 'none))
(if arrow " (arrow)" "")
(if width (format " (width %d)" width) "")
(djvu-insert-color "lineclr" lineclr)
")\n\n")
(undo-boundary)))
(defun djvu-text-line-area (string area &optional doc)
(with-current-buffer (djvu-ref text-buf doc)
(goto-char (point-max))
(skip-chars-backward " \t\n")
(backward-char) (insert (apply 'format "\n (line %d %d %d %d" area))
(let* ((word-list (split-string string))
(n (+ (length (apply 'concat word-list)) -1 (length word-list))) (m1 0) (m2 (1+ n))
(x1 (nth 0 area)) (x2 (nth 2 area))
(width (/ (- x2 x1) (float n))) word)
(dotimes (i (length word-list))
(setq word (nth i word-list))
(setq m2 (- m2 1 (length word)))
(insert (format "\n (word %d %d %d %d %S)"
(+ x1 (round (* m1 width)))
(nth 1 area)
(- x2 (round (* m2 width)))
(nth 3 area)
word))
(setq m1 (+ m1 1 (length word)))))
(insert ")")))
(defun djvu-mouse-word-area (event)
"Insert word."
(interactive "e")
(djvu-with-event-buffer event
(unwind-protect
(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))))
(defun djvu-interactive-pages (&optional doc)
"Specify page range to operate on in interactive calls.
Without a prefix, return nil meaning \"all pages\".
Otherwise return a cons pair (PAGE1 . PAGE2).
With prefix C-u, this becomes the current page.
With prefix C-u C-u, read page range from minibuffer."
(let ((pages (cond ((equal '(16) current-prefix-arg)
(cons (read-number "First page: " 1)
(read-number "Last page: "
(djvu-ref pagemax doc))))
(current-prefix-arg
(cons (djvu-ref page doc) (djvu-ref page doc))))))
(cons (if pages
(if (eq (car pages) (cdr pages))
(if (eq (car pages) (djvu-ref page doc)) "current page"
(format "page %d" (car pages)))
(format "pages %d-%d" (car pages) (cdr pages)))
"all pages")
pages)))
(defun djvu-pages-action (pages action doc)
"Apply ACTION to PAGES of Djvu document DOC.
If PAGES is nil, operate on all pages.
Otherwise PAGES is a cons pair (PAGE1 . PAGE2)."
(unless doc (setq doc djvu-doc))
(djvu-save doc t)
(if pages
(djvu-djvused doc nil "-e"
(mapconcat
(lambda (page)
(format "select %s; %s" page action))
(number-sequence
(max 1 (car pages))
(min (djvu-ref pagemax doc) (cdr pages)))
"; ")
"-s")
(djvu-djvused doc nil "-e"
(format "select; %s" action)
"-s")))
(defun djvu-dpi (dpi &optional pages doc)
"Set DPI resolution of djvu document DOC.
If optional arg PAGES is nil, operate on all pages.
Otherwise PAGES is a cons pair (PAGE1 . PAGE2).
With prefix C-u, PAGES becomes the current page.
With prefix C-u C-u, read range PAGES from minibuffer."
(interactive
(let ((pages (djvu-interactive-pages)))
(list (read-number (format "(%s) Dpi: " (car pages)))
(cdr pages))))
(djvu-pages-action pages (format "set-dpi %d" dpi) doc))
(defun djvu-dpi-unify (width dpi &optional doc)
"Unify the ratio WIDTH / DPI of all pages of a Djvu document.
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 (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 ((d (/ (* (djvu-match-number 1) dpi) width)))
(if (< dpi d)
(push (cons page d) job)))
(forward-line))))
(if (not job)
(message "Nothing to unify")
(djvu-djvused doc nil "-e"
(mapconcat (lambda (elt)
(format "select %s; set-dpi %d"
(car elt) (cdr elt)))
job "; ")
"-s")
(message "%s pages updated: %s" (length job)
(mapconcat (lambda (elt) (format "%d" (car elt)))
(nreverse job) ", ")))))
(defun djvu-rotate (&optional rot pages doc)
"Set rotation of Djvu document DOC.
The rotation angle ROT is in multiples of 90 degrees counterclockwise.
If string ROT has prefix [+-] apply relative rotation.
If optional arg PAGES is nil, operate on all pages.
Otherwise PAGES is a cons pair (PAGE1 . PAGE2).
With prefix C-u, PAGES becomes the current page.
With prefix C-u C-u, read range PAGES from minibuffer."
(interactive
(let ((pages (djvu-interactive-pages)))
(list (read-string
(format "(%s) Rotate ([+-]0...3, default +1): " (car pages))
nil nil "+1")
(cdr pages))))
(cond ((or (not rot) (equal rot ""))
(setq rot "+1"))
((not (string-match "\\`[-+]?[0123]\\'" rot))
(user-error "Djvu rotation `%s' invalid" rot)))
(djvu-pages-action pages (format "set-rotation %s" rot) doc))
(defun djvu-page-title (title &optional pages doc)
"Set page TITLE of Djvu document DOC.
If TITLE is empty string or nil remove page title.
If optional arg PAGES is nil, operate on all pages.
Otherwise PAGES is a cons pair (PAGE1 . PAGE2).
With prefix C-u, PAGES becomes the current page.
With prefix C-u C-u, read range PAGES from minibuffer."
(interactive
(let ((pages (djvu-interactive-pages)))
(list (read-string (format "(%s) Page title: " (car pages)))
(cdr pages))))
(unless doc (setq doc djvu-doc))
(if (and (stringp title)
(not (equal "" title)))
(djvu-pages-action (or pages (cons 1 (djvu-ref pagemax doc)))
(format "set-page-title %s" title) doc)
(djvu-save doc t)
(djvu-djvused doc nil "-e"
(mapconcat (lambda (page)
(format "select %d; set-page-title %s" page
(cdr (assq page (djvu-ref page-id doc)))))
(number-sequence (or (car pages) 1)
(or (cdr pages) (djvu-ref pagemax doc)))
"; ")
"-s")))
(defun djvu-ls (&optional doc)
"List component files in the Djvu document.
This uses the command \"djvused doc.djvu -e ls\"."
(interactive)
(let ((buffer (get-buffer-create "*djvu-ls*"))
(doc (or doc djvu-doc)))
(with-current-buffer buffer
(let ((buffer-undo-list t)
buffer-read-only)
(erase-buffer)
(djvu-djvused doc t "-e" "ls"))
(set-buffer-modified-p nil)
(setq buffer-read-only t)
(goto-char (point-min)))
(pop-to-buffer buffer)))
(defun djvu-inspect-file (file &optional page)
"Inspect Djvu FILE on PAGE.
Call djvused with the same sequence of arguments that is used
by `djvu-init-page'. Display the output in `djvu-script-buffer'.
This may come handy if `djvu-find-file' chokes on a Djvu file."
(interactive (djvu-read-file-name))
(with-current-buffer (get-buffer-create djvu-script-buffer)
(erase-buffer)
(let* ((coding-system-for-read 'utf-8)
(fmt (concat "create-shared-ant; print-ant; n; ls; print-outline; "
"select %d; size; print-txt; print-ant;"))
(status (apply 'call-process "djvused" nil t nil
(list "-u" file "-e" (format fmt (or page 1))))))
(unless (zerop status)
(error "Djvused error %s" status)))
(set-buffer-modified-p nil)
(goto-char (point-min)))
(pop-to-buffer djvu-script-buffer))
(defun djvu-delete-page (&optional doc)
"Delete current page from the Djvu document. Use with care!"
(interactive)
(unless doc (setq doc djvu-doc))
(djvu-save doc t)
(when (and (< 1 (djvu-ref pagemax doc))
(yes-or-no-p "Delete current page "))
(djvu-resolve-all-urls 'long doc)
(djvu-backup doc)
(let* ((inhibit-quit t)
(page (djvu-ref page doc))
(status (call-process "djvm" nil nil nil "-d"
(djvu-ref file doc) (number-to-string page))))
(unless (zerop status) (error "Djvm error %s" status))
(djvu-all-buffers doc
(set-visited-file-modtime))
(djvu-set image nil doc)
(let ((page-id (delq (assq page (djvu-ref page-id doc))
(djvu-ref page-id doc)))
(p (1+ page))
p-i)
(while (<= p (djvu-ref pagemax doc))
(setq p-i (assq p page-id)
page-id (cons (cons (1- p) (cdr p-i))
(delq p-i page-id))
p (1+ p)))
(djvu-set page-id page-id doc))
(djvu-set pagemax (1- (djvu-ref pagemax doc)) doc)
(djvu-init-page (min page (djvu-ref pagemax doc)) doc))))
(defun djvu-remove-annot (&optional doc outline)
"Remove Annotations. Use with care!
With prefix OUTLINE non-nil remove Outline, too."
(interactive (list nil current-prefix-arg))
(unless doc (setq doc djvu-doc))
(djvu-save doc t)
(when (yes-or-no-p (format "Remove Annotations%s: "
(if outline " and Outline" "")))
(djvu-djvused doc nil "-e"
(format "select; remove-ant;%s"
(if outline " set-outline;\n." ""))
"-s")
(djvu-init-page nil doc)))
(declare-function bookmark-make-record-default "bookmark"
(&optional no-file no-context posn))
(declare-function bookmark-prop-get "bookmark" (bookmark prop))
(declare-function bookmark-get-filename "bookmark" (bookmark))
(declare-function bookmark-get-front-context-string "bookmark" (bookmark))
(declare-function bookmark-get-rear-context-string "bookmark" (bookmark))
(declare-function bookmark-get-position "bookmark" (bookmark))
(defun djvu-bookmark-make-record ()
(nconc (bookmark-make-record-default)
`((page . ,(djvu-ref page))
(d-buffer . ,djvu-buffer)
(handler . djvu-bookmark-handler))))
(defun djvu-bookmark-handler (bmk)
"Handler to jump to a particular bookmark location in a djvu document.
BMK is a bookmark record, not a bookmark name (i.e., not a string).
Changes current buffer and point and returns nil, or signals a `file-error'."
(let ((file (bookmark-get-filename bmk))
(buf (bookmark-prop-get bmk 'buffer))
(d-buffer (bookmark-prop-get bmk 'd-buffer))
(page (bookmark-prop-get bmk 'page))
(forward-str (bookmark-get-front-context-string bmk))
(behind-str (bookmark-get-rear-context-string bmk))
(pos (bookmark-get-position bmk)))
(set-buffer
(cond
((and file (file-readable-p file) (not (buffer-live-p buf)))
(find-file-noselect file))
((and buf (get-buffer buf)))
(t (signal 'bookmark-error-no-filename (list 'stringp file)))))
(if page (djvu-goto-page page))
(if d-buffer
(set-buffer
(pcase d-buffer
(`read (djvu-ref read-buf))
(`text (djvu-ref text-buf))
(`annot (djvu-ref annot-buf))
(`shared (djvu-ref shared-buf))
(`bookmarks (djvu-ref bookmarks-buf))
(`outline (djvu-ref outline-buf)))))
(if pos (goto-char pos))
(when (and forward-str (search-forward forward-str (point-max) t))
(goto-char (match-beginning 0)))
(when (and behind-str (search-backward behind-str (point-min) t))
(goto-char (match-end 0)))
nil))
(provide 'djvu)