;;; djvu.el --- Edit and view Djvu files via djvused ;; Copyright (C) 2011 Free Software Foundation, Inc. ;; Author: Roland Winkler ;; Keywords: files, wp ;; Version: 0.5 ;; This file is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; djvu.el is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with djvu.el. If not, see . ;;; Commentary: ;; This package is a front end for the command-line program djvused ;; from DjVuLibre, see http://djvu.sourceforge.net/. It assumes you ;; have the programs djvused, djview, and ddjvu installed. ;; ;; A normal work flow is as follows: ;; ;; To visit a djvu file type M-x fjvu-find-file. This command is the ;; only entry point to this package. You may want to bind this ;; command to a key you like. I use ;; ;; (global-set-key "\C-cd" 'djvu-find-file) ;; ;; If you use this command to visit file foo.djvu, it puts you into ;; the (read-only) buffer foo@djvu. Normally, this buffer is all you ;; need. ;; ;; The menu bar of this buffer lists most of the commands with their ;; repsective key bindings. For example, you can: ;; ;; - Use `g' to go to the page you want. (Yes, this package operates on ;; one page at a time. I guess that anything else would be too slow ;; for large documents.) ;; ;; - Use `v' to (re)start djview using the position in foo.djvu ;; matching where point is in foo@djvu. (I find djview fast enough ;; for this, even for larger documents.) ;; ;; - To highlight a region in foo.djvu mark the corresponding region ;; in foo@djvu (as usual, `transient-mark-mode' comes handy for ;; this). Then type `h' and add a comment in the minibuffer if you ;; like. Type C-x C-s to save this editing. Then type `v' to ;; (re)start djview to show what you have done. ;; ;; - Type i to enable `djvu-image-mode', a minor mode displaying the ;; current page as an image. Then ;; drag-mouse-1 defines a region where to put a comment, ;; C-drag-mouse-1 defines a region where to put a pushpin comment, ;; S-drag-mouse-1 defines a region to highlight ;; ;; - The editing of the text, annotation and outline (bookmark) layers ;; really happens in the buffers foo@djvu-t.el, foo@djvu-a.el, and ;; foo@djvu-o.el. (The djvused syntax used in these buffers is so ;; close to elisp that it was natural to give these buffers a ;; djvu-edit-mode that is derived from emacs-lisp-mode.) ;; ;; You can check what is happening by switching to these buffers. ;; The respective switching commands put point in these buffers such ;; that it matches where you were in foo@djvu. ;; ;; In these buffers, the menu bar lists a few low-level commands ;; available for editing these buffers directly. If you know the ;; djvused syntax, sometimes it can also be helpful to do such ;; editing "by hand". ;; ;; But wait: the syntax in the annotations buffer foo@djvu-a.el is a ;; slightly modified djvused syntax. djvused can only highlight ;; rectangles. So the highlighting of larger regions of text must use ;; multiple rectangles (i.e., multiple djvused "mapareas"). To make ;; editing easier, these are combined in the buffer foo@djvu-a.el. ;; (Before saving these things, they are converted using the proper ;; djvused syntax.) ;; ;; When you visit a djvu file, djvu-mode recognizes mapareas belonging ;; together by checking that "everything else in these mapareas except ;; for the rects" is the same. So if you entered a (unique) comment, ;; this allows djvu-mode to combine all the mapareas when you visit ;; such a file the second time. Without a comment, this fails! ;; ;; A second difference between what is displayed in the djvu buffers ;; and the input/output of djvused refers to nonascii characters. I ;; am using djvused from DjVuLibre-3.5.22 which handles utf-8 by ;; backslash sequences. So djvu mode converts these backslash ;; sequences into the corresponding utf-8 characters. (More recent ;; versions of djvused can do this conversion, too.) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Djvu internals: ;; (see /usr/share/doc/libdjvulibre-dev/djvu3spec.djvu) ;; ;; Supported area attributes rect oval poly line text ;; (none)/(xor)/(border c) X X X X X ;; (shadow_* t) X ;; (border_avis) X X X ;; (hilite color) / (opacity o) X ;; (arrow) / (width w) / (lineclr c) X ;; (backclr c) / (textclr c) / (pushpin) X ;; ;; c = #RRGGBB t = thickness (1..32) ;; o = opacity = 0..100 ;;; Code: (defvar djvu-color-highlight "yellow" "Default color for highlighting.") (defvar djvu-color-himark "red" "Default color for highmarking.") (defvar djvu-color-url "blue" "Default color for URLs.") (defvar djvu-color-background "white" "Default background.") (defvar djvu-color-alist ;; If the keys are strings, they are directly compatible with what ;; we get back from something like `completing-read'. '(("red" . "#FF0070") ("green" . "#00FF00") ("blue" . "#6666FF") ("yellow" . "#EEFF00") ("white" . "#FFFFFF")) "Alist of colors for highlighting.") (defvar djvu-opacity 50 "Default opacity for Highlighting.") (defvar djvu-coords-justify 0.02 "Upper threshold for justifying rect coordinates.") (defvar djvu-fill-column 50 "Fill column for Djvu annotations.") (defvar djvu-all-buffer "*djvu*" "Buffer for `all' operations.") (defvar djvu-buffer-name-extensions '("@djvu" "@djvu-t.el" "@djvu-a.el" "@djvu-o.el") "Extensions for Djvu buffer names. This is a list with four elements (READ TEXT ANNOT OUTLINE).") (defvar djvu-resolve-url nil "Flag for resolving internal URLs. If 'long replace short page numbers by long FileIDs. If 'short replace long FileIDs by short page numbers. If nil do nothing.") (defvar djvu-image-size 1024 "Size of internally displayed image.") ;; Internal variables (defvar djvu-test nil "If non-nil do not process / delete djvused scripts.") ;; (setq djvu-test t) (setq djvu-test nil) (defvar djvu-doc nil "Internal look-up table (a vector) for each Djvu document. For the different buffers of one Djvu document the buffer-local value of this variable is the same vector holding all the relevant information about this document. This way, we obtain a \"document-local\" variable, where changes are seen in all buffers refering to this Djvu document.") (make-variable-buffer-local 'djvu-doc) ;;; Helper functions ;; "read" refers to the text-only display of djvu files inside emacs ;; "view" refers to external graphical viewers (default djview) (eval-and-compile (let ((count 0)) (dolist (elt '(file basename text-buf read-buf annot-buf outline-buf page pagemax page-id pagesize pos view-proc image)) (eval (list 'defsubst (intern (concat "djvu-doc-" (symbol-name elt))) '(&optional doc) `(aref (or doc djvu-doc) ,count))) (eval (list 'defsubst (intern (concat "djvu-doc-set-" (symbol-name elt))) '(val &optional doc) `(aset (or doc djvu-doc) ,count val))) (setq count (1+ count))) (eval `(defconst djvu-doc-length ,count)))) (defun djvu-switch-text () "Switch to Djvu Text buffer." (interactive) (let ((pos (djvu-read-pos))) (switch-to-buffer (djvu-doc-text-buf)) (djvu-locate-pos 'word pos))) (defun djvu-switch-annot () "Switch to Djvu Annotations buffer." (interactive) (let ((pos (djvu-read-pos))) (switch-to-buffer (djvu-doc-annot-buf)) (djvu-locate-pos 'rect pos))) (defun djvu-switch-outline () "Switch to Djvu Outline buffer." (interactive) ;; Try to locate the current page in the outline buffer. ;; If this page is not defined, try to locate the nearest preceding page. (let ((page (djvu-doc-page)) pnt) (with-current-buffer (djvu-doc-outline-buf) (goto-char (point-min)) (if (looking-at "(bookmarks") (while (and (< 0 page) (not (setq pnt (re-search-forward (format "\"#%d\"" page) nil t)))) (setq page (1- page))))) (switch-to-buffer (djvu-doc-outline-buf)) (if pnt (goto-char pnt)))) (defun djvu-switch-read () "Switch to Djvu Read buffer." (interactive) (switch-to-buffer (djvu-doc-read-buf))) (defun djvu-goto-page (page) "Goto PAGE of Djvu document." (interactive (let ((str (read-string (format "Page (f, 1-%d, l): " (djvu-doc-pagemax))))) (list (cond ((string-match "\\`f" str) 1) ((string-match "\\`l" str) (djvu-doc-pagemax)) ((string-match "\\`[[:digit:]]+\\'" str) (string-to-number str)) (t (error "Page `%s' invalid" str)))))) (if (or (not (integerp page)) (<= page 0) (< (djvu-doc-pagemax) page)) (error "Page `%s' out of range" page)) (djvu-init-page djvu-doc page)) (defun djvu-next-page (n) (interactive "p") (djvu-goto-page (+ (djvu-doc-page) n))) (defun djvu-prev-page (n) (interactive "p") (djvu-goto-page (- (djvu-doc-page) n))) (defun djvu-set-color-highlight (color) "Set color for highlighting based on `djvu-color-alist'." (interactive (list (completing-read "Color: " djvu-color-alist nil t))) (setq djvu-color-highlight color)) (defun djvu-kill-view (&optional doc) (when (djvu-doc-view-proc doc) (unless (memq (process-status (djvu-doc-view-proc doc)) '(exit signal)) (kill-process (djvu-doc-view-proc doc))) (djvu-doc-set-view-proc nil doc))) (defun djvu-kill-doc (&optional doc) (interactive) (djvu-save doc t) (djvu-kill-view doc) (mapc 'kill-buffer (list (djvu-doc-text-buf doc) (djvu-doc-read-buf doc) (djvu-doc-annot-buf doc) (djvu-doc-outline-buf doc)))) (defsubst djvu-delete-file (script) (unless djvu-test (delete-file script))) (defun djvu-save (&optional doc query) "Save Djvu DOC." (interactive) (let ((pos (djvu-read-pos)) (text-modified (buffer-modified-p (djvu-doc-text-buf doc))) (annot-modified (buffer-modified-p (djvu-doc-annot-buf doc))) (outline-modified (buffer-modified-p (djvu-doc-outline-buf doc))) script) (when (and (or text-modified annot-modified outline-modified) (or (not query) (yes-or-no-p (format "Save %s? " (djvu-doc-basename doc))))) (unwind-protect (progn (setq script (make-temp-file "djvu-el-")) (if text-modified (djvu-process-text script doc)) (if annot-modified (djvu-process-annot script doc)) (if outline-modified (djvu-process-outline script doc)) (djvu-djvused doc nil "-f" script "-s") (dolist (buf (list (djvu-doc-text-buf doc) (djvu-doc-annot-buf doc) (djvu-doc-outline-buf doc) (djvu-doc-read-buf doc))) (with-current-buffer buf (set-buffer-modified-p nil))) (if text-modified (djvu-locate-read-pos pos))) (djvu-delete-file script))))) (defun djvu-modified () "Mark Djvu Read buffer as modified if necessary. Used in `post-command-hook' of the Djvu Outline, Text and Read buffers." (with-current-buffer (djvu-doc-read-buf) (set-buffer-modified-p (or (buffer-modified-p (djvu-doc-outline-buf)) (buffer-modified-p (djvu-doc-text-buf)) (buffer-modified-p (djvu-doc-annot-buf)))))) (defun djvu-process (&optional doc view) "Process Djvu DOC." (interactive (list djvu-doc t)) (djvu-save doc) (if view (djvu-view doc))) (defun djvu-djvused (doc buffer &rest args) ;; BUFFER is nil if we update the Djvu file. (unless (or buffer (file-writable-p (djvu-doc-file doc))) (error "File `%s' not writable" (abbreviate-file-name (djvu-doc-file doc)))) (unless (and (not buffer) djvu-test) (let ((status (apply 'call-process "djvused" nil buffer nil (djvu-doc-file doc) args))) (unless (zerop status) (error "Djvused error %s (args: %s)" status args))))) (defun djvu-hide-hash (&optional recover) (let* ((old (concat " " (if recover "@!@" "#") "\\([[:xdigit:]]\\)")) (new (concat " " (if recover "#" "@!@") "\\1"))) (goto-char (point-min)) (while (re-search-forward old nil t) (replace-match new)))) (defun djvu-interactive-region () "Return active region for use in interactive calls." (let (beg end) (if (use-region-p) (setq beg (region-beginning) end (region-end)) (setq beg (point) end (1+ (point)))) (cons (if (get-text-property beg 'word) (djvu-property-beg beg 'word) (next-single-property-change beg 'word nil end)) (if (get-text-property (1- end) 'word) (djvu-property-end end 'word) (previous-single-property-change end 'word nil beg))))) (defun djvu-interactive-color (color) "Return color specification for use in interactive calls." (let ((colnum (or (and (consp current-prefix-arg) (1- (/ (car current-prefix-arg) 4))) (and (integerp current-prefix-arg) current-prefix-arg)))) (if (and colnum (>= colnum (length djvu-color-alist))) (error "Color undefined")) (if colnum (car (nth colnum djvu-color-alist)) color))) (defun djvu-interactive-url (&optional color interrupt) "Return URL specification for use in interactive calls." (unless color (setq color djvu-color-url)) (let ((fmt (format "(%s) Page URL: " (or color djvu-color-url))) (page "") num ignore) (while (and (not ignore) (or (not (integerp (setq num (string-to-number page)))) (< num 1) (< (djvu-doc-pagemax) num))) (setq page (read-string fmt)) (if (and interrupt (string= "" page)) (setq ignore t))) (unless ignore (concat "#" page)))) (defsubst djvu-color-string-to-hex (color i) "Convert rgb COLOR string (part I) to hex number." (string-to-number (substring-no-properties (cdr (assoc color djvu-color-alist)) (1+ (* i 2)) (+ 3 (* i 2))) 16)) (defun djvu-color-background (color background opacity) "For rgb COLOR and BACKGROUND apply OPACITY. Return the new rgb color string." (let* ((str "#") ;; Why opacity squared?? (a (/ (float (* opacity opacity)) 10000)) (b (- 1 a))) (dotimes (i 3 str) (setq str (concat str (format "%X" (round (+ (* a (djvu-color-string-to-hex color i)) (* b (djvu-color-string-to-hex background i)))))))))) ;;; Djvu modes (defvar djvu-read-mode-map (let ((km (make-sparse-keymap))) (define-key km "i" 'djvu-image-mode) (define-key km "v" 'djvu-view) (define-key km "\C-c\C-v" 'djvu-view) (define-key km "n" 'djvu-next-page) (define-key km "p" 'djvu-prev-page) (define-key km "g" 'djvu-goto-page) (define-key km "k" 'djvu-kill-doc) (define-key km "\C-c\C-c" 'djvu-process) (define-key km "\C-x\C-s" 'djvu-save) (define-key km "h" 'djvu-highlight) (define-key km "u" 'djvu-url) (define-key km "a" 'djvu-switch-annot) (define-key km "A" 'djvu-display-annot-all) (define-key km "c" 'djvu-comment) (define-key km "C" 'djvu-comment-pushpin) (define-key km "b" 'djvu-bookmark) (define-key km "m" 'djvu-himark) (define-key km "o" 'djvu-switch-outline) (define-key km "s" 'djvu-split-word) (define-key km "w" 'djvu-merge-words) (define-key km "l" 'djvu-merge-lines) (define-key km "t" 'djvu-switch-text) (define-key km "T" 'djvu-display-text-all) km) "Keymap for Djvu Read Mode. This is a child of `special-mode-map'.") (easy-menu-define djvu-read-menu djvu-read-mode-map "Djvu Menu" '("Djvu" ["View File" djvu-view t] ["Image File" djvu-image-mode t] ["Go to Page" djvu-goto-page t] ["Process Doc" djvu-process t] ["Save Doc" djvu-save t] "---" ["Split Word" djvu-split-word t] ["Merge Words" djvu-merge-words t] ["Merge Lines" djvu-merge-lines t] ["Switch to Text" djvu-switch-text t] "---" ["Highlight Region" djvu-highlight t] ["URL over Region" djvu-url t] ["Himark Region" djvu-himark t] ["Add Comment" djvu-comment t] ["Add Comment w/pushpin" djvu-comment-pushpin t] ["Switch to Annotations" djvu-switch-annot t] "---" ["Show all Text" djvu-display-text-all t] ["Show all Annotations" djvu-display-annot-all t] ["Resolve all URLs" djvu-resolve-all-urls t] ["Process all Annotations" djvu-process-all t] ["Remove Annot / Outline" djvu-make-clean t] "---" ["Add Bookmark" djvu-bookmark t] ["Switch to Outline" djvu-switch-outline t] "---" ["Quit Djvu" quit-window t] ["Kill Djvu buffers" djvu-kill-doc t])) (define-derived-mode djvu-read-mode special-mode "Djview" "Mode for reading Djvu files." (setq mode-line-buffer-identification (list 24 (buffer-name) " " '(:eval (format "p%d" (djvu-doc-page)))))) (defvar djvu-edit-mode-map (let ((km (make-sparse-keymap))) (define-key km "\C-c\C-r" 'djvu-switch-read) (define-key km "\C-c\C-g" 'djvu-goto-page) (define-key km "\C-c\C-s" 'djvu-split-word-internal) (define-key km "\C-c\C-m" 'djvu-merge-words-internal) (define-key km "\C-c\M-m" 'djvu-merge-lines-internal) (define-key km "\C-c\C-c" 'djvu-process) (define-key km "\C-x\C-s" 'djvu-save) (define-key km "\C-c\C-v" 'djvu-view) (define-key km "\C-c\C-k" 'djvu-kill-doc) km) "Keymap for Djvu Annot Mode. This is a child of `text-mode-map'.") (easy-menu-define djvu-annot-menu djvu-edit-mode-map "Djvu Menu" '("Djvu" ["Go to Page" djvu-goto-page t] ["Switch to Read" djvu-switch-read t] ["Process Doc" djvu-process t] ["Save Doc" djvu-save t] "---" ["Switch to Text" djvu-switch-text t] ["Split Word" djvu-split-word-internal t] ["Merge Words" djvu-merge-words-internal t] ["Merge Lines" djvu-merge-lines-internal t] "---" ["Switch to Annot" djvu-switch-annot t] "---" ["Quit Djvu" quit-window t] ["Kill Djvu buffers" djvu-kill-doc t])) (define-derived-mode djvu-edit-mode emacs-lisp-mode "Djvu Edit" "Mode for editing (parts of) Djvu files." (setq mode-line-buffer-identification (list 24 (buffer-name) " " '(:eval (format "p%d" (djvu-doc-page)))))) ;;; General Setup ;;;###autoload (defun djvu-find-file (file &optional page view) "Read and edit Djvu FILE on PAGE. If VIEW is non-nil start external viewer." (interactive (list (read-file-name "Find Djvu file: " nil nil nil nil (lambda (f) (or (equal "djvu" (file-name-extension f)) (file-directory-p f)))) (prefix-numeric-value current-prefix-arg))) (unless page (setq page 1)) (setq file (expand-file-name file)) (unless (file-regular-p file) (error "Cannot open Djvu file `%s'." file)) ;; Initialize `djvu-doc' for FILE. (let* ((basename (file-name-sans-extension (file-name-nondirectory file))) (read-buf (concat basename (nth 0 djvu-buffer-name-extensions))) (text-buf (concat basename (nth 1 djvu-buffer-name-extensions))) (annot-buf (concat basename (nth 2 djvu-buffer-name-extensions))) (outline-buf (concat basename (nth 3 djvu-buffer-name-extensions))) (buffers (list text-buf read-buf annot-buf outline-buf)) doc) ;; Do nothing if we are already visiting FILE such that all required ;; buffers are properly defined. If some buffers were killed ;; do not attempt to recycle the remaining buffers. (if (eval (cons 'and (mapcar 'get-buffer buffers))) (with-current-buffer read-buf (setq doc djvu-doc)) (setq doc (make-vector djvu-doc-length nil)) (dolist (buf buffers) (if (get-buffer buf) (kill-buffer buf))) (djvu-doc-set-file file doc) (djvu-doc-set-basename basename doc) (djvu-doc-set-text-buf (get-buffer-create text-buf) doc) (djvu-doc-set-read-buf (get-buffer-create read-buf) doc) (djvu-doc-set-annot-buf (get-buffer-create annot-buf) doc) (djvu-doc-set-outline-buf (get-buffer-create outline-buf) doc) ;; Initialize all buffers. (dolist (buf (list (djvu-doc-text-buf doc) (djvu-doc-annot-buf doc) (djvu-doc-outline-buf doc))) (with-current-buffer buf (djvu-edit-mode) (setq djvu-doc doc) (cd (file-name-directory (djvu-doc-file))) (add-hook 'post-command-hook 'djvu-modified nil t))) (with-current-buffer (djvu-doc-read-buf doc) (djvu-read-mode) (setq djvu-doc doc) (cd (file-name-directory (djvu-doc-file))) (add-hook 'post-command-hook 'djvu-modified nil t)) (djvu-init-page doc page)) (if view (djvu-view doc)) (switch-to-buffer read-buf))) (defun djvu-init-page (&optional doc page) "For Djvu DOC initialize PAGE." (if (djvu-doc-pagemax doc) (djvu-save doc t)) (djvu-doc-set-pos nil doc) (if page (djvu-doc-set-page page doc)) (let* ((doc (or doc djvu-doc)) (new (not (djvu-doc-pagemax doc))) object alist) (with-temp-buffer (djvu-djvused doc t "-e" (format "%sselect %d; size; print-txt; print-ant;" (if new "n; ls; print-outline; " "") (djvu-doc-page doc))) (goto-char (point-min)) (when new ;; page max (djvu-doc-set-pagemax (read (current-buffer)) doc) ;; page id (let (page-id) (while (progn (skip-chars-forward " \t\n") (looking-at "\\(?:\\([0-9]+\\)[ \t]+\\)?\\([PIAT]\\)[ \t]+\\([0-9]+\\)[ \t]+\\([^ \t\n]+\\)$")) (if (match-string 1) ;; page-id is an alist with elements (PAGE-NUM . FILE-ID) (push (cons (match-string 1) (match-string 4)) page-id)) (goto-char (match-end 0))) (unless (eq (djvu-doc-pagemax doc) (length page-id)) (error "Page id list broken")) (djvu-doc-set-page-id (nreverse page-id) doc)) ;; bookmarks (skip-chars-forward " \t\n") (when (looking-at "(bookmarks") (setq object (read (current-buffer))) (djvu-decode-outline (cdr object)) (with-current-buffer (djvu-doc-outline-buf doc) (insert "(bookmarks") (let (print-escape-newlines) (djvu-insert-outline (cdr object) " ")) (insert ")\n") (goto-char (point-min)) (set-buffer-modified-p nil) (setq buffer-undo-list nil)))) ;; page size (skip-chars-forward " \t\n") (if (looking-at "width=\\([[:digit:]]+\\)[ \t]+height=\\([[:digit:]]+\\)$") (djvu-doc-set-pagesize (cons (string-to-number (match-string 1)) (string-to-number (match-string 2))) doc) (error "No pagesize")) ;; raw text (goto-char (match-end 0)) (skip-chars-forward " \t\n") (setq object (if (looking-at "(\\(page\\|column\\|region\\|para\\|line\\|word\\|char\\)") (read (current-buffer)))) (djvu-decode-text object) (with-current-buffer (djvu-doc-text-buf doc) (erase-buffer) (djvu-insert-text object "") (insert "\n") (goto-char (point-min)) (set-buffer-modified-p nil) (setq buffer-undo-list nil)) ;; Set up read buffer (djvu-init-read doc object) ;; Set up annotations buffer: (save-excursion (save-restriction (narrow-to-region (point) (point-max)) (djvu-hide-hash))) (setq object nil) (while (progn (skip-chars-forward " \t\n") (not (eobp))) (if (looking-at "(\\(background\\|zoom\\|mode\\|align\\|maparea\\|metadata\\)\\>") (push (read (current-buffer)) object) (error "Unknown annotation `%s'" (buffer-substring-no-properties (point) (line-end-position))))) ;; To simplify the editing of annotations, identify mapareas (rect) ;; sharing the same text string. (dolist (elt object) (if (not (eq 'maparea (car elt))) (push elt alist) (setcar (cdr elt) (decode-coding-string (nth 1 elt) 'utf-8)) (setcar (nthcdr 2 elt) (decode-coding-string (nth 2 elt) 'utf-8)) (cond ((eq 'rect (car (nth 3 elt))) ; rect (let ((rect (djvu-rect (nth 3 elt))) e) (setcdr (nthcdr 2 elt) (nthcdr 4 elt)) ; remove rect destructively ;; The new elements of alist are cons cells, where the car is the ;; maparea without rect, and the cdr is the list of rect areas. ;; Even if we have just an empty string, we still want to massage ;; the rect box. (if (or (string= "" (nth 2 elt)) (not (setq e (assoc elt alist)))) (push (cons elt (list rect)) alist) (setcdr e (cons rect (cdr e)))))) ((eq 'text (car (nth 3 elt))) ; text (setcar (nthcdr 3 elt) (djvu-rect (nth 3 elt))) (push elt alist)) (t (push elt alist))))) ;; Pretty print annotations. (with-current-buffer (djvu-doc-annot-buf doc) (let ((standard-output (current-buffer)) print-escape-newlines) (erase-buffer) (dolist (elt alist) (cond ((consp (car elt)) ;; maparea with list of rects (let ((c (car elt))) (insert (format "(maparea %S\n %S\n (" (djvu-resolve-url (nth 1 c) doc) (nth 2 c)) (mapconcat 'prin1-to-string (cdr elt) "\n ") ")\n " ; rect (mapconcat 'prin1-to-string (nthcdr 3 c) " ") ; rest ")"))) ((not (eq 'maparea (car elt))) (prin1 elt)) ((eq 'text (car (nth 3 elt))) ; text (insert (format "(maparea %S\n %S\n " (nth 1 elt) (nth 2 elt)) (mapconcat 'prin1-to-string (nthcdr 3 elt) " ") ; rest ")")) (t (error "Djvu maparea %s undefined" (car (nth 3 elt))))) (insert "\n\n"))) (djvu-hide-hash t) (goto-char (point-max)) (set-buffer-modified-p nil) (setq buffer-undo-list nil))))) (defun djvu-resolve-url (url &optional doc) "Resolve internal URLs. See variable `djvu-resolve-url'." (cond ((eq 'long djvu-resolve-url) ;; Replace page number by file id (cond ((string-match "\\`#[0-9]+\\'" url) (let ((page-id (assoc (substring-no-properties url 1) (djvu-doc-page-id doc)))) (if page-id (concat "#" (cdr page-id)) (error "Page id broken: %s" url)))) ((string-match "\\`#" url) (if (rassoc (substring-no-properties url 1) (djvu-doc-page-id doc)) url (error "Page id broken: %s" url))) (t url))) ; some other URL ((eq 'short djvu-resolve-url) ;; Replace file id by page number (cond ((string-match "\\`#[0-9]+\\'" url) url) ((string-match "\\`#" url) (let ((page-id (rassoc (substring-no-properties url 1) (djvu-doc-page-id doc)))) (if page-id (concat "#" (car page-id)) (error "Page id broken: %s" url)))) (t url))) ; some other URL (t url))) ; do nothing (defun djvu-resolve-all-urls (dir) "Resolve all internal URLs in a Djvu file." (interactive (list (intern (completing-read "Direction: " '((long) (short)) nil t)))) (if (djvu-modified) (error "Djvu file should be saved")) (let ((page-id (djvu-doc-page-id djvu-doc)) (djvu-all-buffer (generate-new-buffer " *djvu*")) (djvu-resolve-url dir)) (djvu-display-annot-all) (with-current-buffer djvu-all-buffer (goto-char (point-min)) (cond ((eq dir 'long) (while (re-search-forward "^(maparea[ \t]+\"#\\([0-9]+\\)\"" nil t) (replace-match (cdr (assoc (match-string 1) page-id)) nil nil nil 1))) ((eq dir 'short) (while (re-search-forward "^(maparea[ \t]+\"#\\([^\"]+\\)\"" nil t) (replace-match (car (rassoc (match-string 1) page-id)) nil nil nil 1))))) (djvu-process-all) (kill-buffer djvu-all-buffer) (with-current-buffer (djvu-doc-outline-buf) (set-buffer-modified-p t)) (djvu-save))) (defun djvu-rect (rect &optional back) "Convert (rect xmin ymin width height) to (rect xmin ymin xmax ymax). If BACK is non-nil do inverse transformation." (let* ((f (if back '- '+)) (lst (list (nth 0 rect) (nth 1 rect) (nth 2 rect) (funcall f (nth 3 rect) (nth 1 rect)) (funcall f (nth 4 rect) (nth 2 rect))))) ;; Only for back transforms we might get an error... (if (or (> 0 (nth 3 lst)) (> 0 (nth 4 lst))) (error "Annotation rect dimensions %s, %s" (nth 3 lst) (nth 4 lst))) lst)) (defun djvu-view (&optional doc) "Start Djview for DOC." (interactive (list djvu-doc)) (if (not (window-system)) (message "No window system available") (djvu-kill-view doc) (let* ((djvu-doc doc) (pos (or (djvu-doc-pos) (djvu-read-pos))) (px (/ (float (car pos)) (float (car (djvu-doc-pagesize))))) (py (- 1 (/ (float (cdr pos)) (float (cdr (djvu-doc-pagesize)))))) process-connection-type) ; Use a pipe. (if (or (< px 0) (< 1 px) (< py 0) (< 1 py)) (error "px=%s, py=%s out of range" px py)) (djvu-doc-set-pos nil) (djvu-doc-set-view-proc (start-process "djview" nil "djview" (format "-page=%d" (djvu-doc-page)) (format "-showposition=%06f,%06f" px py) (djvu-doc-file)))))) ;;; Djvu Text mode (defun djvu-split-word (pos) "Split word at position POS. This command operates on the read buffer." (interactive "d") (let ((beg (djvu-property-beg pos 'word)) (rpos (djvu-read-pos pos))) (with-current-buffer (djvu-doc-text-buf) (djvu-split-word-internal (djvu-locate-pos 'word rpos) (- pos beg)))) ;; Things get rather confusing without updating the read buffer. ;; So we better save everything. (djvu-save)) (defun djvu-split-word-internal (wpos split) "Split word at position WPOS at character position SPLIT. This command operates on the text buffer." (interactive (let* ((pnt (point)) (pps (parse-partial-sexp (line-beginning-position) pnt))) (unless (nth 3 pps) (error "Not inside string")) (list pnt (1- (- pnt (nth 8 pps)))))) (goto-char wpos) (beginning-of-line) (skip-chars-forward " \t") (setq wpos (point)) (let ((indent (buffer-substring-no-properties (line-beginning-position) wpos)) word) (condition-case nil (progn (setq word (read (current-buffer))) (unless (eq 'word (car word)) (error "invalid"))) (error (error "Syntax error in raw text"))) (if (or (< split 1) (<= (length (nth 5 word)) split)) (error "nothing to split")) (delete-region wpos (point)) ;; To split the bounding box horizontally, we take the fraction ;; of the number of characters in each fragment. This scheme ;; is only approximate, but it is better than nothing. (let ((frac (round (* (/ (float split) (length (nth 5 word))) (- (nth 3 word) (nth 1 word)))))) (djvu-insert-text (list 'word (nth 1 word) (nth 2 word) (+ (nth 1 word) frac) (nth 4 word) (substring (nth 5 word) 0 split)) "") (insert "\n" indent) (djvu-insert-text (list 'word (+ (nth 1 word) frac 1) (nth 2 word) (nth 3 word) (nth 4 word) (substring (nth 5 word) split)) "")))) (defun djvu-merge-words (beg end) "Merge words between positions BEG and END. This command operates on the read buffer." (interactive "r") (let ((bpos (djvu-read-pos beg)) (epos (djvu-read-pos (1- end)))) (with-current-buffer (djvu-doc-text-buf) (djvu-merge-words-internal (djvu-locate-pos 'word bpos) (djvu-locate-pos 'word epos)))) ;; Things get rather confusing without updating the read buffer. ;; So we better save everything. (djvu-save)) (defun djvu-merge-words-internal (beg end) "Merge words between positions BEG and END. This command operates on the text buffer." (interactive "r") (let (words) (goto-char end) (if (bolp) (setq end (1- end))) (goto-char beg) (beginning-of-line) (skip-chars-forward " \t") (setq beg (point)) (condition-case nil (while (< (point) end) (push (read (current-buffer)) words) (unless (eq 'word (caar words)) (error "invalid"))) (error (error "Syntax error in raw text"))) (delete-region beg (point)) (let ((object (apply 'list 'word 0 0 0 0 (nreverse words)))) (djvu-process-text-bbox object 0 (make-vector 3 nil)) (setcdr (nthcdr 4 object) (list (mapconcat (lambda (w) (nth 5 w)) (nthcdr 5 object) ""))) (djvu-insert-text object ""))) (undo-boundary)) (defun djvu-merge-lines (beg end) "Merge lines between positions BEG and END. This command operates on the read buffer." (interactive "r") (let ((bpos (djvu-read-pos beg)) (epos (djvu-read-pos (1- end)))) (with-current-buffer (djvu-doc-text-buf) (djvu-merge-lines-internal (djvu-locate-pos 'word bpos) (djvu-locate-pos 'word epos)))) ;; Things get rather confusing without updating the read buffer. ;; So we better save everything. (djvu-save)) (defun djvu-merge-lines-internal (beg end) "Merge lines between positions BEG and END. This command operates on the text buffer." (interactive "r") ;; Calculate proper value of END (goto-char end) (beginning-of-line) (unless (looking-at "[ \t]*(line ") (re-search-backward "^[ \t]*(line ") (forward-sexp) (setq end (point))) ;; Calculate proper value of BEG (goto-char beg) (beginning-of-line) (unless (looking-at "[ \t]*(line ") (re-search-backward "^[ \t]*(line ")) (skip-chars-forward " \t") (setq beg (point)) (unless (< beg end) (error "Nothing to merge")) ;; Parsing fails if the words belong to different paragraphs, ;; regions or columns. We would have to determine the lowest common ;; object level of these words. Then we could possibly merge ;; everything (!) within this level (if (re-search-forward "^[ \t]*\\(?:para\\|region\\|column\\)" end t) (error "Cannot merge paragraphs, regions or columns")) (let (words) ;; Collect all words (condition-case nil (while (<= (point) end) (cond ((looking-at "[ \t]*(word ") (push (read (current-buffer)) words)) ((not (looking-at "[ \t]*(line ")) (error "invalid"))) (forward-line)) (error (error "Syntax error in raw text"))) ;; Remove old words (goto-char beg) (while (let ((start (point))) (forward-sexp) (or (<= (point) end) (progn (goto-char start) nil)))) (delete-region beg (point)) ;; Re-insert words (let ((indent (buffer-substring-no-properties (line-beginning-position) (point))) (object (apply 'list 'line 0 0 0 0 (nreverse words)))) (djvu-process-text-bbox object 0 (make-vector 3 nil)) (delete-region (line-beginning-position) (point)) (djvu-insert-text object indent))) (undo-boundary)) (defun djvu-decode-text (object &optional encode) (if (stringp (nth 5 object)) (setcar (nthcdr 5 object) (if encode (encode-coding-string (nth 5 object) 'utf-8) (decode-coding-string (nth 5 object) 'utf-8))) (dolist (elt (nthcdr 5 object)) (djvu-decode-text elt encode)))) (defun djvu-insert-text (object indent) ;; This function is called recursively. (insert indent "(" (mapconcat 'prin1-to-string (list (nth 0 object) (nth 1 object) (nth 2 object) (nth 3 object) (nth 4 object)) " ")) (let ((tail (nthcdr 5 object)) (indent (concat indent " "))) (if (stringp (car tail)) ;; use `prin1-to-string' as we use this function both for ;; utf-8 and encoded stuff. (insert " " (prin1-to-string (car tail)) ")") (dolist (elt tail) (insert "\n") (djvu-insert-text elt indent)) (insert ")")))) (defun djvu-process-text (script &optional doc) (let ((doc (or doc djvu-doc)) object) (with-current-buffer (djvu-doc-text-buf doc) (save-excursion (goto-char (point-min)) (condition-case nil (setq object (read (current-buffer))) (error (error "Syntax error in raw text"))) (skip-chars-forward " \t\n") ;; We should have swallowed all raw text. (unless (eobp) (error "Syntax error in raw text (end of buffer)")))) (djvu-process-text-bbox object 0 (make-vector 7 nil)) ;; Update read buffer (djvu-init-read doc object) ;; FIXME: Should we also update the text buffer? ;; A transparent solution would update only the part of the buffer ;; that we actually changed so that `undo' works as expected. (djvu-decode-text object t) (with-temp-buffer (insert (format "select %d\nremove-txt\nset-txt\n" (djvu-doc-page doc))) (djvu-insert-text object "") (insert "\n.\n") (write-region nil nil script t 0)))) (defun djvu-process-text-bbox (object depth coords) "Evaluate bounding box for text OBJECT recursively." (if (stringp (nth 5 object)) (aset coords depth (vector (nth 1 object) (nth 2 object) (nth 3 object) (nth 4 object))) (let ((depth1 (1+ depth)) coord) (aset coords depth nil) (dolist (elt (nthcdr 5 object)) (djvu-process-text-bbox elt depth1 coords) (if (setq coord (aref coords depth)) (let ((coord1 (aref coords depth1))) (aset coord 0 (min (aref coord 0) (aref coord1 0))) (aset coord 1 (min (aref coord 1) (aref coord1 1))) (aset coord 2 (max (aref coord 2) (aref coord1 2))) (aset coord 3 (max (aref coord 3) (aref coord1 3)))) (aset coords depth (copy-sequence (aref coords depth1))))) (if (setq coord (aref coords depth)) (setcdr object (apply 'list (aref coord 0) (aref coord 1) (aref coord 2) (aref coord 3) (nthcdr 5 object))) (error "No coords??"))))) (defun djvu-display-text-all () "Display text for all pages." (interactive) (let ((doc djvu-doc) (buf (get-buffer-create djvu-all-buffer))) ;; Put this in a separate buffer! (with-current-buffer buf (let (buffer-read-only) (emacs-lisp-mode) (erase-buffer) (djvu-djvused doc t "-e" "output-txt") (goto-char (point-min))) (set-buffer-modified-p nil) (setq buffer-read-only t)) (switch-to-buffer buf))) (defun djvu-process-all () "Process all pages. Use at your own risk. You get what you want." (interactive) (let ((buf (get-buffer djvu-all-buffer)) script) (unless buf (error "No buffer `%s'" buf)) (unless djvu-doc (error "No Djvu doc")) (unwind-protect (progn (setq script (make-temp-file "djvu-el-")) (with-temp-file script (insert-buffer-substring buf)) (djvu-djvused djvu-doc nil "-f" script "-s")) (djvu-delete-file script)))) ;;; Djvu Read mode (defun djvu-init-read (doc object) (with-current-buffer (djvu-doc-read-buf doc) (let (buffer-read-only) (erase-buffer) (djvu-insert-read object)) (set-buffer-modified-p nil) (setq buffer-read-only t) (goto-char (point-min)) (djvu-image))) (defun djvu-insert-read (object) "Display text OBJECT." ;; This function is called recursively. (let ((opoint (point)) (tail (nthcdr 5 object))) (if (stringp (car tail)) (insert (decode-coding-string (car tail) 'utf-8)) (let* ((obj (caar tail)) (sep (cond ((eq 'line obj) "\n") ((eq 'word obj) "\s") ((eq 'char obj) nil) (t "\n\n"))) elt) (while (setq elt (pop tail)) (djvu-insert-read elt) (if (and sep tail (not (looking-back sep))) (insert sep))))) (put-text-property opoint (point) (car object) (vector (nth 1 object) (nth 2 object) (nth 3 object) (nth 4 object))))) (defun djvu-read-pos (&optional point) "Return Djvu position (x . y) of POINT in Djvu Read buffer." (with-current-buffer (djvu-doc-read-buf) ;; An empty djvu page gives us something like (page 0 0 0 0 "") (if (= (point-min) (point-max)) ;; Take the center of an empty page (cons (/ (car (djvu-doc-pagesize)) 2) (/ (cdr (djvu-doc-pagesize)) 2)) (unless point (setq point (point))) (djvu-mean-pos ; Return mean coordinates (or (get-text-property point 'word) (get-text-property (1- point) 'word) (get-text-property ;; Search backward because more often point is at the end ;; of region we operated on (1- (previous-single-property-change point 'word)) 'word)))))) (defun djvu-mean-pos (reg) "For region REG return mean coordinates (x . y)." ;; This works both for REG being vectors and lists. (cons (/ (+ (elt reg 0) (elt reg 2)) 2) (/ (+ (elt reg 1) (elt reg 3)) 2))) (defun djvu-locate-pos (object pos) "Locate OBJECT at position POS in the text or annotation buffer. If found, return corresponding position. Otherwise, return nil." (goto-char (point-min)) (when pos (let ((re (concat "\\<" (symbol-name object) "\\> +" (mapconcat 'identity (make-list 4 "\\([[:digit:]]+\\)") " +") "\\( +\"\\)?")) done) (while (and (not done) (re-search-forward re nil t)) (let ((x1 (string-to-number (match-string 1))) (x2 (string-to-number (match-string 3))) (y1 (string-to-number (match-string 2))) (y2 (string-to-number (match-string 4)))) (setq done (and (<= x1 (car pos)) (<= (car pos) x2) (<= y1 (cdr pos)) (<= (cdr pos) y2))))) (if done (point) (goto-char (point-min)) nil)))) (defsubst djvu-dist (width height) (+ (* width width) (* height height))) (defun djvu-locate-read-pos (pos) "Locate POS in Djvu Read buffer. Return corresponding position." (with-current-buffer (djvu-doc-read-buf) (if (not pos) (goto-char (point-min)) (let ((hpos (car pos)) (vpos (cdr pos)) (good-dist (djvu-dist (car (djvu-doc-pagesize)) (cdr (djvu-doc-pagesize)))) (pnt (point-min)) (good-pnt (point-min)) word dist) (while (progn (when (setq word (get-text-property pnt 'word)) (setq dist (djvu-dist (- (/ (+ (aref word 0) (aref word 2)) 2) hpos) (- (/ (+ (aref word 1) (aref word 3)) 2) vpos))) (if (< dist good-dist) (setq good-pnt pnt good-dist dist))) (setq pnt (next-single-property-change pnt 'word)))) (goto-char good-pnt))))) ;;; Djvu Annotation mode (defun djvu-comment-interactive (&optional border backclr textclr pushpin) "Interactive spec for `djvu-comment' and friends." (let ((pos (djvu-read-pos)) (pagesize (djvu-doc-pagesize)) (color (djvu-interactive-color djvu-color-highlight))) (list "" (read-string (format "(%s) Text: " color)) (list (car pos) (cdr pos) (+ (car pos) (/ (car pagesize) 2)) (+ (cdr pos) (/ (cdr pagesize) 30))) border (or backclr (djvu-color-background color djvu-color-background djvu-opacity)) textclr pushpin))) (defsubst djvu-insert-color (key color) (if color (format " (%s %s)" key (cond ((string-match "\\`#" color) color) ((cdr (assoc color djvu-color-alist))) (t (error "Color `%s' undefined" color)))) "")) (defun djvu-comment (url text rect &optional border backclr textclr pushpin) "Using URL and TEXT, highlight RECT. This defines a rect area for djvused." (interactive (djvu-comment-interactive)) (with-current-buffer (djvu-doc-annot-buf) (goto-char (point-max)) (let (print-escape-newlines) (insert (format "(maparea %S\n %S\n " url (djvu-fill text)) (apply 'format "(text %d %d %d %d)" rect) (if border (format " (%s)" border) "") (djvu-insert-color "backclr" backclr) (djvu-insert-color "textclr" textclr) (if pushpin " (pushpin)" "") ")\n\n")) (undo-boundary))) (defun djvu-comment-pushpin (url text rect &optional border backclr textclr pushpin) (interactive (djvu-comment-interactive nil nil nil t)) (djvu-comment url text rect border backclr textclr pushpin)) (defun djvu-himark (beg end url text &optional color opacity border) "Himark region between BEG and END. This highlights the region between BEG and END and creates a bookmark entry." (interactive (let ((region (djvu-interactive-region))) (list (car region) (cdr region) "" "" djvu-color-himark djvu-opacity 'none))) (djvu-highlight beg end url text color opacity border) (djvu-bookmark (buffer-substring-no-properties beg end) (djvu-doc-page))) (defun djvu-url (beg end url text &optional color opacity border) (interactive (let* ((region (djvu-interactive-region)) (color (djvu-interactive-color djvu-color-url)) (url (djvu-interactive-url color))) (list (car region) (cdr region) url "" color djvu-opacity 'xor))) (djvu-highlight beg end url text color opacity border)) (defun djvu-highlight (beg end url text &optional color opacity border) "Highlight region between BEG and END, add annotation TEXT." (interactive (let ((region (djvu-interactive-region)) (color (djvu-interactive-color djvu-color-highlight))) (list (car region) (cdr region) "" (read-string (format "(%s) Annotation: " color)) color djvu-opacity 'none))) (unless (get-text-property beg 'word) (error "Start position `%s' not a word" beg)) (unless (get-text-property (1- end) 'word) (error "End position `%s' not a word" end)) (let ((words (djvu-region-count beg end 'word)) (lines (djvu-region-count beg end 'line)) (paras (djvu-region-count beg end 'para)) (regions (djvu-region-count beg end 'region)) (columns (djvu-region-count beg end 'column)) coords) (unless (and (>= 1 paras) (>= 1 regions) (>= 1 columns)) (error "Region spans multiple paragraphs")) (if (eq 1 lines) (setq coords (list (djvu-scan-coords beg end 'word))) (if (eq 2 lines) (let ((c1 (djvu-scan-coords beg (djvu-property-end (1+ beg) 'line) 'word)) (c2 (djvu-scan-coords (djvu-property-beg (1- end) 'line) end 'word))) ;; If BEG is beginning of first line, both lines share same left margin. (if (and (= beg (djvu-property-beg beg 'line)) (djvu-coords-justify t c1 c2)) (djvu-justify-coords 'min 0 c1 c2)) ;; If END is end of second line, both lines share same right margin. (if (and (= end (djvu-property-end end 'line)) (djvu-coords-justify nil c2 c1)) (djvu-justify-coords 'max 2 c1 c2)) (if (<= (aref c1 0) (aref c2 2)) ;; Lower bound of upper box and upper bound of lower box coincide. (let ((tmp (/ (+ (aref c1 1) (aref c2 3)) 2))) (aset c1 1 tmp) (aset c2 3 tmp))) (setq coords (list c1 c2))) ;; 3 lines (let* ((l1e (djvu-property-end (1+ beg) 'line)) (l2b (djvu-property-beg (1- end) 'line)) (c1 (djvu-scan-coords beg l1e 'word)) (ci (djvu-scan-coords (1+ l1e) (1- l2b) 'line)) (c2 (djvu-scan-coords l2b end 'word))) ;; If BEG is beginning of first line, all lines share same left margin. (cond ((and (= beg (djvu-property-beg beg 'line)) (djvu-coords-justify t c1 ci c2)) (djvu-justify-coords 'min 0 c1 ci c2)) ((djvu-coords-justify t ci c2) (djvu-justify-coords 'min 0 ci c2))) ;; If END is end of last line, all lines share same right margin. (cond ((and (= end (djvu-property-end end 'line)) (djvu-coords-justify nil c2 ci c1)) (djvu-justify-coords 'max 2 c1 ci c2)) ((djvu-coords-justify nil c1 ci) (djvu-justify-coords 'max 2 c1 ci))) (let ((tmp1 (/ (+ (aref c1 1) (aref ci 3)) 2)) (tmp2 (/ (+ (aref ci 1) (aref c2 3)) 2))) ;; Lower bound of upper boxes and upper bound of lower boxes coincide. (aset c1 1 tmp1) (aset ci 3 tmp1) (aset ci 1 tmp2) (aset c2 3 tmp2)) (setq coords (list c1 ci c2))))) (djvu-highlight-region url text coords color opacity border))) (defun djvu-highlight-region (url text coords &optional color opacity border) "Using URL and TEXT, highlight COORDS. This defines a hilite area for djvused." ;; Record position where annotation was made. (let ((posl (mapcar 'djvu-mean-pos coords)) (n (length coords))) (djvu-doc-set-pos (cons (/ (apply '+ (mapcar 'car posl)) n) (/ (apply '+ (mapcar 'cdr posl)) n)))) ;; Insert in Annotations buffer. (with-current-buffer (djvu-doc-annot-buf) (goto-char (point-max)) (let (print-escape-newlines) (insert (format "(maparea %S\n %S\n (" url (djvu-fill text)) (mapconcat (lambda (rect) (apply 'format "(rect %d %d %d %d)" (append rect nil))) coords "\n ") ")\n" (djvu-insert-color "hilite" color) (if opacity (format " (opacity %s)" opacity) "") (if border (format " (%s)" border) "") ")\n\n")) (undo-boundary))) (defun djvu-fill (text) "Fill string TEXT using `djvu-fill-column'." (if djvu-fill-column (with-temp-buffer (insert text) (let ((fill-column djvu-fill-column)) (fill-region (point-min) (point-max))) (buffer-substring-no-properties (point-min) (point-max))) text)) (defun djvu-property-beg (pnt prop) ;; Assume that PNT has PROP. Otherwise we would not know whether ;; to search for it before or after PNT. (let ((p1 (get-text-property pnt prop))) (unless p1 (error "Position %s does not have property %s" pnt prop)) (if (> pnt (point-min)) (let ((p0 (get-text-property (1- pnt) prop))) (if (eq p0 p1) (setq pnt (previous-single-property-change pnt prop nil (point-min)))))) pnt)) (defun djvu-property-end (pnt prop) ;; Assume that (1- PNT) has PROP. Otherwise we would not know whether ;; to search for it before or after PNT. (let ((p1 (get-text-property (1- pnt) prop))) (unless p1 (error "Position %s does not have property %s" pnt prop)) (if (< pnt (point-max)) (let ((p0 (get-text-property pnt prop))) (if (eq p0 p1) (setq pnt (next-single-property-change (1- pnt) prop nil (point-max)))))) pnt)) (defun djvu-coords-justify (left &rest ci) "Return non-nil if rect coordinates CI shall be justified horizontally. If LEFT is nil analyze left boundaries of CI, otherwise the right boundaries." (let ((xl (apply 'min (mapcar (lambda (c) (aref c 0)) ci))) (xr (apply 'max (mapcar (lambda (c) (aref c 2)) ci)))) (> djvu-coords-justify (/ (apply 'max (mapcar (lambda (cj) (abs (float (if left (- (aref cj 0) xl) (- xr (aref cj 2)))))) ci)) (float (- xr xl)))))) (defun djvu-justify-coords (fun n &rest ci) "Pass Nth elements of arrays CI to function FUN. Set these elements to return value of FUN. If FUN is `min' or `max' these elements are set to the respective minimum or maximum among the Nth elements of all arrays CI." (let ((tmp (apply fun (mapcar (lambda (c) (aref c n)) ci)))) (dolist (c ci) (aset c n tmp)))) (defun djvu-scan-coords (beg end prop) "Between BEG and END calculate total bounding box for PROP." ;; Assume that BEG has PROP. (let ((coords (copy-sequence (get-text-property beg prop))) (pnt beg) val) (while (and (/= pnt end) (setq pnt (next-single-property-change pnt prop nil end))) (when (setq val (get-text-property pnt prop)) (aset coords 0 (min (aref coords 0) (aref val 0))) (aset coords 1 (min (aref coords 1) (aref val 1))) (aset coords 2 (max (aref coords 2) (aref val 2))) (aset coords 3 (max (aref coords 3) (aref val 3))))) coords)) (defun djvu-region-count (beg end prop) "Count regions between BEG and END with distinct non-nil values of PROP." (let ((count 0) (pnt beg)) (while (and (/= pnt end) (setq pnt (next-single-property-change pnt prop nil end))) (if (get-text-property (1- pnt) prop) (setq count (1+ count)))) count)) (defun djvu-process-annot (script &optional doc) (let ((doc djvu-doc) object) (with-temp-buffer (insert-buffer-substring (djvu-doc-annot-buf doc)) (djvu-hide-hash) (goto-char (point-min)) (while (progn (skip-chars-forward " \t\n") (not (eobp))) (if (looking-at "(\\(background\\|zoom\\|mode\\|align\\|maparea\\|metadata\\)\\>") (condition-case nil (push (read (current-buffer)) object) (error (error "Syntax error in annotations"))) (error "Unknown annotation `%s'" (buffer-substring-no-properties (point) (line-end-position)))))) (setq object (nreverse object)) (dolist (elt object) (when (eq 'maparea (car elt)) ;; URL (setcar (cdr elt) (encode-coding-string (djvu-resolve-url (nth 1 elt)) 'utf-8)) ;; Comment (setcar (nthcdr 2 elt) (encode-coding-string (nth 2 elt) 'utf-8)))) (with-temp-buffer (let ((standard-output (current-buffer)) (print-escape-newlines t) str) (insert (format "select %d\nremove-ant\nset-ant\n" (djvu-doc-page doc))) (dolist (elt object) (cond ((not (eq 'maparea (car elt))) (prin1 elt) (insert "\n")) ((consp (car (nth 3 elt))) ; rect (dolist (e (nth 3 elt)) (insert (prin1-to-string (apply 'list (car elt) (nth 1 elt) (nth 2 elt) (djvu-rect e t) (nthcdr 4 elt))) "\n"))) ((eq 'text (car (nth 3 elt))) ; text (insert (prin1-to-string (apply 'list (car elt) (nth 1 elt) (nth 2 elt) (djvu-rect (nth 3 elt) t) (nthcdr 4 elt))) "\n")) (t (error "Djvu maparea %s undefined" (car (nth 3 elt)))))) (insert ".\n") (djvu-hide-hash t)) (write-region nil nil script t 0)))) (defun djvu-display-annot-all (&optional display) "Print annotations for all pages." (interactive (list t)) (let ((doc djvu-doc) (buf (get-buffer-create djvu-all-buffer))) ;; Put this in a separate buffer! (with-current-buffer buf (let (buffer-read-only) (emacs-lisp-mode) (erase-buffer) (djvu-djvused doc t "-e" "output-ant") (goto-char (point-min)) (while (re-search-forward "^(maparea" nil t) (forward-sexp) ; jump over URL ;; replace newlines within text (let ((limit (save-excursion (forward-sexp) (point)))) (while (search-forward "\\n" limit t) (replace-match "\n")))) (goto-char (point-min))) (set-buffer-modified-p nil) (setq buffer-undo-list nil)) (if display (switch-to-buffer buf)))) ;;; Djvu Outline mode (defun djvu-bookmark (text page) "Create bookmark" (interactive (let ((region (djvu-interactive-region))) (list (read-string "Bookmark: " (buffer-substring-no-properties (car region) (cdr region))) (djvu-doc-page)))) ;; Remove newlines that are ignored anyway (setq text (replace-regexp-in-string "\n" " " text)) (let (object) (with-current-buffer (djvu-doc-outline-buf) (goto-char (point-min)) (if (equal (point) (point-max)) (setq object (list 'bookmarks)) (condition-case nil (setq object (read (current-buffer))) (error (error "Syntax error in outline")))) (unless (eq 'bookmarks (car object)) (error "No bookmarks")) ;; No decoding/encoding necessary if we add another bookmark. (setcdr object (sort (append (cdr object) (list (list text (format "#%d" page)))) (lambda (x y) (< (string-to-number (substring (nth 1 x) 1)) (string-to-number (substring (nth 1 y) 1)))))) (erase-buffer) (insert "(bookmarks") (let (print-escape-newlines) (djvu-insert-outline (cdr object) " ")) (insert ")\n") (goto-char (point-min)) (undo-boundary)))) (defun djvu-decode-outline (object &optional encode) "Decode Djvu Outline OBJECT. Encode if ENCODE is non-nil." (dolist (elt object) ;; Title (setcar elt (if encode (encode-coding-string (car elt) 'utf-8) (decode-coding-string (car elt) 'utf-8))) ;; URL (setcar (cdr elt) (djvu-resolve-url (if encode (encode-coding-string (cadr elt) 'utf-8) (decode-coding-string (cadr elt) 'utf-8)))) ;; Continue with subtree. (djvu-decode-outline (nthcdr 2 elt) encode))) (defun djvu-insert-outline (object indent) "Insert Outline OBJECT." ;; This function is called recursively. (let ((indent1 (concat indent " "))) (dolist (elt object) (insert (format "\n%s(%S\n%s %S" indent (car elt) indent (nth 1 elt))) (djvu-insert-outline (nthcdr 2 elt) indent1) (insert ")")))) (defun djvu-process-outline (script &optional doc) (let (object) (with-current-buffer (djvu-doc-outline-buf doc) (save-excursion (goto-char (point-min)) (unless (= (point-min) (point-max)) (condition-case nil (setq object (read (current-buffer))) (error (error "Syntax error in outline")))) (skip-chars-forward " \t\n") ;; We should have swallowed all bookmarks. (unless (eobp) (error "Syntax error in outline (end of buffer)")))) (unless (eq 'bookmarks (car object)) (error "No bookmarks")) (djvu-decode-outline (cdr object) t) (with-temp-buffer (insert "set-outline\n") (when object (insert "(bookmarks") (let ((print-escape-newlines t)) (djvu-insert-outline (cdr object) " ")) (insert ")\n")) (insert ".\n") (write-region nil nil script t 0)))) ;;; Image minor mode (define-minor-mode djvu-image-mode "Toggle image display of current page." :lighter "Image" :keymap '(([drag-mouse-1] . djvu-mouse-comment) ([C-drag-mouse-1] . djvu-mouse-comment-pushpin) ([S-drag-mouse-1] . djvu-mouse-highlight) ;; (Global) bindings of down-mouse events would take precedence over ;; drag-mouse events. So we bind the down-mouse events to `ignore'. ([down-mouse-1] . ignore) ([C-down-mouse-1] . ignore) ([S-down-mouse-1] . ignore) ("+" . djvu-image-zoom-in) ("-" . djvu-image-zoom-out)) (djvu-image)) (defun djvu-image-zoom-in () (interactive) (djvu-image (round (* (nth 1 (djvu-doc-image)) 1.2)))) (defun djvu-image-zoom-out () (interactive) (djvu-image (round (/ (nth 1 (djvu-doc-image)) 1.2)))) (defun djvu-image (&optional isize) "If `djvu-image-mode' is enabled, display image of current Djvu page. Otherwise remove the image." (if (not djvu-image-mode) (let (buffer-read-only) (remove-text-properties (point-min) (point-max) '(display nil))) ;; Update image if necessary. (if (or (not (eq (djvu-doc-page) (car (djvu-doc-image)))) (and isize (not (eq isize (nth 1 (djvu-doc-image)))))) (let ((file (make-temp-file "djvu-")) (isize (or isize (nth 1 (djvu-doc-image)) djvu-image-size))) (unwind-protect ;; ddjvu does not send tiff files to stdout (let ((doc djvu-doc) (status (call-process "ddjvu" nil t nil (format "-size=%dx%d" isize isize) "-format=tiff" (format "-page=%d" (djvu-doc-page)) (djvu-doc-file) file))) (unless (zerop status) (error "Ddjvu error %s" status)) (with-temp-buffer (set-buffer-multibyte nil) (insert-file-contents-literally file) (djvu-doc-set-image (list (djvu-doc-page doc) isize (create-image (buffer-substring-no-properties (point-min) (point-max)) 'tiff t)) doc))) (djvu-delete-file file)))) ;; Display image. (let (buffer-read-only) (put-text-property (point-min) (point-max) 'display (nth 2 (djvu-doc-image)))))) (defun djvu-event-to-rect (event) "Convert mouse EVENT to Djvu rect coordinates." (let* ((start (posn-object-x-y (event-start event))) (end (posn-object-x-y (event-end event))) (x1 (car start)) (y1 (cdr start)) (x2 (car end)) (y2 (cdr end)) (size (posn-object-width-height (event-start event))) (width (/ (float (car (djvu-doc-pagesize))) (car size))) (height (/ (float (cdr (djvu-doc-pagesize))) (cdr size)))) (list (round (* (min x1 x2) width)) (round (* (- (cdr size) (max y1 y2)) height)) (round (* (max x1 x2) width)) (round (* (- (cdr size) (min y1 y2)) height))))) (defun djvu-mouse-highlight (event) (interactive "e") ;; Mouse events ignore prefix args? (let ((color (djvu-interactive-color djvu-color-highlight))) (djvu-highlight-region "" (read-string (format "(%s) H-Text: " color)) (list (djvu-event-to-rect event)) color djvu-opacity))) (defun djvu-mouse-comment (event &optional pushpin) (interactive "e") ;; Mouse events ignore prefix args? (let ((color (djvu-interactive-color djvu-color-highlight))) (djvu-comment "" (read-string (format "(%s) C-Text: " color)) (djvu-event-to-rect event) nil (djvu-color-background color djvu-color-background djvu-opacity) nil pushpin))) (defun djvu-mouse-comment-pushpin (event) (interactive "e") (djvu-mouse-comment event t)) ;;; clean up (defun djvu-make-clean () "Remove Outline and Annotations." (interactive) (when (yes-or-no-p "Remove Outline and Annotations ") (djvu-djvused djvu-doc nil "-e" "select; remove-ant; set-outline;\n." "-s") (djvu-init-page))) (provide 'djvu) ;;; djvu.el ends here