-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
djvu.el
4559 lines (4223 loc) · 192 KB
/
djvu.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; djvu.el --- Edit and view Djvu files via djvused -*- lexical-binding: t -*-
;; Copyright (C) 2011-2022 Free Software Foundation, Inc.
;; Author: Roland Winkler <winkler@gnu.org>
;; Keywords: files, wp
;; Version: 1.1.2
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package is a front end for the command-line program djvused
;; from DjVuLibre, see https://djvu.sourceforge.net/. It assumes you
;; have the programs djvused, djview, ddjvu, and djvm installed.
;; The main purpose of djvu.el is to edit Djvu documents via djvused.
;; If you only seek an Emacs viewer for Djvu documents, you may be
;; better off with DocView shipped with GNU Emacs. Starting from
;; GNU Emacs 26, DocView supports Djvu documents.
;;
;; A Djvu document contains an image layer (typically scanned page images)
;; as well as multiple textual layers [text (for scanned documents from OCR),
;; annotations, shared annotations, and bookmarks]. The command-line
;; program djvused allows one to edit these textual layers via suitable
;; scripts. With Djvu mode you can edit and apply these djvused scripts
;; as if you were directly editing the textual layers of a Djvu document
;; (though Emacs never visits the Djvu document in the usual Emacs sense
;; of copying the content of a file into a buffer to manipulate it).
;; With Djvu mode you can also view the page images of a Djvu document,
;; yet Djvu mode does not attempt to reinvent the functionality of the
;; native viewer djview for Djvu documents. (I find djview very efficient
;; / fast for its purposes that also include features like searching the
;; text layer.) So Djvu mode supports that you use djview to view the
;; Djvu document while editing its textual layers. Djview and Djvu mode
;; complement each other.
;;
;; A normal work flow is as follows:
;;
;; Djvu files are assumed to have the file extension ".djvu".
;; When you visit the file foo.djvu, it puts you into the (read-only)
;; buffer foo.djvu. Normally, this buffer (plus possibly the outline buffer)
;; is all you need.
;;
;; The menu bar of this buffer lists most of the commands with their
;; respective key bindings. For example, you can:
;;
;; - Use `g' to go to the page you want. (Yes, Djvu mode operates on one
;; page at a time. Anything else would be too slow for large documents.)
;;
;; - Use `v' to (re)start djview using the position in the file foo.djvu
;; matching where point is in the buffer foo.djvu. (I find djview
;; fast enough for this, even for larger documents.)
;;
;; Yet note also that, starting from its version 4.9, djview reloads
;; djvu documents automatically when the djvu file changed on disk.
;; So you need not restart it anymore while editing a Djvu document
;; with Djvu mode. (Thank you, Leon Bottou!)
;;
;; Djvu mode likewise detects when the file changed on disk
;; (say, because the file was modified by some other application),
;; so that you can revert the buffers visiting this file.
;;
;; - To highlight a region in foo.djvu mark the corresponding region
;; in the buffer 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. View your changes with
;; djview.
;;
;; - Type `i' to enable `djvu-image-mode', a minor mode displaying the
;; current page as an image. Then
;; drag-mouse-1 defines a rect area
;; S-drag-mouse-1 defines an area where to put a text area,
;; C-drag-mouse-1 defines an area where to put a text area w/pushpin.
;;
;; - Use `o' to switch to the buffer foo.djvu-o displaying the outline
;; of the document (provided the document contains bookmarks that you
;; can add with Djvu mode). You can move through a multi-page document
;; by selecting a bookmark in the outline buffer.
;;
;; - The editing of the text, annotation, shared annotation and outline
;; (bookmarks) layers really happens in the buffers foo.djvu-t,
;; foo.djvu-a, foo-djvu-s, and foo.djvu-b. The djvused script syntax
;; used in these buffers is so close to Lisp that it was natural to give
;; these buffers a `djvu-script-mode' that is derived from `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 the main buffer 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 script syntax, sometimes it can also be helpful to do
;; such editing "by hand".
;;
;; But wait: the syntax in the annotations buffer foo.djvu-a is a
;; slightly modified djvused script syntax.
;;
;; - djvused can only highlight rectangles. So the highlighting of
;; larger areas of text must use multiple rectangles (i.e.,
;; multiple djvused "mapareas"). To make editing easier, these
;; are combined in the buffer foo.djvu-a. (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!
;;
;; - djvused uses two different ways of specifying coordinates for
;; rectangles
;; (1) hidden text uses quadrupels (xmin ymin xmax ymax)
;; (2) maparea annotations use (xmin ymin width height)
;; Djvu mode always uses quadrupels (xmin ymin xmax ymax)
;; Thus maparea coordinates are converted from and to djvused's format
;; when reading and writing djvu files.
;;
;; - Usually Djvu mode operates on the text and annotations layers
;; for one page of a Djvu document. If you really (I mean: REALLY)
;; want to edit a raw djvused script for the complete text or
;; annotations layer of a djvu document, use `djvu-text-script' or
;; `djvu-annot-script' to generate these raw scripts. When you have
;; finished editing, you can re-apply the script by calling
;; `djvu-process-script'. Use this at your own risk. This code does
;; not check whether the raw script is meaningful. You can loose the
;; text or annotations layer if the script is messed up.
;;; News:
;; v1.1.2:
;; - Support changing the mode of a buffer visiting a Djvu document.
;;
;; - Support `doc-view-toggle-display' with `major-mode-suspend'.
;;
;; - Selecting the background color "transparent" removes the
;; background color attribute.
;;
;; - New options `djvu-image-zoom' and `djvu-ascenders-re'.
;;
;; - Bug fixes.
;;
;; v1.1.1:
;; - Support text and image scrolling similar to `doc-view-mode'.
;; New option `djvu-continuous'.
;;
;; - New option `djvu-descenders-re'.
;;
;; - Bug fixes.
;;
;; v1.1:
;; - Use `auto-mode-alist' with file extension ".djvu".
;;
;; - Support bookmarks.
;;
;; - Display total number of pages in mode line.
;;
;; - New option `djvu-rect-area-nodups'.
;;
;; - User options `djvu-save-after-edit' and `djvu-region-history' removed
;; (obsolete).
;;
;; - More robust code for merging lines in text layer.
;;
;; - Clean up handling of editing positions in a djvu document.
;;
;; - Bug fixes.
;;
;; v1.0.1:
;; - Use `create-file-buffer' instead of `generate-new-buffer'
;; for compatibility with uniquify.
;;
;; v1.0:
;; - New commands `djvu-revert-buffer', `djvu-re-search-forward',
;; `djvu-re-search-forward-continue', `djvu-history-backward',
;; `djvu-history-forward', `djvu-dpi', `djvu-dpi-unify', `djvu-rotate',
;; `djvu-page-title', `djvu-ls', `djvu-inspect-file', `djvu-delete-page',
;; and `djvu-remove-annot'.
;;
;; - New commands for editing the text layer `djvu-edit-word',
;; `djvu-split-word', `djvu-merge-words', and `djvu-merge-lines'.
;;
;; - Make backups when editing Djvu documents.
;;
;; - Pretty-printed outline buffer for bookmarks.
;;
;; - Shared annotations buffer.
;;
;; - Font locking.
;;; To do:
;; - Auto-save script buffers. How can we recover these buffers
;; in a meaningful way?
;;
;; - Use `replace-buffer-contents'?
;;
;; - New command that makes line breaks in text layer better searchable:
;; Scan text layer for lines ending with hyphenated words "xxx-".
;; If the first word of the next line is "yyy" and ispell knows
;; the word "xxxyyy", replace "yyy" with that string. A search
;; for the word "xxxyyy" will then succeed.
;;; Code:
;;; Djvu internals (see Sec. 8.3.4.2.3.1 of djvu3spec.djvu)
;;
;; Supported area attributes rect text oval line poly
;; (none)/(xor)/(border c) X X X X X
;; (shadow_* t) X
;; (border_avis) X X X
;; (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..200 (yes)
;;
;; zones: page, column, region, para, line, word, and char
;; areas: rect, text, oval, line, and poly
(require 'image-mode)
(eval-when-compile
(require 'cl-lib))
(defgroup djvu nil
"Djvu mode."
:group 'wp
:prefix "djvu-")
(defcustom djvu-color-highlight "yellow"
"Default color for highlighting."
:group 'djvu
:type 'string)
(defcustom djvu-color-himark "red"
"Default color for highmarking."
:group 'djvu
:type 'string)
(defcustom djvu-color-url "blue"
"Default color for URLs."
:group 'djvu
:type 'string)
(defcustom djvu-color-background "white"
"Default background."
:group 'djvu
:type 'string)
(defcustom djvu-color-line "black"
"Default line color."
:group 'djvu
:type 'string)
(defcustom djvu-color-alist
;; If the keys are strings, they are directly compatible with what
;; we get back from something like `completing-read'.
'(("red" . "#FF0070") ; 0
("green" . "#00FF00") ; 1
("blue" . "#6666FF") ; 2
("yellow" . "#EEFF00") ; 3
("orange" . "#FF7F00") ; 4
("magenta" . "#FF00FF") ; 5
("purple" . "#7F60FF") ; 6
("cyan" . "#00FFFF") ; 7
("pink" . "#FF6060") ; 8
("white" . "#FFFFFF") ; 9
("black" . "#000000")); 10
"Alist of colors for highlighting."
:group 'djvu
:type '(repeat (cons (string) (string))))
(defcustom djvu-line-width 1
"Default line width."
:group 'djvu
:type 'integer)
(defcustom djvu-opacity 50
"Default opacity for Highlighting."
:group 'djvu
:type 'integer)
(defcustom djvu-areas-justify 0.02
"Upper threshold for justifying area coordinates."
:group 'djvu
:type 'number)
(defcustom djvu-fill-column 50
"Fill column for Djvu annotations."
:group 'djvu
:type 'integer)
(defcustom djvu-script-buffer "*djvu*"
"Default buffer for \"raw\" djvused scripts."
:group 'djvu
:type 'string)
(defcustom djvu-buffer-name-extensions
'("" "-t" "-a" "-s" "-b" "-o")
"Extensions for Djvu buffer names.
This is a list with six elements (READ TEXT ANNOT SHARED BOOKMARKS OUTLINE)."
:group 'djvu
:type '(list (string) (string) (string) (string) (string) (string)))
(defcustom djvu-image-size 1024
"Size of internally displayed image. This is MAX (width, height)."
:group 'djvu
:type 'integer)
(defcustom djvu-inherit-input-method t
"If non-nil calls of `read-string' inherit the input method."
:group 'djvu
:type 'boolean)
(defcustom djvu-djview-command "djview"
"Command for the Djvu Viewer."
:group 'djvu
:type 'string)
(defcustom djvu-djview-options nil
"List of command options for the Djvu Viewer."
:group 'djvu
:type '(repeat (string)))
(defcustom djvu-file-name-extension-re (regexp-opt '(".djvu" ".djbz" ".iff"))
"Regular expression for file name extensions in bundled multi-page documents.
These extensions include the period."
:group 'djvu
:type 'regexp)
(defcustom djvu-read-prop-newline 2
"Number of newline characters in Read buffer for consecutive region."
:group 'djvu
:type 'integer)
(defcustom djvu-outline-faces
;; Same as `outline-font-lock-faces'
[font-lock-function-name-face font-lock-variable-name-face
font-lock-keyword-face font-lock-comment-face
font-lock-type-face font-lock-constant-face
font-lock-builtin-face font-lock-string-face]
"Vector of faces for Outline buffer."
:group 'djvu
:type '(sexp))
(defcustom djvu-string-replace-list
'(("-\n+\\([[:lower:]]\\)" . "\\1") ; hyphenation
("-\n+" . "-") ; hyphenation
("[\n ]+" . " ")) ; white space
"Replacement list for text strings.
Each element is of the form (REGEXP . REP).
Used by `djvu-region-string'."
:group 'djvu
:type '(repeat (cons (regexp) (string))))
(defcustom djvu-rect-area-nodups nil
"If non-nil `djvu-rect-area' does not create multiple rects for same areas."
:group 'djvu
:type 'boolean)
(defcustom djvu-continuous nil
"When non-nil, scrolling to the page edge advances to next/previous page."
:group 'djvu
:type 'boolean)
(defcustom djvu-image-zoom 1.2
"Zoom factor for images."
:group 'djvu
:type 'number)
(defcustom djvu-descenders-re "[(),;Qgjpqy]" ; some fonts also `J' and `f'
;; https://en.wikipedia.org/wiki/Descender
"Regexp matching any descending characters or nil.
With Djvu mode, mapareas of annotations match tight the text they refer to.
This may appear visually awkward if the lower bound of the maparea lines up
with the baseline of the text because the text contains no descenders
from characters such as `g' or `q'. Then, if the text does not match
the regexp `djvu-descenders-re', the annotation area will descend
slightly below the baseline."
:group 'djvu
:type '(choice regexp (const nil)))
(defcustom djvu-ascenders-re "[^-,.;:acegm-su-z\s]"
"Regexp matching ascending characters or nil, see `djvu-descenders-re'."
:group 'djvu
:type '(choice regexp (const nil)))
;; Internal variables
(defvar djvu-coords-re
(format "\\(?2:%s\\)"
(mapconcat (lambda (i) (format "\\(?%d:-?[0-9]+\\)" i))
'(3 4 5 6) "[\s\t]+"))
"Regexp matching the coordinates of Djvu areas and zones.
Substring 2: coordinates, 3-6: individual coordinates.")
(defvar djvu-coord-xy-re
(mapconcat (lambda (i) (format "\\(?%d:-?[0-9]+\\)" i))
'(1 2) "[\s\t]+")
"Regexp matching pair of xy coordinates of Djvu maparea poly.
Substrings 1-2: individual coordinates.")
(defvar djvu-area-re
(format "(\\(?1:%s\\)[\s\t]+%s[)\s\t\n]"
(regexp-opt '("rect" "oval" "text" "line" "poly"))
djvu-coords-re)
"Regexp matching a Djvu area.
Substring 1: area type, 2: coordinates, 3-6: individual coordinates.")
(defvar djvu-zone-re
(format "[\s\t]*(\\(?1:%s\\)[\s\t]+%s[\s\t\n]+" ; omit closing `)'
(regexp-opt '("page" "column" "region" "para" "line"
"word" "char"))
djvu-coords-re)
"Regexp matching the beginning of a Djvu text zone.
Substring 1: zone type, 2: coordinates, 3-6: individual coordinates.")
(defvar djvu-test nil
"If non-nil do not process / delete djvused scripts. Useful for testing.")
;; (setq djvu-test t) (setq djvu-test nil)
(defvar-local djvu-buffer nil
"Type of Djvu buffer.")
(defvar djvu-rect-list nil
"Expanded rect list for propertizing the Read buffer.
This is a list with elements (COORDS URL TEXT COLOR ID) stored
in `djvu-doc-rect-list'.")
(defvar djvu-last-rect nil
"Last rect used for propertizing the Read buffer.
This is a list (BEG END COORDS URL TEXT COLOR).")
;; We use variable `djvu-resolve-url' as an internal flag while we update
;; all internal URLs in a Djvu document via `djvu-resolve-all-urls'.
;; Then we use `djvu-doc-resolve-url' to remember this scheme
;; and for adding new internal URLs consistent with this scheme.
(defvar djvu-resolve-url nil
"Flag for resolving internal URLs.
If `long' replace short page numbers by long FileIDs.
If `short' replace long FileIDs by short page numbers.
If nil do nothing.
Bind this with `let' to select one of these schemes.")
(defvar djvu-bookmark-level nil
"Counter for bookmark level.")
(defvar djvu-image-mode) ; fully defined by `define-minor-mode' (buffer-local)
(defvar djvu-init nil
"Non-nil during initialization of Djview mode.")
(defvar djvu-color-attributes '(border hilite lineclr backclr textclr)
"List of color attributes known to Djvu. See djvused(1).")
(defvar djvu-color-re
(concat "(" (regexp-opt (mapcar #'symbol-name djvu-color-attributes) t)
"[ \t\n]+\\(%s\\(%s[[:xdigit:]][[:xdigit:]]"
"[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]\\)%s\\)[ \t\n]*)")
"Format string to create a regular expression matching color attributes.")
(defvar djvu-beg-object-re
(concat "^[\s\t]*(" (regexp-opt '("background" "zoom" "mode" "align"
"maparea" "metadata" "xmp" "bookmarks")
t)
"\\>")
"Regexp matching the beginning of a Djvu object. See djvused(1).")
(defvar djvu-last-search-re nil
"Last regexp used by `djvu-re-search-forward'.")
(defvar djvu-modified nil
"Let-bound in `djvu-mouse-drag-track-area'.")
;; See `ediff-defvar-local'
(defmacro djvu-defvar-local (var &optional val doc)
"Define VAR as a permanent-local variable, and return VAR."
(declare (doc-string 3))
`(progn
(defvar ,var ,val ,doc)
(make-variable-buffer-local ',var)
(put ',var 'permanent-local t)
,var))
(djvu-defvar-local djvu-doc nil
"The \"ID\" of a Djvu document.
This is actually the Read buffer acting as the master buffer
of the Djvu document. This buffer holds all buffer-local values
of variables for a Djvu document.")
;; permanent-local like `buffer-file-name'
(djvu-defvar-local djvu-doc-file nil
"File name of a Djvu document.")
(djvu-defvar-local djvu-doc-text-buf nil
"Text buffer of a Djvu document.")
;; "read" refers to the text-only display of djvu files inside emacs
;; "view" refers to external graphical viewers (default djview)
(djvu-defvar-local djvu-doc-read-buf nil
"Read buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-annot-buf nil
"Annotation buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-shared-buf nil
"Shared annotation buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-bookmarks-buf nil
"Bookmarks buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-outline-buf nil
"Outline buffer of a Djvu document.")
(djvu-defvar-local djvu-doc-view-proc nil
"List of djview processes for a Djvu document.")
(defvar-local djvu-doc-resolve-url nil
"Resolve URLs of a Djvu document.")
(defvar-local djvu-doc-rect-list nil
"Rect list of a Djvu document.")
(defvar-local djvu-doc-history-backward nil
"Backward history of a Djvu document.
This is a stack of pages visited previously.")
(defvar-local djvu-doc-history-forward nil
"Forward history of a Djvu document.")
(defvar-local djvu-doc-page nil
"Current page number of a Djvu document.")
(defvar-local djvu-doc-pagemax nil
"Total number of pages of a Djvu document.")
(defvar-local djvu-doc-page-id nil
"Alist of page IDs of a Djvu document.
Each element is a cons pair (PAGE-NUM . FILE-ID).")
(defvar-local djvu-doc-pagesize nil
"Size of current page of a Djvu document.")
(defvar-local djvu-doc-read-pos nil
"The current editing position in the Read buffer (image coordinates).
This is either a list (X Y) or a list or vector (XMIN YMIN XMAX YMAX).
Used in `djvu-image-mode' when we cannot go to this position.")
(defvar-local djvu-doc-image nil
"Image of current page of a Djvu document.
This is a list (PAGE-NUM MAGNIFICATION IMAGE).")
(defvar-local djvu-doc-image-hscroll 0
"Number of columns by which a page image is scrolled from left margin.")
(defvar-local djvu-doc-image-vscroll 0
"Amount by which a page image is scrolled vertically.")
;;; Helper functions and macros
;; For each Djvu document we have six buffers associated with this document
;; (read, text, annotations, shared annotations, bookmarks and outline buffers).
;; To have document-local variables, `djvu-doc' defines a master buffer
;; that shares the buffer-local values of its variables with the other buffers
;; via the macros `djvu-set' and `djvu-ref'. We make the read buffer the
;; master buffer. This choice is rather arbitrary. The main reason for
;; this choice is that the read buffer is usually the main buffer to work
;; with. So it becomes easier to inspect the document-local variables.
(defmacro djvu-set (var val &optional doc)
"Set VAR's value to VAL in Djvu document DOC, and return VAL.
DOC defaults to `djvu-doc'."
;; `intern' VAR only once upon compilation
(let ((var (intern (format "djvu-doc-%s" var)))
(tmpval (make-symbol "tmpval")))
;; There is no equivalent of `buffer-local-value' for setting VAR.
;; Therefore, we need to make buffer DOC current before we can set VAR.
;; But we evaluate VAL in the current buffer before making DOC current.
`(let ((,tmpval ,val))
(with-current-buffer (or ,doc djvu-doc)
(set ',var ,tmpval)))))
(defmacro djvu-ref (var &optional doc)
"Return VAR's value in Djvu document DOC.
DOC defaults to `djvu-doc'."
;; `intern' VAR only once upon compilation
(let ((var (intern (format "djvu-doc-%s" var))))
`(buffer-local-value ',var (or ,doc djvu-doc))))
(defun djvu-header-line (identifier)
(list (propertize " " 'display '(space :align-to 0))
;; Emacs >= 26: compare `proced-header-line'
(format "%s -- %s (p%d)" (buffer-name (djvu-ref read-buf))
identifier (djvu-ref page))))
(defsubst djvu-substring-number (string &optional from to base)
"Parse substring of STRING as a decimal number and return the number.
If BASE, interpret STRING as a number in that base."
(string-to-number (substring-no-properties string from to) base))
(defsubst djvu-match-number (num &optional string base)
"Return string of text matched by last search, as a number.
If BASE, interpret match as a number in that base."
(string-to-number (match-string num string) base))
(defsubst djvu-buffers (&optional doc)
"Return a list of all buffers for DOC."
(list (djvu-ref read-buf doc) (djvu-ref text-buf doc)
(djvu-ref annot-buf doc) (djvu-ref shared-buf doc)
(djvu-ref bookmarks-buf doc) (djvu-ref outline-buf doc)))
(defmacro djvu-all-buffers (doc &rest body)
"Evaluate BODY in all buffers of Djvu DOC."
(declare (indent 1))
`(dolist (buf (djvu-buffers ,doc))
(with-current-buffer buf
,@body)))
(defmacro djvu-with-temp-file (file &rest body)
"Evaluate BODY with temp file FILE deleted at the end.
Preserve FILE if `djvu-test' is non-nil."
(declare (indent 1) (debug (symbolp body)))
`(let ((,file (make-temp-file "djvu-")))
(unwind-protect
(progn ,@body)
(unless djvu-test (delete-file ,file)))))
(defun djvu-switch-read (&optional doc dpos)
"Switch to Djvu Read buffer."
(interactive (list nil (djvu-dpos)))
(switch-to-buffer (djvu-ref read-buf doc))
(djvu-goto-read dpos))
(defun djvu-switch-text (&optional doc dpos)
"Switch to Djvu Text buffer."
(interactive (list nil (djvu-dpos)))
(switch-to-buffer (djvu-ref text-buf doc))
(djvu-goto-dpos 'word dpos))
(defun djvu-switch-annot (&optional doc dpos)
"Switch to Djvu Annotations buffer."
(interactive (list nil (djvu-dpos)))
(switch-to-buffer (djvu-ref annot-buf doc))
(if (djvu-goto-dpos "\\(?:rect\\|text\\)" dpos)
;; If we have matching buffer position in the annotations buffer,
;; put point at the end of the annotations string.
(re-search-backward "\"")))
(defun djvu-switch-shared (&optional doc)
"Switch to Djvu Shared Annotations buffer."
(interactive)
(switch-to-buffer (djvu-ref shared-buf doc)))
(defun djvu-switch-bookmarks (&optional doc page)
"Switch to Djvu Bookmarks buffer."
(interactive (list nil (if (eq djvu-buffer 'outline)
(djvu-outline-page)
(djvu-ref page))))
;; Try to go to the current page in the bookmarks buffer.
;; If this page is not defined, try to go to the nearest preceding page.
(switch-to-buffer (djvu-ref bookmarks-buf doc))
(when page
(goto-char (point-min))
(if (looking-at "(bookmarks")
(while (and (< 0 page)
(not (re-search-forward
(format "\"#\\(%d\\|%s\\)\"" page
(cdr (assq page (djvu-ref page-id doc))))
nil t)))
(setq page (1- page))))))
(defun djvu-switch-outline (&optional doc page)
"Switch to Djvu Outline buffer."
(interactive (list nil (if (eq djvu-buffer 'bookmarks)
(djvu-bookmarks-page)
(djvu-ref page))))
(switch-to-buffer (djvu-ref outline-buf doc))
(if page (djvu-goto-outline page)))
(defun djvu-dpos (&optional doc)
"Djvu position in current Djvu buffer."
(cond ((eq djvu-buffer 'read)
(djvu-read-dpos nil doc))
((eq djvu-buffer 'text)
(djvu-text-dpos nil doc))
((eq djvu-buffer 'annot)
(djvu-annot-dpos nil doc))))
(defun djvu-read-page ()
"Read page number interactively."
(let ((str (read-string (format "Page (f, 1-%d, l): " (djvu-ref pagemax)))))
(cond ((string-match "\\`f" str) 1)
((string-match "\\`l" str) (djvu-ref pagemax))
((string-match "\\`[[:digit:]]+\\'" str)
(string-to-number str))
(t (user-error "Page `%s' invalid" str)))))
(defun djvu-next-page (n)
"Go to the next page of this Djvu document."
(interactive "p")
(djvu-goto-page (+ (djvu-ref page) n)))
(defun djvu-prev-page (n)
"Go to the previous page of this Djvu document."
(interactive "p")
(djvu-goto-page (- (djvu-ref page) n)))
(defun djvu-scroll-up-command (&optional arg)
"Scroll text upward ARG lines; or near full screen if no ARG.
At the bottom of the page, when `djvu-continuous' is non-nil
go to the next page.
Prefix ARG may take the same values as arg ARG of `scroll-up-command'."
(interactive "^P") ; same as `scroll-up-command'
(if (and djvu-continuous
(= (window-end) (point-max))
(< (djvu-ref page) (djvu-ref pagemax)))
(djvu-next-page 1)
(condition-case nil ; Grrr, we should not need this, but
(scroll-up-command arg)
(end-of-buffer nil)))) ; `mwheel-scroll' does not like this.
(defun djvu-scroll-down-command (&optional arg)
"Scroll text downward ARG lines; or near full screen if no ARG.
At the top of the page, when `djvu-continuous' is non-nil
go to the previous page.
Prefix ARG may take the same values as arg ARG of `scroll-down-command'."
(interactive "^P") ; same as `scroll-down-command'
(if (and djvu-continuous
(= (point-min) (window-start))
(< 1 (djvu-ref page)))
(progn
(djvu-prev-page 1)
(goto-char (point-max))
(beginning-of-line)
(recenter -3))
(condition-case nil ; Grrr, we should not need this, but
(scroll-down-command arg)
(beginning-of-buffer nil)))) ; `mwheel-scroll' does not like this.
(defun djvu-next-line (&optional _arg _try-vscroll)
"Move cursor vertically down ARG lines.
ARG and TRY-VSCROLL have the same meaning as for `next-line'.
At the bottom of the page, when `djvu-continuous' is non-nil,
go to the next page."
;; The interactive spec gives both args the numeric value
;; of `current-prefix-arg'.
(interactive "^p\np") ; same as `next-line'
(if (and djvu-continuous
(= (line-end-position) (point-max))
(< (djvu-ref page) (djvu-ref pagemax)))
(djvu-next-page 1)
(call-interactively 'next-line)))
(defun djvu-prev-line (&optional _arg _try-vscroll)
"Move cursor vertically up ARG lines.
ARG and TRY-VSCROLL have the same meaning as for `previous-line'.
At the top of the page, when `djvu-continuous' is non-nil,
go to the previous page."
;; The interactive spec gives both args the numeric value
;; of `current-prefix-arg'.
(interactive "^p\np") ; same as `previous-line'
(if (and djvu-continuous
(= (point-min) (line-beginning-position))
(< 1 (djvu-ref page)))
(progn
(djvu-prev-page 1)
(goto-char (point-max))
(beginning-of-line)
(recenter -3))
(call-interactively 'previous-line)))
(defun djvu-history-backward ()
"Go backward in the history of visited pages."
(interactive)
(let ((history-backward (djvu-ref history-backward))
(history-forward (cons (djvu-ref page)
(djvu-ref history-forward))))
(unless history-backward
(user-error "This is the first page you looked at"))
(djvu-goto-page (car history-backward))
(djvu-set history-backward (cdr history-backward))
(djvu-set history-forward history-forward)))
(defun djvu-history-forward ()
"Go forward in the history of visited pages."
(interactive)
(let ((history-forward (djvu-ref history-forward)))
(unless history-forward
(user-error "This is the last page you looked at"))
(djvu-goto-page (car history-forward))
(djvu-set history-forward (cdr history-forward))))
(defun djvu-kill-view (&optional doc all)
"Kill most recent Djview process for DOC.
If ALL is non-nil, kill all Djview processes."
(interactive (list nil current-prefix-arg))
(let ((proc-list (djvu-ref view-proc doc)) proc nproc-list)
;; Clean up process list
(while (setq proc (pop proc-list))
(unless (memq (process-status proc) '(exit signal))
(push proc nproc-list)))
(setq proc-list (nreverse nproc-list))
(while (setq proc (pop proc-list))
(quit-process proc)
(djvu-set view-proc proc-list)
(unless all (setq proc-list nil)))))
(defun djvu-kill-doc (&optional doc)
"Kill all buffers visiting DOC.
This relies on `djvu-kill-doc-all' for doing the real work."
(interactive)
;; `djvu-kill-doc-all' will try to save our work and kill all djview
;; processes.
(mapc #'kill-buffer (djvu-buffers doc)))
(defvar djvu-in-kill-doc nil
"Non-nil if we are running `djvu-kill-doc-all'.")
(defun djvu-kill-doc-all ()
"Kill all buffers visiting `djvu-doc' except for the current buffer.
This function is added to `kill-buffer-hook' of all buffers visiting `djvu-doc'
so that killing the current buffer kills all buffers visiting `djvu-doc'."
(unless djvu-in-kill-doc
(let ((djvu-in-kill-doc t)
buffers)
;; Sometimes we choke on broken djvu files so that many things
;; do not work anymore the way they should. At least, we want to
;; be able to kill the relevant buffers. So do not bail out here.
(condition-case nil
(let ((doc djvu-doc))
(setq buffers (djvu-buffers doc))
(unless (memq nil (mapcar #'buffer-live-p buffers))
(djvu-save doc t))
(djvu-kill-view doc t))
(error nil))
;; A function in `kill-buffer-hook' should not kill the buffer
;; for which we called this hook in the first place, so that
;; other functions in this hook can do their job, too.
(mapc #'kill-buffer (delq (current-buffer) buffers)))))
(defun djvu-change-major-mode ()
"Clean up Djvu mode buffers and hooks.
Djvu mode puts this into `change-major-mode-hook'."
(unless djvu-init
(djvu-kill-doc-all)
;; These local variables are permanent local
(kill-local-variable 'kill-buffer-hook)
(kill-local-variable 'djvu-doc)
(kill-local-variable 'revert-buffer-function)
(kill-local-variable 'write-file-functions)
(let ((inhibit-read-only t)
(buffer-undo-list t))
(insert-file-contents-literally buffer-file-name t nil nil t))
(setq buffer-undo-list nil
buffer-read-only (not (file-writable-p buffer-file-name)))))
(defun djvu-save (&optional doc query)
"Save Djvu DOC."
(interactive)
(unless doc (setq doc djvu-doc))
(let ((afile (abbreviate-file-name (djvu-ref file doc)))
(text-modified (buffer-modified-p (djvu-ref text-buf doc)))
(annot-modified (buffer-modified-p (djvu-ref annot-buf doc)))
(shared-modified (buffer-modified-p (djvu-ref shared-buf doc)))
(bookmarks-modified (buffer-modified-p (djvu-ref bookmarks-buf doc))))
(when (and (or text-modified annot-modified shared-modified bookmarks-modified)
(or (and (verify-visited-file-modtime doc)
(or (not query)
(yes-or-no-p (format "Save %s? " afile))))
(yes-or-no-p (format "%s has changed since visited or saved. Save anyway? "
afile))))
(djvu-with-temp-file script
(if annot-modified (djvu-save-annot script doc))
(if shared-modified (djvu-save-annot script doc t))
(if text-modified (djvu-save-text doc script)) ; updates Read buffer
(if bookmarks-modified (djvu-save-bookmarks script doc))
(djvu-djvused doc nil "-f" script "-s"))
(if (and annot-modified (not text-modified))
(djvu-init-read (djvu-read-text doc) doc))
(djvu-all-buffers doc
(set-buffer-modified-p nil))))
t) ; for `write-file-function'
(defun djvu-modified ()
"Mark Djvu Read and Outline buffers as modified if necessary.
Used in `post-command-hook' of the Djvu Read, Text, Annotations,
Bookmarks and Outline buffers."
(let ((modified (or (buffer-modified-p (djvu-ref bookmarks-buf))
(buffer-modified-p (djvu-ref text-buf))
(buffer-modified-p (djvu-ref annot-buf))
(buffer-modified-p (djvu-ref shared-buf)))))
(with-current-buffer (djvu-ref read-buf)
(set-buffer-modified-p modified))
(with-current-buffer (djvu-ref outline-buf)
(set-buffer-modified-p modified))))
(defun djvu-quit-window (&optional kill doc)
"Quit all windows of Djvu document DOC and bury its buffers.
With prefix KILL non-nil, kill the buffers instead of burying them."
(interactive "P")
(unless doc (setq doc djvu-doc))
(dolist (buf (djvu-buffers doc))
(let ((window (get-buffer-window buf t)))
(cond (window
;; Quitting one Djvu window may bring up again the Djvu window
;; of another Djvu buffer for the same Djvu document.
;; So we first remove these Djvu buffers from the list
;; of buffers previously displayed in this window.
(let ((prev-buffers (window-prev-buffers window)))
(dolist (b (djvu-buffers doc))
(setq prev-buffers (assq-delete-all b prev-buffers)))
(set-window-prev-buffers window prev-buffers))
(quit-window kill window))
(kill
(kill-buffer buf))
(t
(bury-buffer buf))))))
(defun djvu-djvused (doc buffer &rest args)
"Process Djvu DOC by running the command djvused with ARGS.
BUFFER receives the process output, t means current buffer.
If BUFFER is nil, discard the process output, assuming that
the purpose of calling djvused is to update the Djvu file."
(unless doc (setq doc djvu-doc))
(unless (or buffer (file-writable-p (djvu-ref file doc)))
(user-error "File `%s' not writable"
(abbreviate-file-name (djvu-ref file doc))))
(when (or buffer (not djvu-test))
(unless buffer
(djvu-backup doc))
;; We could separately preserve the error stream. Yet this must go
;; into a file; it cannot go into a buffer. So we'd have to check
;; in the end whether the file was non-empty and then delete it.
(let* ((inhibit-quit t)
(coding-system-for-read 'utf-8)
(status (apply 'call-process "djvused" nil buffer nil
"-u" (djvu-ref file doc) args)))
(unless (zerop status)
(error "Djvused error %s (args: %s)" status args))
(unless buffer
(djvu-all-buffers doc
(set-visited-file-modtime))))))
(defun djvu-backup (doc)
"Make a backup of the disk file for Djvu document DOC, if appropriate."
(with-current-buffer doc
(unless buffer-backed-up
(let* ((file (djvu-ref file doc))
(real-file (file-chase-links file))
(val (backup-buffer)))
(when buffer-backed-up
;; Propagate the news
(djvu-all-buffers doc
(setq buffer-backed-up t))
;; Honor `backup-by-copying' and friends.
;; Yet if FILE does not exist anymore because `backup-buffer'
;; renamed it, we need to recreate FILE for djvused.
;; Strictly speaking, we recreate REAL-FILE because that is
;; the file that `backup-buffer' has renamed.
;; Then we also update the file-number for FILE. Yet we
;; need not worry here about the modification time because
;; we called `djvu-backup' from something like `djvu-djvused',
;; which anyway needs to update the recorded modification time.
(unless (file-exists-p real-file)
(backup-buffer-copy (nth 2 val) real-file
(nth 0 val) (nth 1 val))
(let ((file-number (nthcdr 10 (file-attributes file))))
(djvu-all-buffers doc
(setq buffer-file-number file-number)))))))))
;; The Emacs lisp reader gets confused by the Djvu color syntax with
;; symbols '#000000. So we temporarily convert these symbols to strings.
(defun djvu-convert-hash (&optional reverse)
"Convert color symbols #000000 to strings \"#000000\".
Perform inverse transformation if REVERSE is non-nil."
(if reverse
(let ((re (format djvu-color-re "\"" "#" "\"")))
(goto-char (point-min))
(while (re-search-forward re nil t)
(replace-match (match-string 3) nil nil nil 2)))
(let ((re (format djvu-color-re "#" "" "")))
(goto-char (point-min))
(while (re-search-forward re nil t)
(replace-match (format "\"%s\"" (match-string 2)) nil nil nil 2)))))
;; Without this macro, we'd have to deactivate the region immediately,
;; before we have decided what to do with it. That would be annoying.