]> code.delx.au - gnu-emacs-elpa/blob - packages/djvu/djvu.el
15a76a59c365692e5d4f90d2c6e6bfa4304d2904
[gnu-emacs-elpa] / packages / djvu / djvu.el
1 ;;; djvu.el --- edit and view Djvu files via djvused
2
3 ;; Copyright (C) 2011 Free Software Foundation, Inc.
4
5 ;; Author: Roland Winkler <winkler@gnu.org>
6 ;; Keywords: files, wp
7 ;; Version: 0.5
8
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.
13
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.
18
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/>.
21
22 ;;; Commentary:
23
24 ;; djvu.el is a front end for the command-line program djvused
25 ;; from DjVuLibre, see http://djvu.sourceforge.net/
26 ;;
27 ;; This code requires you have the programs djvused, djview, and ddjvu
28 ;; installed.
29 ;;
30 ;; A normal work flow is as follows:
31 ;;
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
35 ;;
36 ;; (global-set-key "\C-cd" 'djvu-find-file)
37 ;;
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
40 ;; need.
41 ;;
42 ;; The menu bar of this buffer lists most of the commands with their
43 ;; repsective key bindings. For example, you can:
44 ;;
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.)
48 ;;
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.)
52 ;;
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.
58 ;;
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
64 ;;
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.)
70 ;;
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.
74 ;;
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
78 ;; editing "by hand".
79 ;;
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
86 ;; djvused syntax.)
87 ;;
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!
94 ;;
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.)
101
102 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
103 ;;
104 ;; Djvu internals:
105 ;; (see /usr/share/doc/libdjvulibre-dev/djvu3spec.djvu)
106 ;;
107 ;; Supported area attributes rect oval poly line text
108 ;; (none)/(xor)/(border c) X X X X X
109 ;; (shadow_* t) 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
114 ;;
115 ;; c = #RRGGBB t = thickness (1..32)
116 ;; o = opacity = 0..100
117
118 ;;; Code:
119
120 (defvar djvu-color-highlight "yellow"
121 "Default color for highlighting.")
122
123 (defvar djvu-color-himark "red"
124 "Default color for highmarking.")
125
126 (defvar djvu-color-url "blue"
127 "Default color for URLs.")
128
129 (defvar djvu-color-background "white"
130 "Default background.")
131
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")
137 ("blue" . "#6666FF")
138 ("yellow" . "#EEFF00")
139 ("white" . "#FFFFFF"))
140 "Alist of colors for highlighting.")
141
142 (defvar djvu-opacity 50
143 "Default opacity for Highlighting.")
144
145 (defvar djvu-coords-justify 0.02
146 "Upper threshold for justifying rect coordinates.")
147
148 (defvar djvu-fill-column 50
149 "Fill column for Djvu annotations.")
150
151 (defvar djvu-all-buffer "*djvu*"
152 "Buffer for `all' operations.")
153
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).")
158
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.
163 If nil do nothing.")
164
165 (defvar djvu-image-size 1024
166 "Size of internally displayed image.")
167
168 ;; Internal variables
169
170 (defvar djvu-test nil
171 "If non-nil do not process / delete djvused scripts.")
172 ;; (setq djvu-test t) (setq djvu-test nil)
173
174 (defvar djvu-doc 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)
182
183 ;;; Helper functions
184
185 ;; "read" refers to the text-only display of djvu files inside emacs
186 ;; "view" refers to external graphical viewers (default djview)
187
188 (eval-and-compile
189 (let ((count 0))
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))))
198
199 (defun djvu-switch-text ()
200 "Switch to Djvu Text buffer."
201 (interactive)
202 (let ((pos (djvu-read-pos)))
203 (switch-to-buffer (djvu-doc-text-buf))
204 (djvu-locate-pos 'word pos)))
205
206 (defun djvu-switch-annot ()
207 "Switch to Djvu Annotations buffer."
208 (interactive)
209 (let ((pos (djvu-read-pos)))
210 (switch-to-buffer (djvu-doc-annot-buf))
211 (djvu-locate-pos 'rect pos)))
212
213 (defun djvu-switch-outline ()
214 "Switch to Djvu Outline buffer."
215 (interactive)
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))))
228
229 (defun djvu-switch-read ()
230 "Switch to Djvu Read buffer."
231 (interactive)
232 (switch-to-buffer (djvu-doc-read-buf)))
233
234 (defun djvu-goto-page (page)
235 "Goto PAGE of Djvu document."
236 (interactive
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))
247
248 (defun djvu-next-page (n)
249 (interactive "p")
250 (djvu-goto-page (+ (djvu-doc-page) n)))
251
252 (defun djvu-prev-page (n)
253 (interactive "p")
254 (djvu-goto-page (- (djvu-doc-page) n)))
255
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))
260
261 (defun djvu-kill-view (&optional doc)
262 (when (djvu-doc-view-proc doc)
263 (unless (memq (process-status (djvu-doc-view-proc doc))
264 '(exit signal))
265 (kill-process (djvu-doc-view-proc doc)))
266 (djvu-doc-set-view-proc nil doc)))
267
268 (defun djvu-kill-doc (&optional doc)
269 (interactive)
270 (djvu-save doc t)
271 (djvu-kill-view 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))))
274
275 (defsubst djvu-delete-file (script)
276 (unless djvu-test (delete-file script)))
277
278 (defun djvu-save (&optional doc query)
279 "Save Djvu DOC."
280 (interactive)
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)))
285 script)
286 (when (and (or text-modified annot-modified outline-modified)
287 (or (not query)
288 (yes-or-no-p (format "Save %s? " (djvu-doc-basename doc)))))
289 (unwind-protect
290 (progn
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)))))
301
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))))))
309
310 (defun djvu-process (&optional doc view)
311 "Process Djvu DOC."
312 (interactive (list djvu-doc t))
313 (djvu-save doc)
314 (if view (djvu-view doc)))
315
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)))))
326
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))))
332
333 (defun djvu-interactive-region ()
334 "Return active region for use in interactive calls."
335 (let (beg end)
336 (if (use-region-p)
337 (setq beg (region-beginning)
338 end (region-end))
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)))))
346
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)))
356
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))))
368
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))
374
375 (defun djvu-color-background (color background opacity)
376 "For rgb COLOR and BACKGROUND apply OPACITY.
377 Return the new rgb color string."
378 (let* ((str "#")
379 ;; Why opacity squared??
380 (a (/ (float (* opacity opacity)) 10000))
381 (b (- 1 a)))
382 (dotimes (i 3 str)
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))))))))))
386
387 ;;; Djvu modes
388
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)
400
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)
405
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)
411
412 (define-key km "s" 'djvu-split-word)
413 (define-key km "w" 'djvu-merge-words)
414 (define-key km "l" 'djvu-merge-lines)
415
416 (define-key km "t" 'djvu-switch-text)
417 (define-key km "T" 'djvu-display-text-all)
418 km)
419 "Keymap for Djvu Read Mode.
420 This is a child of `special-mode-map'.")
421
422 (easy-menu-define
423 djvu-read-menu djvu-read-mode-map "Djvu Menu"
424 '("Djvu"
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]
430 "---"
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]
435 "---"
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]
442 "---"
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]
448 "---"
449 ["Add Bookmark" djvu-bookmark t]
450 ["Switch to Outline" djvu-switch-outline t]
451 "---"
452 ["Quit Djvu" quit-window t]
453 ["Kill Djvu buffers" djvu-kill-doc t]))
454
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))))))
460
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)
472 km)
473 "Keymap for Djvu Annot Mode.
474 This is a child of `text-mode-map'.")
475
476 (easy-menu-define
477 djvu-annot-menu djvu-edit-mode-map "Djvu Menu"
478 '("Djvu"
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]
483 "---"
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]
488 "---"
489 ["Switch to Annot" djvu-switch-annot t]
490 "---"
491 ["Quit Djvu" quit-window t]
492 ["Kill Djvu buffers" djvu-kill-doc t]))
493
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))))))
499
500 ;;; General Setup
501
502 ;;;###autoload
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."
506 (interactive
507 (list (read-file-name "Find Djvu file: " nil nil nil nil
508 (lambda (f)
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))
524 doc)
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
530 (setq doc djvu-doc))
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
544 (djvu-edit-mode)
545 (setq djvu-doc doc)
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)
549 (djvu-read-mode)
550 (setq djvu-doc 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)))
556
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)))
564 object alist)
565 (with-temp-buffer
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))
571 (when new
572 ;; page max
573 (djvu-doc-set-pagemax (read (current-buffer)) doc)
574 ;; page id
575 (let (page-id)
576 (while (progn
577 (skip-chars-forward " \t\n")
578 (looking-at "\\(?:\\([0-9]+\\)[ \t]+\\)?\\([PIAT]\\)[ \t]+\\([0-9]+\\)[ \t]+\\([^ \t\n]+\\)$"))
579 (if (match-string 1)
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))
586 ;; bookmarks
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) " "))
595 (insert ")\n")
596 (goto-char (point-min))
597 (set-buffer-modified-p nil)
598 (setq buffer-undo-list nil))))
599
600 ;; page size
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"))
606
607 ;; raw text
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)
614 (erase-buffer)
615 (djvu-insert-text object "")
616 (insert "\n")
617 (goto-char (point-min))
618 (set-buffer-modified-p nil)
619 (setq buffer-undo-list nil))
620
621 ;; Set up read buffer
622 (djvu-init-read doc object)
623
624 ;; Set up annotations buffer:
625 (save-excursion
626 (save-restriction
627 (narrow-to-region (point) (point-max))
628 (djvu-hide-hash)))
629 (setq object nil)
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.
637 (dolist (elt object)
638 (if (not (eq 'maparea (car elt)))
639 (push elt alist)
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)))
644 e)
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
649 ;; the rect box.
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)))
656 (push elt alist))
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)
662 (erase-buffer)
663 (dolist (elt alist)
664 (cond ((consp (car elt)) ;; maparea with list of rects
665 (let ((c (car elt)))
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
670 ")")))
671 ((not (eq 'maparea (car elt)))
672 (prin1 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
676 ")"))
677 (t (error "Djvu maparea %s undefined" (car (nth 3 elt)))))
678 (insert "\n\n")))
679 (djvu-hide-hash t)
680 (goto-char (point-max))
681 (set-buffer-modified-p nil)
682 (setq buffer-undo-list nil)))))
683
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))))
691 (if page-id
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))
697 url
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)
703 url)
704 ((string-match "\\`#" url)
705 (let ((page-id (rassoc (substring-no-properties url 1)
706 (djvu-doc-page-id doc))))
707 (if page-id
708 (concat "#" (car page-id))
709 (error "Page id broken: %s" url))))
710 (t url))) ; some other URL
711 (t url))) ; do nothing
712
713 (defun djvu-resolve-all-urls (dir)
714 "Resolve all internal URLs in a Djvu file."
715 (interactive
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))
727 nil nil nil 1)))
728 ((eq dir 'short)
729 (while (re-search-forward "^(maparea[ \t]+\"#\\([^\"]+\\)\"" nil t)
730 (replace-match (car (rassoc (match-string 1) page-id))
731 nil nil nil 1)))))
732 (djvu-process-all)
733 (kill-buffer djvu-all-buffer)
734 (with-current-buffer (djvu-doc-outline-buf)
735 (set-buffer-modified-p t))
736 (djvu-save)))
737
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)))
748 lst))
749
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")
755 (djvu-kill-view doc)
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)
770 (djvu-doc-file))))))
771
772 ;;; Djvu Text mode
773
774 (defun djvu-split-word (pos)
775 "Split word at position POS.
776 This command operates on the read buffer."
777 (interactive "d")
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)
782 (- pos beg))))
783 ;; Things get rather confusing without updating the read buffer.
784 ;; So we better save everything.
785 (djvu-save))
786
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."
790 (interactive
791 (let* ((pnt (point))
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))))))
795 (goto-char wpos)
796 (beginning-of-line)
797 (skip-chars-forward " \t")
798 (setq wpos (point))
799 (let ((indent (buffer-substring-no-properties
800 (line-beginning-position) wpos))
801 word)
802 (condition-case nil
803 (progn
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)) "")
818 (insert "\n" indent)
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)) ""))))
822
823 (defun djvu-merge-words (beg end)
824 "Merge words between positions BEG and END.
825 This command operates on the read buffer."
826 (interactive "r")
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.
834 (djvu-save))
835
836 (defun djvu-merge-words-internal (beg end)
837 "Merge words between positions BEG and END.
838 This command operates on the text buffer."
839 (interactive "r")
840 (let (words)
841 (goto-char end)
842 (if (bolp) (setq end (1- end)))
843 (goto-char beg)
844 (beginning-of-line)
845 (skip-chars-forward " \t")
846 (setq beg (point))
847 (condition-case nil
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 "")))
858 (undo-boundary))
859
860 (defun djvu-merge-lines (beg end)
861 "Merge lines between positions BEG and END.
862 This command operates on the read buffer."
863 (interactive "r")
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.
871 (djvu-save))
872
873 (defun djvu-merge-lines-internal (beg end)
874 "Merge lines between positions BEG and END.
875 This command operates on the text buffer."
876 (interactive "r")
877 ;; Calculate proper value of END
878 (goto-char end)
879 (beginning-of-line)
880 (unless (looking-at "[ \t]*(line ")
881 (re-search-backward "^[ \t]*(line ")
882 (forward-sexp)
883 (setq end (point)))
884 ;; Calculate proper value of BEG
885 (goto-char beg)
886 (beginning-of-line)
887 (unless (looking-at "[ \t]*(line ")
888 (re-search-backward "^[ \t]*(line "))
889 (skip-chars-forward " \t")
890 (setq beg (point))
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"))
898 (let (words)
899 ;; Collect all words
900 (condition-case nil
901 (while (<= (point) end)
902 (cond ((looking-at "[ \t]*(word ")
903 (push (read (current-buffer)) words))
904 ((not (looking-at "[ \t]*(line "))
905 (error "invalid")))
906 (forward-line))
907 (error (error "Syntax error in raw text")))
908 ;; Remove old words
909 (goto-char beg)
910 (while (let ((start (point)))
911 (forward-sexp)
912 (or (<= (point) end)
913 (progn (goto-char start) nil))))
914 (delete-region beg (point))
915 ;; Re-insert words
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)))
922 (undo-boundary))
923
924 (defun djvu-decode-text (object &optional encode)
925 (if (stringp (nth 5 object))
926 (setcar (nthcdr 5 object)
927 (if encode
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))))
932
933 (defun djvu-insert-text (object indent)
934 ;; This function is called recursively.
935 (insert indent "("
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)) ")")
945 (dolist (elt tail)
946 (insert "\n")
947 (djvu-insert-text elt indent))
948 (insert ")"))))
949
950 (defun djvu-process-text (script &optional doc)
951 (let ((doc (or doc djvu-doc))
952 object)
953 (with-current-buffer (djvu-doc-text-buf doc)
954 (save-excursion
955 (goto-char (point-min))
956 (condition-case nil
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.
961 (unless (eobp)
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)
970 (with-temp-buffer
971 (insert (format "select %d\nremove-txt\nset-txt\n" (djvu-doc-page doc)))
972 (djvu-insert-text object "")
973 (insert "\n.\n")
974 (write-region nil nil script t 0))))
975
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))
982 coord)
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)
996 (nthcdr 5 object)))
997 (error "No coords??")))))
998
999 (defun djvu-display-text-all ()
1000 "Display text for all pages."
1001 (interactive)
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)
1007 (emacs-lisp-mode)
1008 (erase-buffer)
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)))
1014
1015 (defun djvu-process-all ()
1016 "Process all pages. Use at your own risk. You get what you want."
1017 (interactive)
1018 (let ((buf (get-buffer djvu-all-buffer))
1019 script)
1020 (unless buf (error "No buffer `%s'" buf))
1021 (unless djvu-doc (error "No Djvu doc"))
1022 (unwind-protect
1023 (progn
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))))
1028
1029 ;;; Djvu Read mode
1030
1031 (defun djvu-init-read (doc object)
1032 (with-current-buffer (djvu-doc-read-buf doc)
1033 (let (buffer-read-only)
1034 (erase-buffer)
1035 (djvu-insert-read object))
1036 (set-buffer-modified-p nil)
1037 (setq buffer-read-only t)
1038 (goto-char (point-min))
1039 (djvu-image)))
1040
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)
1052 (t "\n\n")))
1053 elt)
1054 (while (setq elt (pop tail))
1055 (djvu-insert-read elt)
1056 (if (and sep tail (not (looking-back sep)))
1057 (insert sep)))))
1058 (put-text-property opoint (point) (car object)
1059 (vector (nth 1 object) (nth 2 object)
1060 (nth 3 object) (nth 4 object)))))
1061
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)
1074 (get-text-property
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))))))
1078
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)))
1084
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))
1089 (when pos
1090 (let ((re (concat "\\<" (symbol-name object) "\\> +"
1091 (mapconcat 'identity
1092 (make-list 4 "\\([[:digit:]]+\\)") " +")
1093 "\\( +\"\\)?"))
1094 done)
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))
1102 (<= (car pos) x2)
1103 (<= y1 (cdr pos))
1104 (<= (cdr pos) y2)))))
1105 (if done (point)
1106 (goto-char (point-min)) nil))))
1107
1108 (defsubst djvu-dist (width height)
1109 (+ (* width width) (* height height)))
1110
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)
1114 (if (not pos)
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))
1120 word dist)
1121 (while (progn
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)))))
1129
1130 ;;; Djvu Annotation mode
1131
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)))
1141 border
1142 (or backclr
1143 (djvu-color-background color djvu-color-background
1144 djvu-opacity))
1145 textclr pushpin)))
1146
1147 (defsubst djvu-insert-color (key color)
1148 (if 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))))
1153 ""))
1154
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)" "")
1168 ")\n\n"))
1169 (undo-boundary)))
1170
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))
1175
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."
1179 (interactive
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)))
1185
1186 (defun djvu-url (beg end url text &optional color opacity border)
1187 (interactive
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))
1193
1194 (defun djvu-highlight (beg end url text &optional color opacity border)
1195 "Highlight region between BEG and END, add annotation TEXT."
1196 (interactive
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)))
1202
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))
1212 coords)
1213 (unless (and (>= 1 paras) (>= 1 regions) (>= 1 columns))
1214 (error "Region spans multiple paragraphs"))
1215
1216 (if (eq 1 lines)
1217 (setq coords (list (djvu-scan-coords beg end 'word)))
1218
1219 (if (eq 2 lines)
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)))
1235 ;; 3 lines
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)))))
1259
1260 (djvu-highlight-region url text coords color opacity border)))
1261
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))
1275 (mapconcat
1276 (lambda (rect) (apply 'format "(rect %d %d %d %d)" (append rect nil)))
1277 coords "\n ") ")\n"
1278 (djvu-insert-color "hilite" color)
1279 (if opacity (format " (opacity %s)" opacity) "")
1280 (if border (format " (%s)" border) "")
1281 ")\n\n"))
1282 (undo-boundary)))
1283
1284 (defun djvu-fill (text)
1285 "Fill string TEXT using `djvu-fill-column'."
1286 (if djvu-fill-column
1287 (with-temp-buffer
1288 (insert text)
1289 (let ((fill-column djvu-fill-column))
1290 (fill-region (point-min) (point-max)))
1291 (buffer-substring-no-properties
1292 (point-min) (point-max)))
1293 text))
1294
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)))
1302 (if (eq p0 p1)
1303 (setq pnt (previous-single-property-change
1304 pnt prop nil (point-min))))))
1305 pnt))
1306
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)))
1314 (if (eq p0 p1)
1315 (setq pnt (next-single-property-change
1316 (1- pnt) prop nil (point-max))))))
1317 pnt))
1318
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))))))
1328 ci))
1329 (float (- xr xl))))))
1330
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))))
1337 (dolist (c ci)
1338 (aset c n tmp))))
1339
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)))
1344 (pnt beg) val)
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)))))
1352 coords))
1353
1354 (defun djvu-region-count (beg end prop)
1355 "Count regions between BEG and END with distinct non-nil values of PROP."
1356 (let ((count 0)
1357 (pnt beg))
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))))
1362 count))
1363
1364 (defun djvu-process-annot (script &optional doc)
1365 (let ((doc djvu-doc) object)
1366 (with-temp-buffer
1367 (insert-buffer-substring (djvu-doc-annot-buf doc))
1368 (djvu-hide-hash)
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\\)\\>")
1372 (condition-case nil
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))
1380 ;; URL
1381 (setcar (cdr elt) (encode-coding-string (djvu-resolve-url (nth 1 elt)) 'utf-8))
1382 ;; Comment
1383 (setcar (nthcdr 2 elt) (encode-coding-string (nth 2 elt) 'utf-8))))
1384
1385 (with-temp-buffer
1386 (let ((standard-output (current-buffer))
1387 (print-escape-newlines t)
1388 str)
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)))
1393 (prin1 elt)
1394 (insert "\n"))
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))))))
1406 (insert ".\n")
1407 (djvu-hide-hash t))
1408 (write-region nil nil script t 0))))
1409
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)
1418 (emacs-lisp-mode)
1419 (erase-buffer)
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))))
1432
1433 ;;; Djvu Outline mode
1434
1435 (defun djvu-bookmark (text page)
1436 "Create bookmark"
1437 (interactive
1438 (let ((region (djvu-interactive-region)))
1439 (list (read-string "Bookmark: " (buffer-substring-no-properties
1440 (car region) (cdr region)))
1441 (djvu-doc-page))))
1442 ;; Remove newlines that are ignored anyway
1443 (setq text (replace-regexp-in-string "\n" " " text))
1444 (let (object)
1445 (with-current-buffer (djvu-doc-outline-buf)
1446 (goto-char (point-min))
1447 (if (equal (point) (point-max))
1448 (setq object (list 'bookmarks))
1449 (condition-case nil
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))))
1457 (lambda (x y)
1458 (< (string-to-number (substring (nth 1 x) 1))
1459 (string-to-number (substring (nth 1 y) 1))))))
1460 (erase-buffer)
1461 (insert "(bookmarks")
1462 (let (print-escape-newlines)
1463 (djvu-insert-outline (cdr object) " "))
1464 (insert ")\n")
1465 (goto-char (point-min))
1466 (undo-boundary))))
1467
1468 (defun djvu-decode-outline (object &optional encode)
1469 "Decode Djvu Outline OBJECT. Encode if ENCODE is non-nil."
1470 (dolist (elt object)
1471 ;; Title
1472 (setcar elt
1473 (if encode
1474 (encode-coding-string (car elt) 'utf-8)
1475 (decode-coding-string (car elt) 'utf-8)))
1476 ;; URL
1477 (setcar (cdr elt)
1478 (djvu-resolve-url
1479 (if encode
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)))
1484
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)
1492 (insert ")"))))
1493
1494 (defun djvu-process-outline (script &optional doc)
1495 (let (object)
1496 (with-current-buffer (djvu-doc-outline-buf doc)
1497 (save-excursion
1498 (goto-char (point-min))
1499 (unless (= (point-min) (point-max))
1500 (condition-case nil
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.
1505 (unless (eobp)
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)
1510 (with-temp-buffer
1511 (insert "set-outline\n")
1512 (when object
1513 (insert "(bookmarks")
1514 (let ((print-escape-newlines t))
1515 (djvu-insert-outline (cdr object) " "))
1516 (insert ")\n"))
1517 (insert ".\n")
1518 (write-region nil nil script t 0))))
1519
1520 ;;; Image minor mode
1521
1522 (define-minor-mode djvu-image-mode
1523 "Toggle image display of current page."
1524 :lighter "Image"
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))
1535 (djvu-image))
1536
1537 (defun djvu-image-zoom-in ()
1538 (interactive)
1539 (djvu-image (round (* (nth 1 (djvu-doc-image)) 1.2))))
1540
1541 (defun djvu-image-zoom-out ()
1542 (interactive)
1543 (djvu-image (round (/ (nth 1 (djvu-doc-image)) 1.2))))
1544
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))))
1553 (and isize
1554 (not (eq isize (nth 1 (djvu-doc-image))))))
1555 (let ((file (make-temp-file "djvu-"))
1556 (isize (or isize
1557 (nth 1 (djvu-doc-image))
1558 djvu-image-size)))
1559 (unwind-protect
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)
1564 "-format=tiff"
1565 (format "-page=%d" (djvu-doc-page))
1566 (djvu-doc-file)
1567 file)))
1568 (unless (zerop status)
1569 (error "Ddjvu error %s" status))
1570 (with-temp-buffer
1571 (set-buffer-multibyte nil)
1572 (insert-file-contents-literally file)
1573 (djvu-doc-set-image
1574 (list (djvu-doc-page doc)
1575 isize
1576 (create-image (buffer-substring-no-properties
1577 (point-min) (point-max))
1578 'tiff t)) doc)))
1579 (djvu-delete-file file))))
1580 ;; Display image.
1581 (let (buffer-read-only)
1582 (put-text-property (point-min) (point-max)
1583 'display (nth 2 (djvu-doc-image))))))
1584
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)))))
1597
1598 (defun djvu-mouse-highlight (event)
1599 (interactive "e")
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)))
1605
1606 (defun djvu-mouse-comment (event &optional pushpin)
1607 (interactive "e")
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
1613 djvu-opacity)
1614 nil pushpin)))
1615
1616 (defun djvu-mouse-comment-pushpin (event)
1617 (interactive "e")
1618 (djvu-mouse-comment event t))
1619
1620 ;;; clean up
1621
1622 (defun djvu-make-clean ()
1623 "Remove Outline and Annotations."
1624 (interactive)
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")
1628 (djvu-init-page)))
1629
1630 \f
1631 (provide 'djvu)
1632 ;;; djvu.el ends here