1 ;;; djvu.el --- edit and view Djvu files via djvused
3 ;; Copyright (C) 2011 Free Software Foundation, Inc.
5 ;; Author: Roland Winkler <winkler@gnu.org>
9 ;; This file is free software: you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; djvu.el is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with djvu.el. If not, see <http://www.gnu.org/licenses/>.
24 ;; djvu.el is a front end for the command-line program djvused
25 ;; from DjVuLibre, see http://djvu.sourceforge.net/
27 ;; This code requires you have the programs djvused, djview, and ddjvu
30 ;; A normal work flow is as follows:
32 ;; To visit a djvu file use `fjvu-find-file'. This command is the
33 ;; only entry point to this package. You may want to bind this command
34 ;; to a key you like. I use
36 ;; (global-set-key "\C-cd" 'djvu-find-file)
38 ;; If you use this command to visit file foo.djvu, it puts you into the
39 ;; (not editable) buffer foo@djvu. Normally, this buffer is all you
42 ;; The menu bar of this buffer lists most of the commands with their
43 ;; repsective key bindings. For example, you can:
45 ;; - Use `g' to go to the page you want. (Yes, this package operates on
46 ;; one page at a time. I guess that anything else would be too slow
47 ;; for large documents.)
49 ;; - Use `v' to (re)start djview using the position in foo.djvu
50 ;; matching where point is in foo@djvu. (I find djview fast enough
51 ;; for this, even for larger documents.)
53 ;; - To highlight a region in foo.djvu mark the corresponding region in
54 ;; foo@djvu (as usual, `transient-mark-mode' comes handy for this).
55 ;; Then type `h' and add a comment in the minibuffer if you like.
56 ;; Type C-x C-s to save this editing. Then type `v' to (re)start
57 ;; djview to show what you have done.
59 ;; - Type i to enable `djvu-image-mode', a minor mode displaying the
60 ;; current page as an image. Then
61 ;; drag-mouse-1 defines a region where to put a comment,
62 ;; C-drag-mouse-1 defines a region where to put a pushpin comment,
63 ;; S-drag-mouse-1 defines a region to highlight
65 ;; - The editing of the text, annotation and outline (bookmark) layers
66 ;; really happens in the buffers foo@djvu-t.el, foo@djvu-a.el, and
67 ;; foo@djvu-o.el. (The djvused syntax used in these buffers is so
68 ;; close to elisp that it was natural to give these buffers a
69 ;; djvu-edit-mode that is derived from emacs-lisp-mode.)
71 ;; You can check what is happening by switching to these buffers. The
72 ;; respective switching commands put point in these buffers such that
73 ;; it matches where you were in foo@djvu.
75 ;; In these buffers, the menu bar lists a few low-level commands
76 ;; available for editing these buffers directly. If you know the
77 ;; djvused syntax, sometimes it can also be helpful to do such
80 ;; But wait: The syntax in the annotations buffer foo@djvu-a.el is a
81 ;; slightly modified djvused syntax. djvused can only highlight
82 ;; rectangles. So the highlighting of larger regions of text must
83 ;; use multiple rectangles (i.e., multiple djvused "mapareas").
84 ;; To make editing easier, these are combined in the buffer foo@djvu-a.el.
85 ;; (Before saving these things, they are converted using the proper
88 ;; When you visit a djvu file, djvu-mode recognizes mapareas
89 ;; belonging together by checking that "everything else in these
90 ;; mapareas except for the rects" is the same. So if you entered a
91 ;; (unique) comment, this allows djvu-mode to combine all the
92 ;; mapareas when you visit such a file the second time. Without a
93 ;; comment, this fails!
95 ;; A second difference between what is displayed in the djvu buffers
96 ;; and the input/output of djvused refers to nonascii characters.
97 ;; I am using djvused from DjVuLibre-3.5.22 which handles utf-8 by
98 ;; backslash sequences. So djvu mode converts these backslash
99 ;; sequences into the corresponding utf-8 characters. (More recent
100 ;; versions of djvused can do this conversion, too.)
102 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
105 ;; (see /usr/share/doc/libdjvulibre-dev/djvu3spec.djvu)
107 ;; Supported area attributes rect oval poly line text
108 ;; (none)/(xor)/(border c) X X X X X
110 ;; (border_avis) X X X
111 ;; (hilite color) / (opacity o) X
112 ;; (arrow) / (width w) / (lineclr c) X
113 ;; (backclr c) / (textclr c) / (pushpin) X
115 ;; c = #RRGGBB t = thickness (1..32)
116 ;; o = opacity = 0..100
120 (defvar djvu-color-highlight "yellow"
121 "Default color for highlighting.")
123 (defvar djvu-color-himark "red"
124 "Default color for highmarking.")
126 (defvar djvu-color-url "blue"
127 "Default color for URLs.")
129 (defvar djvu-color-background "white"
130 "Default background.")
132 (defvar djvu-color-alist
133 ;; If the keys are strings, they are directly compatible with what
134 ;; we get back from something like `completing-read'.
135 '(("red" . "#FF0070")
136 ("green" . "#00FF00")
138 ("yellow" . "#EEFF00")
139 ("white" . "#FFFFFF"))
140 "Alist of colors for highlighting.")
142 (defvar djvu-opacity 50
143 "Default opacity for Highlighting.")
145 (defvar djvu-coords-justify 0.02
146 "Upper threshold for justifying rect coordinates.")
148 (defvar djvu-fill-column 50
149 "Fill column for Djvu annotations.")
151 (defvar djvu-all-buffer "*djvu*"
152 "Buffer for `all' operations.")
154 (defvar djvu-buffer-name-extensions
155 '("@djvu" "@djvu-t.el" "@djvu-a.el" "@djvu-o.el")
156 "Extensions for Djvu buffer names.
157 This is a list with four elements (READ TEXT ANNOT OUTLINE).")
159 (defvar djvu-resolve-url nil
160 "Flag for resolving internal URLs.
161 If 'long replace short page numbers by long FileIDs.
162 If 'short replace long FileIDs by short page numbers.
165 (defvar djvu-image-size 1024
166 "Size of internally displayed image.")
168 ;; Internal variables
170 (defvar djvu-test nil
171 "If non-nil do not process / delete djvused scripts.")
172 ;; (setq djvu-test t) (setq djvu-test nil)
175 "Internal look-up table (a vector) for each Djvu document.
176 For the different buffers of one Djvu document the buffer-local
177 value of this variable is the same vector holding all the
178 relevant information about this document. This way, we obtain a
179 \"document-local\" variable, where changes are seen in all buffers
180 refering to this Djvu document.")
181 (make-variable-buffer-local 'djvu-doc)
185 ;; "read" refers to the text-only display of djvu files inside emacs
186 ;; "view" refers to external graphical viewers (default djview)
190 (dolist (elt '(file basename text-buf read-buf annot-buf outline-buf
191 page pagemax page-id pagesize pos view-proc image))
192 (eval (list 'defsubst (intern (concat "djvu-doc-" (symbol-name elt)))
193 '(&optional doc) `(aref (or doc djvu-doc) ,count)))
194 (eval (list 'defsubst (intern (concat "djvu-doc-set-" (symbol-name elt)))
195 '(val &optional doc) `(aset (or doc djvu-doc) ,count val)))
196 (setq count (1+ count)))
197 (eval `(defconst djvu-doc-length ,count))))
199 (defun djvu-switch-text ()
200 "Switch to Djvu Text buffer."
202 (let ((pos (djvu-read-pos)))
203 (switch-to-buffer (djvu-doc-text-buf))
204 (djvu-locate-pos 'word pos)))
206 (defun djvu-switch-annot ()
207 "Switch to Djvu Annotations buffer."
209 (let ((pos (djvu-read-pos)))
210 (switch-to-buffer (djvu-doc-annot-buf))
211 (djvu-locate-pos 'rect pos)))
213 (defun djvu-switch-outline ()
214 "Switch to Djvu Outline buffer."
216 ;; Try to locate the current page in the outline buffer.
217 ;; If this page is not defined, try to locate the nearest preceding page.
218 (let ((page (djvu-doc-page)) pnt)
219 (with-current-buffer (djvu-doc-outline-buf)
220 (goto-char (point-min))
221 (if (looking-at "(bookmarks")
222 (while (and (< 0 page)
223 (not (setq pnt (re-search-forward
224 (format "\"#%d\"" page) nil t))))
225 (setq page (1- page)))))
226 (switch-to-buffer (djvu-doc-outline-buf))
227 (if pnt (goto-char pnt))))
229 (defun djvu-switch-read ()
230 "Switch to Djvu Read buffer."
232 (switch-to-buffer (djvu-doc-read-buf)))
234 (defun djvu-goto-page (page)
235 "Goto PAGE of Djvu document."
237 (let ((str (read-string (format "Page (f, 1-%d, l): " (djvu-doc-pagemax)))))
238 (list (cond ((string-match "\\`f" str) 1)
239 ((string-match "\\`l" str) (djvu-doc-pagemax))
240 ((string-match "\\`[[:digit:]]+\\'" str)
241 (string-to-number str))
242 (t (error "Page `%s' invalid" str))))))
243 (if (or (not (integerp page))
244 (<= page 0) (< (djvu-doc-pagemax) page))
245 (error "Page `%s' out of range" page))
246 (djvu-init-page djvu-doc page))
248 (defun djvu-next-page (n)
250 (djvu-goto-page (+ (djvu-doc-page) n)))
252 (defun djvu-prev-page (n)
254 (djvu-goto-page (- (djvu-doc-page) n)))
256 (defun djvu-set-color-highlight (color)
257 "Set color for highlighting based on `djvu-color-alist'."
258 (interactive (list (completing-read "Color: " djvu-color-alist nil t)))
259 (setq djvu-color-highlight color))
261 (defun djvu-kill-view (&optional doc)
262 (when (djvu-doc-view-proc doc)
263 (unless (memq (process-status (djvu-doc-view-proc doc))
265 (kill-process (djvu-doc-view-proc doc)))
266 (djvu-doc-set-view-proc nil doc)))
268 (defun djvu-kill-doc (&optional doc)
272 (mapc 'kill-buffer (list (djvu-doc-text-buf doc) (djvu-doc-read-buf doc)
273 (djvu-doc-annot-buf doc) (djvu-doc-outline-buf doc))))
275 (defsubst djvu-delete-file (script)
276 (unless djvu-test (delete-file script)))
278 (defun djvu-save (&optional doc query)
281 (let ((pos (djvu-read-pos))
282 (text-modified (buffer-modified-p (djvu-doc-text-buf doc)))
283 (annot-modified (buffer-modified-p (djvu-doc-annot-buf doc)))
284 (outline-modified (buffer-modified-p (djvu-doc-outline-buf doc)))
286 (when (and (or text-modified annot-modified outline-modified)
288 (yes-or-no-p (format "Save %s? " (djvu-doc-basename doc)))))
291 (setq script (make-temp-file "djvu-el-"))
292 (if text-modified (djvu-process-text script doc))
293 (if annot-modified (djvu-process-annot script doc))
294 (if outline-modified (djvu-process-outline script doc))
295 (djvu-djvused doc nil "-f" script "-s")
296 (dolist (buf (list (djvu-doc-text-buf doc) (djvu-doc-annot-buf doc)
297 (djvu-doc-outline-buf doc) (djvu-doc-read-buf doc)))
298 (with-current-buffer buf (set-buffer-modified-p nil)))
299 (if text-modified (djvu-locate-read-pos pos)))
300 (djvu-delete-file script)))))
302 (defun djvu-modified ()
303 "Mark Djvu Read buffer as modified if necessary.
304 Used in `post-command-hook' of the Djvu Outline, Text and Read buffers."
305 (with-current-buffer (djvu-doc-read-buf)
306 (set-buffer-modified-p (or (buffer-modified-p (djvu-doc-outline-buf))
307 (buffer-modified-p (djvu-doc-text-buf))
308 (buffer-modified-p (djvu-doc-annot-buf))))))
310 (defun djvu-process (&optional doc view)
312 (interactive (list djvu-doc t))
314 (if view (djvu-view doc)))
316 (defun djvu-djvused (doc buffer &rest args)
317 ;; BUFFER is nil if we update the Djvu file.
318 (unless (or buffer (file-writable-p (djvu-doc-file doc)))
319 (error "File `%s' not writable"
320 (abbreviate-file-name (djvu-doc-file doc))))
321 (unless (and (not buffer) djvu-test)
322 (let ((status (apply 'call-process "djvused" nil buffer nil
323 (djvu-doc-file doc) args)))
324 (unless (zerop status)
325 (error "Djvused error %s (args: %s)" status args)))))
327 (defun djvu-hide-hash (&optional recover)
328 (let* ((old (concat " " (if recover "@!@" "#") "\\([[:xdigit:]]\\)"))
329 (new (concat " " (if recover "#" "@!@") "\\1")))
330 (goto-char (point-min))
331 (while (re-search-forward old nil t) (replace-match new))))
333 (defun djvu-interactive-region ()
334 "Return active region for use in interactive calls."
337 (setq beg (region-beginning)
339 (setq beg (point) end (1+ (point))))
340 (cons (if (get-text-property beg 'word)
341 (djvu-property-beg beg 'word)
342 (next-single-property-change beg 'word nil end))
343 (if (get-text-property (1- end) 'word)
344 (djvu-property-end end 'word)
345 (previous-single-property-change end 'word nil beg)))))
347 (defun djvu-interactive-color (color)
348 "Return color specification for use in interactive calls."
349 (let ((colnum (or (and (consp current-prefix-arg)
350 (1- (/ (car current-prefix-arg) 4)))
351 (and (integerp current-prefix-arg)
352 current-prefix-arg))))
353 (if (and colnum (>= colnum (length djvu-color-alist)))
354 (error "Color undefined"))
355 (if colnum (car (nth colnum djvu-color-alist)) color)))
357 (defun djvu-interactive-url (&optional color interrupt)
358 "Return URL specification for use in interactive calls."
359 (unless color (setq color djvu-color-url))
360 (let ((fmt (format "(%s) Page URL: " (or color djvu-color-url)))
361 (page "") num ignore)
362 (while (and (not ignore)
363 (or (not (integerp (setq num (string-to-number page))))
364 (< num 1) (< (djvu-doc-pagemax) num)))
365 (setq page (read-string fmt))
366 (if (and interrupt (string= "" page)) (setq ignore t)))
367 (unless ignore (concat "#" page))))
369 (defsubst djvu-color-string-to-hex (color i)
370 "Convert rgb COLOR string (part I) to hex number."
371 (string-to-number (substring-no-properties
372 (cdr (assoc color djvu-color-alist))
373 (1+ (* i 2)) (+ 3 (* i 2))) 16))
375 (defun djvu-color-background (color background opacity)
376 "For rgb COLOR and BACKGROUND apply OPACITY.
377 Return the new rgb color string."
379 ;; Why opacity squared??
380 (a (/ (float (* opacity opacity)) 10000))
383 (setq str (concat str (format "%X"
384 (round (+ (* a (djvu-color-string-to-hex color i))
385 (* b (djvu-color-string-to-hex background i))))))))))
389 (defvar djvu-read-mode-map
390 (let ((km (make-sparse-keymap)))
391 (define-key km "i" 'djvu-image-mode)
392 (define-key km "v" 'djvu-view)
393 (define-key km "\C-c\C-v" 'djvu-view)
394 (define-key km "n" 'djvu-next-page)
395 (define-key km "p" 'djvu-prev-page)
396 (define-key km "g" 'djvu-goto-page)
397 (define-key km "k" 'djvu-kill-doc)
398 (define-key km "\C-c\C-c" 'djvu-process)
399 (define-key km "\C-x\C-s" 'djvu-save)
401 (define-key km "h" 'djvu-highlight)
402 (define-key km "u" 'djvu-url)
403 (define-key km "a" 'djvu-switch-annot)
404 (define-key km "A" 'djvu-display-annot-all)
406 (define-key km "c" 'djvu-comment)
407 (define-key km "C" 'djvu-comment-pushpin)
408 (define-key km "b" 'djvu-bookmark)
409 (define-key km "m" 'djvu-himark)
410 (define-key km "o" 'djvu-switch-outline)
412 (define-key km "s" 'djvu-split-word)
413 (define-key km "w" 'djvu-merge-words)
414 (define-key km "l" 'djvu-merge-lines)
416 (define-key km "t" 'djvu-switch-text)
417 (define-key km "T" 'djvu-display-text-all)
419 "Keymap for Djvu Read Mode.
420 This is a child of `special-mode-map'.")
423 djvu-read-menu djvu-read-mode-map "Djvu Menu"
425 ["View File" djvu-view t]
426 ["Image File" djvu-image-mode t]
427 ["Go to Page" djvu-goto-page t]
428 ["Process Doc" djvu-process t]
429 ["Save Doc" djvu-save t]
431 ["Split Word" djvu-split-word t]
432 ["Merge Words" djvu-merge-words t]
433 ["Merge Lines" djvu-merge-lines t]
434 ["Switch to Text" djvu-switch-text t]
436 ["Highlight Region" djvu-highlight t]
437 ["URL over Region" djvu-url t]
438 ["Himark Region" djvu-himark t]
439 ["Add Comment" djvu-comment t]
440 ["Add Comment w/pushpin" djvu-comment-pushpin t]
441 ["Switch to Annotations" djvu-switch-annot t]
443 ["Show all Text" djvu-display-text-all t]
444 ["Show all Annotations" djvu-display-annot-all t]
445 ["Resolve all URLs" djvu-resolve-all-urls t]
446 ["Process all Annotations" djvu-process-all t]
447 ["Remove Annot / Outline" djvu-make-clean t]
449 ["Add Bookmark" djvu-bookmark t]
450 ["Switch to Outline" djvu-switch-outline t]
452 ["Quit Djvu" quit-window t]
453 ["Kill Djvu buffers" djvu-kill-doc t]))
455 (define-derived-mode djvu-read-mode special-mode "Djview"
456 "Mode for reading Djvu files."
457 (setq mode-line-buffer-identification
458 (list 24 (buffer-name) " "
459 '(:eval (format "p%d" (djvu-doc-page))))))
461 (defvar djvu-edit-mode-map
462 (let ((km (make-sparse-keymap)))
463 (define-key km "\C-c\C-r" 'djvu-switch-read)
464 (define-key km "\C-c\C-g" 'djvu-goto-page)
465 (define-key km "\C-c\C-s" 'djvu-split-word-internal)
466 (define-key km "\C-c\C-m" 'djvu-merge-words-internal)
467 (define-key km "\C-c\M-m" 'djvu-merge-lines-internal)
468 (define-key km "\C-c\C-c" 'djvu-process)
469 (define-key km "\C-x\C-s" 'djvu-save)
470 (define-key km "\C-c\C-v" 'djvu-view)
471 (define-key km "\C-c\C-k" 'djvu-kill-doc)
473 "Keymap for Djvu Annot Mode.
474 This is a child of `text-mode-map'.")
477 djvu-annot-menu djvu-edit-mode-map "Djvu Menu"
479 ["Go to Page" djvu-goto-page t]
480 ["Switch to Read" djvu-switch-read t]
481 ["Process Doc" djvu-process t]
482 ["Save Doc" djvu-save t]
484 ["Switch to Text" djvu-switch-text t]
485 ["Split Word" djvu-split-word-internal t]
486 ["Merge Words" djvu-merge-words-internal t]
487 ["Merge Lines" djvu-merge-lines-internal t]
489 ["Switch to Annot" djvu-switch-annot t]
491 ["Quit Djvu" quit-window t]
492 ["Kill Djvu buffers" djvu-kill-doc t]))
494 (define-derived-mode djvu-edit-mode emacs-lisp-mode "Djvu Edit"
495 "Mode for editing (parts of) Djvu files."
496 (setq mode-line-buffer-identification
497 (list 24 (buffer-name) " "
498 '(:eval (format "p%d" (djvu-doc-page))))))
503 (defun djvu-find-file (file &optional page view)
504 "Read and edit Djvu FILE on PAGE.
505 If VIEW is non-nil start external viewer."
507 (list (read-file-name "Find Djvu file: " nil nil nil nil
509 (or (equal "djvu" (file-name-extension f))
510 (file-directory-p f))))
511 (prefix-numeric-value current-prefix-arg)))
512 (unless page (setq page 1))
513 (setq file (expand-file-name file))
514 (unless (file-regular-p file)
515 (error "Cannot open Djvu file `%s'." file))
516 ;; Initialize `djvu-doc' for FILE.
517 (let* ((basename (file-name-sans-extension
518 (file-name-nondirectory file)))
519 (read-buf (concat basename (nth 0 djvu-buffer-name-extensions)))
520 (text-buf (concat basename (nth 1 djvu-buffer-name-extensions)))
521 (annot-buf (concat basename (nth 2 djvu-buffer-name-extensions)))
522 (outline-buf (concat basename (nth 3 djvu-buffer-name-extensions)))
523 (buffers (list text-buf read-buf annot-buf outline-buf))
525 ;; Do nothing if we are already visiting FILE such that all required
526 ;; buffers are properly defined. If some buffers were killed
527 ;; do not attempt to recycle the remaining buffers.
528 (if (eval (cons 'and (mapcar 'get-buffer buffers)))
529 (with-current-buffer read-buf
531 (setq doc (make-vector djvu-doc-length nil))
532 (dolist (buf buffers)
533 (if (get-buffer buf) (kill-buffer buf)))
534 (djvu-doc-set-file file doc)
535 (djvu-doc-set-basename basename doc)
536 (djvu-doc-set-text-buf (get-buffer-create text-buf) doc)
537 (djvu-doc-set-read-buf (get-buffer-create read-buf) doc)
538 (djvu-doc-set-annot-buf (get-buffer-create annot-buf) doc)
539 (djvu-doc-set-outline-buf (get-buffer-create outline-buf) doc)
540 ;; Initialize all buffers.
541 (dolist (buf (list (djvu-doc-text-buf doc) (djvu-doc-annot-buf doc)
542 (djvu-doc-outline-buf doc)))
543 (with-current-buffer buf
546 (cd (file-name-directory (djvu-doc-file)))
547 (add-hook 'post-command-hook 'djvu-modified nil t)))
548 (with-current-buffer (djvu-doc-read-buf doc)
551 (cd (file-name-directory (djvu-doc-file)))
552 (add-hook 'post-command-hook 'djvu-modified nil t))
553 (djvu-init-page doc page))
554 (if view (djvu-view doc))
555 (switch-to-buffer read-buf)))
557 (defun djvu-init-page (&optional doc page)
558 "For Djvu DOC initialize PAGE."
559 (if (djvu-doc-pagemax doc) (djvu-save doc t))
560 (djvu-doc-set-pos nil doc)
561 (if page (djvu-doc-set-page page doc))
562 (let* ((doc (or doc djvu-doc))
563 (new (not (djvu-doc-pagemax doc)))
566 (djvu-djvused doc t "-e"
567 (format "%sselect %d; size; print-txt; print-ant;"
568 (if new "n; ls; print-outline; " "")
569 (djvu-doc-page doc)))
570 (goto-char (point-min))
573 (djvu-doc-set-pagemax (read (current-buffer)) doc)
577 (skip-chars-forward " \t\n")
578 (looking-at "\\(?:\\([0-9]+\\)[ \t]+\\)?\\([PIAT]\\)[ \t]+\\([0-9]+\\)[ \t]+\\([^ \t\n]+\\)$"))
580 ;; page-id is an alist with elements (PAGE-NUM . FILE-ID)
581 (push (cons (match-string 1) (match-string 4)) page-id))
582 (goto-char (match-end 0)))
583 (unless (eq (djvu-doc-pagemax doc) (length page-id))
584 (error "Page id list broken"))
585 (djvu-doc-set-page-id (nreverse page-id) doc))
587 (skip-chars-forward " \t\n")
588 (when (looking-at "(bookmarks")
589 (setq object (read (current-buffer)))
590 (djvu-decode-outline (cdr object))
591 (with-current-buffer (djvu-doc-outline-buf doc)
592 (insert "(bookmarks")
593 (let (print-escape-newlines)
594 (djvu-insert-outline (cdr object) " "))
596 (goto-char (point-min))
597 (set-buffer-modified-p nil)
598 (setq buffer-undo-list nil))))
601 (skip-chars-forward " \t\n")
602 (if (looking-at "width=\\([[:digit:]]+\\)[ \t]+height=\\([[:digit:]]+\\)$")
603 (djvu-doc-set-pagesize (cons (string-to-number (match-string 1))
604 (string-to-number (match-string 2))) doc)
605 (error "No pagesize"))
608 (goto-char (match-end 0))
609 (skip-chars-forward " \t\n")
610 (setq object (if (looking-at "(\\(page\\|column\\|region\\|para\\|line\\|word\\|char\\)")
611 (read (current-buffer))))
612 (djvu-decode-text object)
613 (with-current-buffer (djvu-doc-text-buf doc)
615 (djvu-insert-text object "")
617 (goto-char (point-min))
618 (set-buffer-modified-p nil)
619 (setq buffer-undo-list nil))
621 ;; Set up read buffer
622 (djvu-init-read doc object)
624 ;; Set up annotations buffer:
627 (narrow-to-region (point) (point-max))
630 (while (progn (skip-chars-forward " \t\n") (not (eobp)))
631 (if (looking-at "(\\(background\\|zoom\\|mode\\|align\\|maparea\\|metadata\\)\\>")
632 (push (read (current-buffer)) object)
633 (error "Unknown annotation `%s'" (buffer-substring-no-properties
634 (point) (line-end-position)))))
635 ;; To simplify the editing of annotations, identify mapareas (rect)
636 ;; sharing the same text string.
638 (if (not (eq 'maparea (car elt)))
640 (setcar (cdr elt) (decode-coding-string (nth 1 elt) 'utf-8))
641 (setcar (nthcdr 2 elt) (decode-coding-string (nth 2 elt) 'utf-8))
642 (cond ((eq 'rect (car (nth 3 elt))) ; rect
643 (let ((rect (djvu-rect (nth 3 elt)))
645 (setcdr (nthcdr 2 elt) (nthcdr 4 elt)) ; remove rect destructively
646 ;; The new elements of alist are cons cells, where the car is the
647 ;; maparea without rect, and the cdr is the list of rect areas.
648 ;; Even if we have just an empty string, we still want to massage
650 (if (or (string= "" (nth 2 elt))
651 (not (setq e (assoc elt alist))))
652 (push (cons elt (list rect)) alist)
653 (setcdr e (cons rect (cdr e))))))
654 ((eq 'text (car (nth 3 elt))) ; text
655 (setcar (nthcdr 3 elt) (djvu-rect (nth 3 elt)))
657 (t (push elt alist)))))
658 ;; Pretty print annotations.
659 (with-current-buffer (djvu-doc-annot-buf doc)
660 (let ((standard-output (current-buffer))
661 print-escape-newlines)
664 (cond ((consp (car elt)) ;; maparea with list of rects
666 (insert (format "(maparea %S\n %S\n ("
667 (djvu-resolve-url (nth 1 c) doc) (nth 2 c))
668 (mapconcat 'prin1-to-string (cdr elt) "\n ") ")\n " ; rect
669 (mapconcat 'prin1-to-string (nthcdr 3 c) " ") ; rest
671 ((not (eq 'maparea (car elt)))
673 ((eq 'text (car (nth 3 elt))) ; text
674 (insert (format "(maparea %S\n %S\n " (nth 1 elt) (nth 2 elt))
675 (mapconcat 'prin1-to-string (nthcdr 3 elt) " ") ; rest
677 (t (error "Djvu maparea %s undefined" (car (nth 3 elt)))))
680 (goto-char (point-max))
681 (set-buffer-modified-p nil)
682 (setq buffer-undo-list nil)))))
684 (defun djvu-resolve-url (url &optional doc)
685 "Resolve internal URLs. See variable `djvu-resolve-url'."
686 (cond ((eq 'long djvu-resolve-url)
687 ;; Replace page number by file id
688 (cond ((string-match "\\`#[0-9]+\\'" url)
689 (let ((page-id (assoc (substring-no-properties url 1)
690 (djvu-doc-page-id doc))))
692 (concat "#" (cdr page-id))
693 (error "Page id broken: %s" url))))
694 ((string-match "\\`#" url)
695 (if (rassoc (substring-no-properties url 1)
696 (djvu-doc-page-id doc))
698 (error "Page id broken: %s" url)))
699 (t url))) ; some other URL
700 ((eq 'short djvu-resolve-url)
701 ;; Replace file id by page number
702 (cond ((string-match "\\`#[0-9]+\\'" url)
704 ((string-match "\\`#" url)
705 (let ((page-id (rassoc (substring-no-properties url 1)
706 (djvu-doc-page-id doc))))
708 (concat "#" (car page-id))
709 (error "Page id broken: %s" url))))
710 (t url))) ; some other URL
711 (t url))) ; do nothing
713 (defun djvu-resolve-all-urls (dir)
714 "Resolve all internal URLs in a Djvu file."
716 (list (intern (completing-read "Direction: " '((long) (short)) nil t))))
717 (if (djvu-modified) (error "Djvu file should be saved"))
718 (let ((page-id (djvu-doc-page-id djvu-doc))
719 (djvu-all-buffer (generate-new-buffer " *djvu*"))
720 (djvu-resolve-url dir))
721 (djvu-display-annot-all)
722 (with-current-buffer djvu-all-buffer
723 (goto-char (point-min))
724 (cond ((eq dir 'long)
725 (while (re-search-forward "^(maparea[ \t]+\"#\\([0-9]+\\)\"" nil t)
726 (replace-match (cdr (assoc (match-string 1) page-id))
729 (while (re-search-forward "^(maparea[ \t]+\"#\\([^\"]+\\)\"" nil t)
730 (replace-match (car (rassoc (match-string 1) page-id))
733 (kill-buffer djvu-all-buffer)
734 (with-current-buffer (djvu-doc-outline-buf)
735 (set-buffer-modified-p t))
738 (defun djvu-rect (rect &optional back)
739 "Convert (rect xmin ymin width height) to (rect xmin ymin xmax ymax).
740 If BACK is non-nil do inverse transformation."
741 (let* ((f (if back '- '+))
742 (lst (list (nth 0 rect) (nth 1 rect) (nth 2 rect)
743 (funcall f (nth 3 rect) (nth 1 rect))
744 (funcall f (nth 4 rect) (nth 2 rect)))))
745 ;; Only for back transforms we might get an error...
746 (if (or (> 0 (nth 3 lst)) (> 0 (nth 4 lst)))
747 (error "Annotation rect dimensions %s, %s" (nth 3 lst) (nth 4 lst)))
750 (defun djvu-view (&optional doc)
751 "Start Djview for DOC."
752 (interactive (list djvu-doc))
753 (if (not (window-system))
754 (message "No window system available")
756 (let* ((djvu-doc doc)
757 (pos (or (djvu-doc-pos) (djvu-read-pos)))
758 (px (/ (float (car pos))
759 (float (car (djvu-doc-pagesize)))))
760 (py (- 1 (/ (float (cdr pos))
761 (float (cdr (djvu-doc-pagesize))))))
762 process-connection-type) ; Use a pipe.
763 (if (or (< px 0) (< 1 px) (< py 0) (< 1 py))
764 (error "px=%s, py=%s out of range" px py))
765 (djvu-doc-set-pos nil)
766 (djvu-doc-set-view-proc
767 (start-process "djview" nil "djview"
768 (format "-page=%d" (djvu-doc-page))
769 (format "-showposition=%06f,%06f" px py)
774 (defun djvu-split-word (pos)
775 "Split word at position POS.
776 This command operates on the read buffer."
778 (let ((beg (djvu-property-beg pos 'word))
779 (rpos (djvu-read-pos pos)))
780 (with-current-buffer (djvu-doc-text-buf)
781 (djvu-split-word-internal (djvu-locate-pos 'word rpos)
783 ;; Things get rather confusing without updating the read buffer.
784 ;; So we better save everything.
787 (defun djvu-split-word-internal (wpos split)
788 "Split word at position WPOS at character position SPLIT.
789 This command operates on the text buffer."
792 (pps (parse-partial-sexp (line-beginning-position) pnt)))
793 (unless (nth 3 pps) (error "Not inside string"))
794 (list pnt (1- (- pnt (nth 8 pps))))))
797 (skip-chars-forward " \t")
799 (let ((indent (buffer-substring-no-properties
800 (line-beginning-position) wpos))
804 (setq word (read (current-buffer)))
805 (unless (eq 'word (car word)) (error "invalid")))
806 (error (error "Syntax error in raw text")))
807 (if (or (< split 1) (<= (length (nth 5 word)) split))
808 (error "nothing to split"))
809 (delete-region wpos (point))
810 ;; To split the bounding box horizontally, we take the fraction
811 ;; of the number of characters in each fragment. This scheme
812 ;; is only approximate, but it is better than nothing.
813 (let ((frac (round (* (/ (float split) (length (nth 5 word)))
814 (- (nth 3 word) (nth 1 word))))))
815 (djvu-insert-text (list 'word (nth 1 word) (nth 2 word)
816 (+ (nth 1 word) frac) (nth 4 word)
817 (substring (nth 5 word) 0 split)) "")
819 (djvu-insert-text (list 'word (+ (nth 1 word) frac 1) (nth 2 word)
820 (nth 3 word) (nth 4 word)
821 (substring (nth 5 word) split)) ""))))
823 (defun djvu-merge-words (beg end)
824 "Merge words between positions BEG and END.
825 This command operates on the read buffer."
827 (let ((bpos (djvu-read-pos beg))
828 (epos (djvu-read-pos (1- end))))
829 (with-current-buffer (djvu-doc-text-buf)
830 (djvu-merge-words-internal (djvu-locate-pos 'word bpos)
831 (djvu-locate-pos 'word epos))))
832 ;; Things get rather confusing without updating the read buffer.
833 ;; So we better save everything.
836 (defun djvu-merge-words-internal (beg end)
837 "Merge words between positions BEG and END.
838 This command operates on the text buffer."
842 (if (bolp) (setq end (1- end)))
845 (skip-chars-forward " \t")
848 (while (< (point) end)
849 (push (read (current-buffer)) words)
850 (unless (eq 'word (caar words)) (error "invalid")))
851 (error (error "Syntax error in raw text")))
852 (delete-region beg (point))
853 (let ((object (apply 'list 'word 0 0 0 0 (nreverse words))))
854 (djvu-process-text-bbox object 0 (make-vector 3 nil))
855 (setcdr (nthcdr 4 object) (list (mapconcat (lambda (w) (nth 5 w))
856 (nthcdr 5 object) "")))
857 (djvu-insert-text object "")))
860 (defun djvu-merge-lines (beg end)
861 "Merge lines between positions BEG and END.
862 This command operates on the read buffer."
864 (let ((bpos (djvu-read-pos beg))
865 (epos (djvu-read-pos (1- end))))
866 (with-current-buffer (djvu-doc-text-buf)
867 (djvu-merge-lines-internal (djvu-locate-pos 'word bpos)
868 (djvu-locate-pos 'word epos))))
869 ;; Things get rather confusing without updating the read buffer.
870 ;; So we better save everything.
873 (defun djvu-merge-lines-internal (beg end)
874 "Merge lines between positions BEG and END.
875 This command operates on the text buffer."
877 ;; Calculate proper value of END
880 (unless (looking-at "[ \t]*(line ")
881 (re-search-backward "^[ \t]*(line ")
884 ;; Calculate proper value of BEG
887 (unless (looking-at "[ \t]*(line ")
888 (re-search-backward "^[ \t]*(line "))
889 (skip-chars-forward " \t")
891 (unless (< beg end) (error "Nothing to merge"))
892 ;; Parsing fails if the words belong to different paragraphs,
893 ;; regions or columns. We would have to determine the lowest common
894 ;; object level of these words. Then we could possibly merge
895 ;; everything (!) within this level
896 (if (re-search-forward "^[ \t]*\\(?:para\\|region\\|column\\)" end t)
897 (error "Cannot merge paragraphs, regions or columns"))
901 (while (<= (point) end)
902 (cond ((looking-at "[ \t]*(word ")
903 (push (read (current-buffer)) words))
904 ((not (looking-at "[ \t]*(line "))
907 (error (error "Syntax error in raw text")))
910 (while (let ((start (point)))
913 (progn (goto-char start) nil))))
914 (delete-region beg (point))
916 (let ((indent (buffer-substring-no-properties
917 (line-beginning-position) (point)))
918 (object (apply 'list 'line 0 0 0 0 (nreverse words))))
919 (djvu-process-text-bbox object 0 (make-vector 3 nil))
920 (delete-region (line-beginning-position) (point))
921 (djvu-insert-text object indent)))
924 (defun djvu-decode-text (object &optional encode)
925 (if (stringp (nth 5 object))
926 (setcar (nthcdr 5 object)
928 (encode-coding-string (nth 5 object) 'utf-8)
929 (decode-coding-string (nth 5 object) 'utf-8)))
930 (dolist (elt (nthcdr 5 object))
931 (djvu-decode-text elt encode))))
933 (defun djvu-insert-text (object indent)
934 ;; This function is called recursively.
936 (mapconcat 'prin1-to-string
937 (list (nth 0 object) (nth 1 object) (nth 2 object)
938 (nth 3 object) (nth 4 object)) " "))
939 (let ((tail (nthcdr 5 object))
940 (indent (concat indent " ")))
941 (if (stringp (car tail))
942 ;; use `prin1-to-string' as we use this function both for
943 ;; utf-8 and encoded stuff.
944 (insert " " (prin1-to-string (car tail)) ")")
947 (djvu-insert-text elt indent))
950 (defun djvu-process-text (script &optional doc)
951 (let ((doc (or doc djvu-doc))
953 (with-current-buffer (djvu-doc-text-buf doc)
955 (goto-char (point-min))
957 (setq object (read (current-buffer)))
958 (error (error "Syntax error in raw text")))
959 (skip-chars-forward " \t\n")
960 ;; We should have swallowed all raw text.
962 (error "Syntax error in raw text (end of buffer)"))))
963 (djvu-process-text-bbox object 0 (make-vector 7 nil))
964 ;; Update read buffer
965 (djvu-init-read doc object)
966 ;; FIXME: Should we also update the text buffer?
967 ;; A transparent solution would update only the part of the buffer
968 ;; that we actually changed so that `undo' works as expected.
969 (djvu-decode-text object t)
971 (insert (format "select %d\nremove-txt\nset-txt\n" (djvu-doc-page doc)))
972 (djvu-insert-text object "")
974 (write-region nil nil script t 0))))
976 (defun djvu-process-text-bbox (object depth coords)
977 "Evaluate bounding box for text OBJECT recursively."
978 (if (stringp (nth 5 object))
979 (aset coords depth (vector (nth 1 object) (nth 2 object)
980 (nth 3 object) (nth 4 object)))
981 (let ((depth1 (1+ depth))
983 (aset coords depth nil)
984 (dolist (elt (nthcdr 5 object))
985 (djvu-process-text-bbox elt depth1 coords)
986 (if (setq coord (aref coords depth))
987 (let ((coord1 (aref coords depth1)))
988 (aset coord 0 (min (aref coord 0) (aref coord1 0)))
989 (aset coord 1 (min (aref coord 1) (aref coord1 1)))
990 (aset coord 2 (max (aref coord 2) (aref coord1 2)))
991 (aset coord 3 (max (aref coord 3) (aref coord1 3))))
992 (aset coords depth (copy-sequence (aref coords depth1)))))
993 (if (setq coord (aref coords depth))
994 (setcdr object (apply 'list (aref coord 0) (aref coord 1)
995 (aref coord 2) (aref coord 3)
997 (error "No coords??")))))
999 (defun djvu-display-text-all ()
1000 "Display text for all pages."
1002 (let ((doc djvu-doc)
1003 (buf (get-buffer-create djvu-all-buffer)))
1004 ;; Put this in a separate buffer!
1005 (with-current-buffer buf
1006 (let (buffer-read-only)
1009 (djvu-djvused doc t "-e" "output-txt")
1010 (goto-char (point-min)))
1011 (set-buffer-modified-p nil)
1012 (setq buffer-read-only t))
1013 (switch-to-buffer buf)))
1015 (defun djvu-process-all ()
1016 "Process all pages. Use at your own risk. You get what you want."
1018 (let ((buf (get-buffer djvu-all-buffer))
1020 (unless buf (error "No buffer `%s'" buf))
1021 (unless djvu-doc (error "No Djvu doc"))
1024 (setq script (make-temp-file "djvu-el-"))
1025 (with-temp-file script (insert-buffer-substring buf))
1026 (djvu-djvused djvu-doc nil "-f" script "-s"))
1027 (djvu-delete-file script))))
1031 (defun djvu-init-read (doc object)
1032 (with-current-buffer (djvu-doc-read-buf doc)
1033 (let (buffer-read-only)
1035 (djvu-insert-read object))
1036 (set-buffer-modified-p nil)
1037 (setq buffer-read-only t)
1038 (goto-char (point-min))
1041 (defun djvu-insert-read (object)
1042 "Display text OBJECT."
1043 ;; This function is called recursively.
1044 (let ((opoint (point))
1045 (tail (nthcdr 5 object)))
1046 (if (stringp (car tail))
1047 (insert (decode-coding-string (car tail) 'utf-8))
1048 (let* ((obj (caar tail))
1049 (sep (cond ((eq 'line obj) "\n")
1050 ((eq 'word obj) "\s")
1051 ((eq 'char obj) nil)
1054 (while (setq elt (pop tail))
1055 (djvu-insert-read elt)
1056 (if (and sep tail (not (looking-back sep)))
1058 (put-text-property opoint (point) (car object)
1059 (vector (nth 1 object) (nth 2 object)
1060 (nth 3 object) (nth 4 object)))))
1062 (defun djvu-read-pos (&optional point)
1063 "Return Djvu position (x . y) of POINT in Djvu Read buffer."
1064 (with-current-buffer (djvu-doc-read-buf)
1065 ;; An empty djvu page gives us something like (page 0 0 0 0 "")
1066 (if (= (point-min) (point-max))
1067 ;; Take the center of an empty page
1068 (cons (/ (car (djvu-doc-pagesize)) 2)
1069 (/ (cdr (djvu-doc-pagesize)) 2))
1070 (unless point (setq point (point)))
1071 (djvu-mean-pos ; Return mean coordinates
1072 (or (get-text-property point 'word)
1073 (get-text-property (1- point) 'word)
1075 ;; Search backward because more often point is at the end
1076 ;; of region we operated on
1077 (1- (previous-single-property-change point 'word)) 'word))))))
1079 (defun djvu-mean-pos (reg)
1080 "For region REG return mean coordinates (x . y)."
1081 ;; This works both for REG being vectors and lists.
1082 (cons (/ (+ (elt reg 0) (elt reg 2)) 2)
1083 (/ (+ (elt reg 1) (elt reg 3)) 2)))
1085 (defun djvu-locate-pos (object pos)
1086 "Locate OBJECT at position POS in the text or annotation buffer.
1087 If found, return corresponding position. Otherwise, return nil."
1088 (goto-char (point-min))
1090 (let ((re (concat "\\<" (symbol-name object) "\\> +"
1091 (mapconcat 'identity
1092 (make-list 4 "\\([[:digit:]]+\\)") " +")
1095 (while (and (not done)
1096 (re-search-forward re nil t))
1097 (let ((x1 (string-to-number (match-string 1)))
1098 (x2 (string-to-number (match-string 3)))
1099 (y1 (string-to-number (match-string 2)))
1100 (y2 (string-to-number (match-string 4))))
1101 (setq done (and (<= x1 (car pos))
1104 (<= (cdr pos) y2)))))
1106 (goto-char (point-min)) nil))))
1108 (defsubst djvu-dist (width height)
1109 (+ (* width width) (* height height)))
1111 (defun djvu-locate-read-pos (pos)
1112 "Locate POS in Djvu Read buffer. Return corresponding position."
1113 (with-current-buffer (djvu-doc-read-buf)
1115 (goto-char (point-min))
1116 (let ((hpos (car pos)) (vpos (cdr pos))
1117 (good-dist (djvu-dist (car (djvu-doc-pagesize))
1118 (cdr (djvu-doc-pagesize))))
1119 (pnt (point-min)) (good-pnt (point-min))
1122 (when (setq word (get-text-property pnt 'word))
1123 (setq dist (djvu-dist (- (/ (+ (aref word 0) (aref word 2)) 2) hpos)
1124 (- (/ (+ (aref word 1) (aref word 3)) 2) vpos)))
1125 (if (< dist good-dist)
1126 (setq good-pnt pnt good-dist dist)))
1127 (setq pnt (next-single-property-change pnt 'word))))
1128 (goto-char good-pnt)))))
1130 ;;; Djvu Annotation mode
1132 (defun djvu-comment-interactive (&optional border backclr textclr pushpin)
1133 "Interactive spec for `djvu-comment' and friends."
1134 (let ((pos (djvu-read-pos))
1135 (pagesize (djvu-doc-pagesize))
1136 (color (djvu-interactive-color djvu-color-highlight)))
1137 (list "" (read-string (format "(%s) Text: " color))
1138 (list (car pos) (cdr pos)
1139 (+ (car pos) (/ (car pagesize) 2))
1140 (+ (cdr pos) (/ (cdr pagesize) 30)))
1143 (djvu-color-background color djvu-color-background
1147 (defsubst djvu-insert-color (key color)
1149 (format " (%s %s)" key
1150 (cond ((string-match "\\`#" color) color)
1151 ((cdr (assoc color djvu-color-alist)))
1152 (t (error "Color `%s' undefined" color))))
1155 (defun djvu-comment (url text rect &optional border backclr textclr pushpin)
1156 "Using URL and TEXT, highlight RECT.
1157 This defines a rect area for djvused."
1158 (interactive (djvu-comment-interactive))
1159 (with-current-buffer (djvu-doc-annot-buf)
1160 (goto-char (point-max))
1161 (let (print-escape-newlines)
1162 (insert (format "(maparea %S\n %S\n " url (djvu-fill text))
1163 (apply 'format "(text %d %d %d %d)" rect)
1164 (if border (format " (%s)" border) "")
1165 (djvu-insert-color "backclr" backclr)
1166 (djvu-insert-color "textclr" textclr)
1167 (if pushpin " (pushpin)" "")
1171 (defun djvu-comment-pushpin (url text rect
1172 &optional border backclr textclr pushpin)
1173 (interactive (djvu-comment-interactive nil nil nil t))
1174 (djvu-comment url text rect border backclr textclr pushpin))
1176 (defun djvu-himark (beg end url text &optional color opacity border)
1177 "Himark region between BEG and END.
1178 This highlights the region between BEG and END and creates a bookmark entry."
1180 (let ((region (djvu-interactive-region)))
1181 (list (car region) (cdr region) "" ""
1182 djvu-color-himark djvu-opacity 'none)))
1183 (djvu-highlight beg end url text color opacity border)
1184 (djvu-bookmark (buffer-substring-no-properties beg end) (djvu-doc-page)))
1186 (defun djvu-url (beg end url text &optional color opacity border)
1188 (let* ((region (djvu-interactive-region))
1189 (color (djvu-interactive-color djvu-color-url))
1190 (url (djvu-interactive-url color)))
1191 (list (car region) (cdr region) url "" color djvu-opacity 'xor)))
1192 (djvu-highlight beg end url text color opacity border))
1194 (defun djvu-highlight (beg end url text &optional color opacity border)
1195 "Highlight region between BEG and END, add annotation TEXT."
1197 (let ((region (djvu-interactive-region))
1198 (color (djvu-interactive-color djvu-color-highlight)))
1199 (list (car region) (cdr region) ""
1200 (read-string (format "(%s) Annotation: " color))
1201 color djvu-opacity 'none)))
1203 (unless (get-text-property beg 'word)
1204 (error "Start position `%s' not a word" beg))
1205 (unless (get-text-property (1- end) 'word)
1206 (error "End position `%s' not a word" end))
1207 (let ((words (djvu-region-count beg end 'word))
1208 (lines (djvu-region-count beg end 'line))
1209 (paras (djvu-region-count beg end 'para))
1210 (regions (djvu-region-count beg end 'region))
1211 (columns (djvu-region-count beg end 'column))
1213 (unless (and (>= 1 paras) (>= 1 regions) (>= 1 columns))
1214 (error "Region spans multiple paragraphs"))
1217 (setq coords (list (djvu-scan-coords beg end 'word)))
1220 (let ((c1 (djvu-scan-coords beg (djvu-property-end (1+ beg) 'line) 'word))
1221 (c2 (djvu-scan-coords (djvu-property-beg (1- end) 'line) end 'word)))
1222 ;; If BEG is beginning of first line, both lines share same left margin.
1223 (if (and (= beg (djvu-property-beg beg 'line))
1224 (djvu-coords-justify t c1 c2))
1225 (djvu-justify-coords 'min 0 c1 c2))
1226 ;; If END is end of second line, both lines share same right margin.
1227 (if (and (= end (djvu-property-end end 'line))
1228 (djvu-coords-justify nil c2 c1))
1229 (djvu-justify-coords 'max 2 c1 c2))
1230 (if (<= (aref c1 0) (aref c2 2))
1231 ;; Lower bound of upper box and upper bound of lower box coincide.
1232 (let ((tmp (/ (+ (aref c1 1) (aref c2 3)) 2)))
1233 (aset c1 1 tmp) (aset c2 3 tmp)))
1234 (setq coords (list c1 c2)))
1236 (let* ((l1e (djvu-property-end (1+ beg) 'line))
1237 (l2b (djvu-property-beg (1- end) 'line))
1238 (c1 (djvu-scan-coords beg l1e 'word))
1239 (ci (djvu-scan-coords (1+ l1e) (1- l2b) 'line))
1240 (c2 (djvu-scan-coords l2b end 'word)))
1241 ;; If BEG is beginning of first line, all lines share same left margin.
1242 (cond ((and (= beg (djvu-property-beg beg 'line))
1243 (djvu-coords-justify t c1 ci c2))
1244 (djvu-justify-coords 'min 0 c1 ci c2))
1245 ((djvu-coords-justify t ci c2)
1246 (djvu-justify-coords 'min 0 ci c2)))
1247 ;; If END is end of last line, all lines share same right margin.
1248 (cond ((and (= end (djvu-property-end end 'line))
1249 (djvu-coords-justify nil c2 ci c1))
1250 (djvu-justify-coords 'max 2 c1 ci c2))
1251 ((djvu-coords-justify nil c1 ci)
1252 (djvu-justify-coords 'max 2 c1 ci)))
1253 (let ((tmp1 (/ (+ (aref c1 1) (aref ci 3)) 2))
1254 (tmp2 (/ (+ (aref ci 1) (aref c2 3)) 2)))
1255 ;; Lower bound of upper boxes and upper bound of lower boxes coincide.
1256 (aset c1 1 tmp1) (aset ci 3 tmp1)
1257 (aset ci 1 tmp2) (aset c2 3 tmp2))
1258 (setq coords (list c1 ci c2)))))
1260 (djvu-highlight-region url text coords color opacity border)))
1262 (defun djvu-highlight-region (url text coords &optional color opacity border)
1263 "Using URL and TEXT, highlight COORDS.
1264 This defines a hilite area for djvused."
1265 ;; Record position where annotation was made.
1266 (let ((posl (mapcar 'djvu-mean-pos coords))
1267 (n (length coords)))
1268 (djvu-doc-set-pos (cons (/ (apply '+ (mapcar 'car posl)) n)
1269 (/ (apply '+ (mapcar 'cdr posl)) n))))
1270 ;; Insert in Annotations buffer.
1271 (with-current-buffer (djvu-doc-annot-buf)
1272 (goto-char (point-max))
1273 (let (print-escape-newlines)
1274 (insert (format "(maparea %S\n %S\n (" url (djvu-fill text))
1276 (lambda (rect) (apply 'format "(rect %d %d %d %d)" (append rect nil)))
1278 (djvu-insert-color "hilite" color)
1279 (if opacity (format " (opacity %s)" opacity) "")
1280 (if border (format " (%s)" border) "")
1284 (defun djvu-fill (text)
1285 "Fill string TEXT using `djvu-fill-column'."
1286 (if djvu-fill-column
1289 (let ((fill-column djvu-fill-column))
1290 (fill-region (point-min) (point-max)))
1291 (buffer-substring-no-properties
1292 (point-min) (point-max)))
1295 (defun djvu-property-beg (pnt prop)
1296 ;; Assume that PNT has PROP. Otherwise we would not know whether
1297 ;; to search for it before or after PNT.
1298 (let ((p1 (get-text-property pnt prop)))
1299 (unless p1 (error "Position %s does not have property %s" pnt prop))
1300 (if (> pnt (point-min))
1301 (let ((p0 (get-text-property (1- pnt) prop)))
1303 (setq pnt (previous-single-property-change
1304 pnt prop nil (point-min))))))
1307 (defun djvu-property-end (pnt prop)
1308 ;; Assume that (1- PNT) has PROP. Otherwise we would not know whether
1309 ;; to search for it before or after PNT.
1310 (let ((p1 (get-text-property (1- pnt) prop)))
1311 (unless p1 (error "Position %s does not have property %s" pnt prop))
1312 (if (< pnt (point-max))
1313 (let ((p0 (get-text-property pnt prop)))
1315 (setq pnt (next-single-property-change
1316 (1- pnt) prop nil (point-max))))))
1319 (defun djvu-coords-justify (left &rest ci)
1320 "Return non-nil if rect coordinates CI shall be justified horizontally.
1321 If LEFT is nil analyze left boundaries of CI, otherwise the right boundaries."
1322 (let ((xl (apply 'min (mapcar (lambda (c) (aref c 0)) ci)))
1323 (xr (apply 'max (mapcar (lambda (c) (aref c 2)) ci))))
1324 (> djvu-coords-justify
1325 (/ (apply 'max (mapcar (lambda (cj)
1326 (abs (float (if left (- (aref cj 0) xl)
1327 (- xr (aref cj 2))))))
1329 (float (- xr xl))))))
1331 (defun djvu-justify-coords (fun n &rest ci)
1332 "Pass Nth elements of arrays CI to function FUN.
1333 Set these elements to return value of FUN.
1334 If FUN is `min' or `max' these elements are set to the respective minimum
1335 or maximum among the Nth elements of all arrays CI."
1336 (let ((tmp (apply fun (mapcar (lambda (c) (aref c n)) ci))))
1340 (defun djvu-scan-coords (beg end prop)
1341 "Between BEG and END calculate total bounding box for PROP."
1342 ;; Assume that BEG has PROP.
1343 (let ((coords (copy-sequence (get-text-property beg prop)))
1345 (while (and (/= pnt end)
1346 (setq pnt (next-single-property-change pnt prop nil end)))
1347 (when (setq val (get-text-property pnt prop))
1348 (aset coords 0 (min (aref coords 0) (aref val 0)))
1349 (aset coords 1 (min (aref coords 1) (aref val 1)))
1350 (aset coords 2 (max (aref coords 2) (aref val 2)))
1351 (aset coords 3 (max (aref coords 3) (aref val 3)))))
1354 (defun djvu-region-count (beg end prop)
1355 "Count regions between BEG and END with distinct non-nil values of PROP."
1358 (while (and (/= pnt end)
1359 (setq pnt (next-single-property-change pnt prop nil end)))
1360 (if (get-text-property (1- pnt) prop)
1361 (setq count (1+ count))))
1364 (defun djvu-process-annot (script &optional doc)
1365 (let ((doc djvu-doc) object)
1367 (insert-buffer-substring (djvu-doc-annot-buf doc))
1369 (goto-char (point-min))
1370 (while (progn (skip-chars-forward " \t\n") (not (eobp)))
1371 (if (looking-at "(\\(background\\|zoom\\|mode\\|align\\|maparea\\|metadata\\)\\>")
1373 (push (read (current-buffer)) object)
1374 (error (error "Syntax error in annotations")))
1375 (error "Unknown annotation `%s'" (buffer-substring-no-properties
1376 (point) (line-end-position))))))
1377 (setq object (nreverse object))
1378 (dolist (elt object)
1379 (when (eq 'maparea (car elt))
1381 (setcar (cdr elt) (encode-coding-string (djvu-resolve-url (nth 1 elt)) 'utf-8))
1383 (setcar (nthcdr 2 elt) (encode-coding-string (nth 2 elt) 'utf-8))))
1386 (let ((standard-output (current-buffer))
1387 (print-escape-newlines t)
1389 (insert (format "select %d\nremove-ant\nset-ant\n"
1390 (djvu-doc-page doc)))
1391 (dolist (elt object)
1392 (cond ((not (eq 'maparea (car elt)))
1395 ((consp (car (nth 3 elt))) ; rect
1396 (dolist (e (nth 3 elt))
1397 (insert (prin1-to-string
1398 (apply 'list (car elt) (nth 1 elt) (nth 2 elt)
1399 (djvu-rect e t) (nthcdr 4 elt))) "\n")))
1400 ((eq 'text (car (nth 3 elt))) ; text
1401 (insert (prin1-to-string
1402 (apply 'list (car elt) (nth 1 elt) (nth 2 elt)
1403 (djvu-rect (nth 3 elt) t)
1404 (nthcdr 4 elt))) "\n"))
1405 (t (error "Djvu maparea %s undefined" (car (nth 3 elt))))))
1408 (write-region nil nil script t 0))))
1410 (defun djvu-display-annot-all (&optional display)
1411 "Print annotations for all pages."
1412 (interactive (list t))
1413 (let ((doc djvu-doc)
1414 (buf (get-buffer-create djvu-all-buffer)))
1415 ;; Put this in a separate buffer!
1416 (with-current-buffer buf
1417 (let (buffer-read-only)
1420 (djvu-djvused doc t "-e" "output-ant")
1421 (goto-char (point-min))
1422 (while (re-search-forward "^(maparea" nil t)
1423 (forward-sexp) ; jump over URL
1424 ;; replace newlines within text
1425 (let ((limit (save-excursion (forward-sexp) (point))))
1426 (while (search-forward "\\n" limit t)
1427 (replace-match "\n"))))
1428 (goto-char (point-min)))
1429 (set-buffer-modified-p nil)
1430 (setq buffer-undo-list nil))
1431 (if display (switch-to-buffer buf))))
1433 ;;; Djvu Outline mode
1435 (defun djvu-bookmark (text page)
1438 (let ((region (djvu-interactive-region)))
1439 (list (read-string "Bookmark: " (buffer-substring-no-properties
1440 (car region) (cdr region)))
1442 ;; Remove newlines that are ignored anyway
1443 (setq text (replace-regexp-in-string "\n" " " text))
1445 (with-current-buffer (djvu-doc-outline-buf)
1446 (goto-char (point-min))
1447 (if (equal (point) (point-max))
1448 (setq object (list 'bookmarks))
1450 (setq object (read (current-buffer)))
1451 (error (error "Syntax error in outline"))))
1452 (unless (eq 'bookmarks (car object))
1453 (error "No bookmarks"))
1454 ;; No decoding/encoding necessary if we add another bookmark.
1455 (setcdr object (sort (append (cdr object)
1456 (list (list text (format "#%d" page))))
1458 (< (string-to-number (substring (nth 1 x) 1))
1459 (string-to-number (substring (nth 1 y) 1))))))
1461 (insert "(bookmarks")
1462 (let (print-escape-newlines)
1463 (djvu-insert-outline (cdr object) " "))
1465 (goto-char (point-min))
1468 (defun djvu-decode-outline (object &optional encode)
1469 "Decode Djvu Outline OBJECT. Encode if ENCODE is non-nil."
1470 (dolist (elt object)
1474 (encode-coding-string (car elt) 'utf-8)
1475 (decode-coding-string (car elt) 'utf-8)))
1480 (encode-coding-string (cadr elt) 'utf-8)
1481 (decode-coding-string (cadr elt) 'utf-8))))
1482 ;; Continue with subtree.
1483 (djvu-decode-outline (nthcdr 2 elt) encode)))
1485 (defun djvu-insert-outline (object indent)
1486 "Insert Outline OBJECT."
1487 ;; This function is called recursively.
1488 (let ((indent1 (concat indent " ")))
1489 (dolist (elt object)
1490 (insert (format "\n%s(%S\n%s %S" indent (car elt) indent (nth 1 elt)))
1491 (djvu-insert-outline (nthcdr 2 elt) indent1)
1494 (defun djvu-process-outline (script &optional doc)
1496 (with-current-buffer (djvu-doc-outline-buf doc)
1498 (goto-char (point-min))
1499 (unless (= (point-min) (point-max))
1501 (setq object (read (current-buffer)))
1502 (error (error "Syntax error in outline"))))
1503 (skip-chars-forward " \t\n")
1504 ;; We should have swallowed all bookmarks.
1506 (error "Syntax error in outline (end of buffer)"))))
1507 (unless (eq 'bookmarks (car object))
1508 (error "No bookmarks"))
1509 (djvu-decode-outline (cdr object) t)
1511 (insert "set-outline\n")
1513 (insert "(bookmarks")
1514 (let ((print-escape-newlines t))
1515 (djvu-insert-outline (cdr object) " "))
1518 (write-region nil nil script t 0))))
1520 ;;; Image minor mode
1522 (define-minor-mode djvu-image-mode
1523 "Toggle image display of current page."
1525 :keymap '(([drag-mouse-1] . djvu-mouse-comment)
1526 ([C-drag-mouse-1] . djvu-mouse-comment-pushpin)
1527 ([S-drag-mouse-1] . djvu-mouse-highlight)
1528 ;; (Global) bindings of down-mouse events would take precedence over
1529 ;; drag-mouse events. So we bind the down-mouse events to `ignore'.
1530 ([down-mouse-1] . ignore)
1531 ([C-down-mouse-1] . ignore)
1532 ([S-down-mouse-1] . ignore)
1533 ("+" . djvu-image-zoom-in)
1534 ("-" . djvu-image-zoom-out))
1537 (defun djvu-image-zoom-in ()
1539 (djvu-image (round (* (nth 1 (djvu-doc-image)) 1.2))))
1541 (defun djvu-image-zoom-out ()
1543 (djvu-image (round (/ (nth 1 (djvu-doc-image)) 1.2))))
1545 (defun djvu-image (&optional isize)
1546 "If `djvu-image-mode' is enabled, display image of current Djvu page.
1547 Otherwise remove the image."
1548 (if (not djvu-image-mode)
1549 (let (buffer-read-only)
1550 (remove-text-properties (point-min) (point-max) '(display nil)))
1551 ;; Update image if necessary.
1552 (if (or (not (eq (djvu-doc-page) (car (djvu-doc-image))))
1554 (not (eq isize (nth 1 (djvu-doc-image))))))
1555 (let ((file (make-temp-file "djvu-"))
1557 (nth 1 (djvu-doc-image))
1560 ;; ddjvu does not send tiff files to stdout
1561 (let ((doc djvu-doc)
1562 (status (call-process "ddjvu" nil t nil
1563 (format "-size=%dx%d" isize isize)
1565 (format "-page=%d" (djvu-doc-page))
1568 (unless (zerop status)
1569 (error "Ddjvu error %s" status))
1571 (set-buffer-multibyte nil)
1572 (insert-file-contents-literally file)
1574 (list (djvu-doc-page doc)
1576 (create-image (buffer-substring-no-properties
1577 (point-min) (point-max))
1579 (djvu-delete-file file))))
1581 (let (buffer-read-only)
1582 (put-text-property (point-min) (point-max)
1583 'display (nth 2 (djvu-doc-image))))))
1585 (defun djvu-event-to-rect (event)
1586 "Convert mouse EVENT to Djvu rect coordinates."
1587 (let* ((start (posn-object-x-y (event-start event)))
1588 (end (posn-object-x-y (event-end event)))
1589 (x1 (car start)) (y1 (cdr start)) (x2 (car end)) (y2 (cdr end))
1590 (size (posn-object-width-height (event-start event)))
1591 (width (/ (float (car (djvu-doc-pagesize))) (car size)))
1592 (height (/ (float (cdr (djvu-doc-pagesize))) (cdr size))))
1593 (list (round (* (min x1 x2) width))
1594 (round (* (- (cdr size) (max y1 y2)) height))
1595 (round (* (max x1 x2) width))
1596 (round (* (- (cdr size) (min y1 y2)) height)))))
1598 (defun djvu-mouse-highlight (event)
1600 ;; Mouse events ignore prefix args?
1601 (let ((color (djvu-interactive-color djvu-color-highlight)))
1602 (djvu-highlight-region "" (read-string (format "(%s) H-Text: " color))
1603 (list (djvu-event-to-rect event))
1604 color djvu-opacity)))
1606 (defun djvu-mouse-comment (event &optional pushpin)
1608 ;; Mouse events ignore prefix args?
1609 (let ((color (djvu-interactive-color djvu-color-highlight)))
1610 (djvu-comment "" (read-string (format "(%s) C-Text: " color))
1611 (djvu-event-to-rect event) nil
1612 (djvu-color-background color djvu-color-background
1616 (defun djvu-mouse-comment-pushpin (event)
1618 (djvu-mouse-comment event t))
1622 (defun djvu-make-clean ()
1623 "Remove Outline and Annotations."
1625 (when (yes-or-no-p "Remove Outline and Annotations ")
1626 (djvu-djvused djvu-doc nil "-e"
1627 "select; remove-ant; set-outline;\n." "-s")
1632 ;;; djvu.el ends here