-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy patheye.el
1743 lines (1515 loc) · 62.6 KB
/
eye.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
;;; eye.el --- Display images, comic books, and documents
;;
;; Released under the GPL. No implied warranties, etc. Use at your own risk.
;;
;;; History:
;; Copyright (C) Arthur Danskin <[email protected]>
;;
;; April 2008: Initial version
;; May 2008: better process interruption handling, `find-file' support
;; ansi-art
;;
;;; Commentary:
;;
;; Eye of Emacs is a major mode for displaying images, slideshows,
;; comic books, and documents. It should seem familar to users of
;; desktop image viewing programs, such as eye of gnome, gqview,
;; mirage, or evince. It also borrows some keybindings from `dired'.
;; It can display multiple images in one window. This is useful for
;; reading comic books and documents. Unfortunately, because of its
;; reliance on external processes, it is a little slower than these
;; programs. It can also display images as color ascii (ansi) art.
;;
;; In contrast to `doc-view-mode', eye supports (optional) resizing,
;; rendering with poppler (xpdf) instead of ghostview, and multiple
;; page display. Docview support postscript and dvi, cropping pages,
;; searching within documents, and has many fewer bugs.
;;
;; Eye avoids hanging emacs by passing continuations to asyncrounous
;; process sentinels. There are several backends, which list and fetch
;; files, and frontends, which display files. In the middle, images
;; are resized and rotated.
;;
;; BUGS:
;; * emacs seems to load images asyncronously, so their size is not
;; known until a short time after they are loaded. This kind of
;; breaks my packing algorithm.
;;
;; TODO:
;; * No 1. problem: it's too slow!
;; * continuous mode like in evince?
;; - could just load all the images and put them in the buffer
;; - this is too slow
;; * write a little program using libspectre to render postscript to png
;; * image based rather than window based zoom?
;; * text view thing
;; * some kind of thumbnail mode (maybe just zoomed out)
;; - support for switching between thumbnail and normal mode
;; * different sort modes
;; * get rid of (eq eye-frontend 'ansi)...
;; * use convert OR netpbm
;; * eye-source, eye-resize, eye-rotate, etc.. should be per image
;; - exim support can set rotation for one image
;;
;;; Features:
;; * resizes images to fit your window (asyncronously)
;; * go to the next, previous, nth, or a random image
;; * slideshow repeats your last manual movement
;; * image preloading
;; * view comic book archives and pdfs, extracting images on demand
;; * multiple image/page display
;; * manga right-to-left mode
;; * rotate images arbitrarily
;; * view images as ANSI art on a text terminal
;;
;;; Requirements:
;; Eye is only tested with emacs 23/24, but it might work with emacs 22
;; External programs are required for various features.
;; * ImageMagick for resize/rotate/pdf support
;; * pdftoppm/pdfinfo (comes with xpdf or poppler) for pdf support
;; * unrar for rar/cbr support
;; * unzip/zipinfo for zip/cbz support
;; * img2txt (comes with libcaca 1.x) for ansi art support
;; * curl for web support
;;
;;
;;; Installation:
;;
;; (require 'eye)
;; (add-to-list 'auto-mode-alist (cons eye-image-type-regexp 'eye-mode))
;; (mapc (lambda (x) (add-to-list 'auto-mode-alist (cons (car x) 'eye-mode)))
;;; eye-backend-alist)
;; (add-to-list 'magic-fallback-mode-alist (cons 'image-type-auto-detected-p 'eye-mode))
;;
;; C-x C-f your file.
;;
;; You can also use autoloads:
;;
;; (autoload 'eye "eye" "View images or comicbooks." t)
;;
;; M-x eye
;;
;;
;; You might want to use M-x eye to view very large documents or
;; comics, since it does not require emacs to read the whole archive.
;;
;;; Code:
(require 'image-mode)
(require 'dired)
(require 'cl-lib)
(require 'ffap)
(defsubst eye-emacs-has-imagemagick () nil)
(defsubst eye-can-resize ()
(and (or (executable-find "convert") (imagemagick-filter-types)) t))
;; ;; user variables
(defvar eye-resize (eye-can-resize)
"Should images be resized to fit the window?
If a number, zoom relative to window size.")
(defvar eye-rotate 0
"How many degrees counterclockwise should image be rotated?")
(defvar eye-manga nil "Should we read right to left?
This has no effect unless `eye-multi' is non-nil")
(defvar eye-multi t "Display multiple images at a time?
Images are displayed left to right, unless `eye-manga' is non-nil")
(defvar eye-invert nil)
;; internal variables
(defvar eye-files nil "Vector of files or number of files.")
(defvar eye-source nil "File or directory where we get our images from")
(defvar eye-index 0 "Current page number we are viewing.")
(defvar eye-process nil "The current subprocess.")
(defvar eye-process-queue nil "List of commands to run")
(defvar eye-last-ndisplay nil "Number of images displayed on this screen.")
(defvar eye-error nil "Did the last redisplay trigger an error?")
(defvar eye-text-files nil "List of text files in `eye-source'.")
(defvar eye-text-regexp "\\.\\(txt\\|nfo\\)\\'"
"Regexp for matching text files")
(defvar eye-temp-dir-base (concat temporary-file-directory "emacs-eye/")
"Directory for cached resized images and extracted images.
Keep in mind that this will be rm -rf ed when we clear the cache.")
(defvar eye-temp-dir nil)
(defvar eye-convert-program (executable-find "convert"))
;; (defvar eye-convert-args '(-filter "Quadratic"))
(defvar eye-convert-args nil)
(defconst eye-image-types
(apply 'nconc (mapcar (lambda (x) (cdr (assoc x '((xbm "xbm") (xpm "xpm") (gif "gif")
(pbm "pbm") (png "png") (svg "svg")
(jpeg "jpg" "jpeg" "JPG") (tiff "tiff")))))
image-types)) "List of supported image types")
(defconst eye-image-type-regexp (format "\\.%s\\'" (regexp-opt eye-image-types t))
"Regexp matching supported image types.")
(defvar eye-unzip (executable-find "unzip"))
(defvar eye-unrar (executable-find "unrar"))
(defvar eye-pdftoppm (executable-find "pdftoppm"))
(defvar eye-convert (executable-find "convert"))
(defvar eye-curl (executable-find "curl"))
(defvar eye-zipinfo (executable-find "zipinfo"))
(defvar eye-img2txt (executable-find "img2txt"))
(defvar eye-backend-alist
(append
'(("/\\'" :list eye-dir-list-files :extract eye-dir-with-image))
`((,eye-image-type-regexp :list eye-dir-initial-list-files))
(and eye-unzip
'(("\\.\\(cbz\\|zip\\)\\'" :list eye-zip-list-files
:extract eye-zip-with-image)))
(and eye-unrar
'(("\\.\\(cb\\|ra\\)r\\'" :list eye-rar-list-files
:extract eye-rar-with-image)))
(and eye-pdftoppm
eye-convert
'(("\\.pdf\\'" :list eye-pdf-pages :extract eye-pdf-with-image
:noresize t)))
(and eye-curl
'(("\\`https?://" :list eye-web-list-links :extract eye-web-with-image)
("\\.html" :list eye-web-file-list-links :extract eye-web-with-image)
)))
"Alist of (REGEXP . BACKEND) for different file types.")
(defvar eye-frontend nil "Element from `eye-frontend-alist'.")
(defvar eye-frontend-alist
'((image :load eye-image-load :display eye-image-display
:init eye-image-init)
(ansi :load eye-ansi-load :display eye-ansi-display :init eye-ansi-init))
"Alist of frontends.")
(defconst eye-subprocess-buffer " *eye subprocess*"
"Name of debugging buffer for process output")
;; slideshow
(defvar eye-slideshow-timer nil "Timer for slideshow.")
(defvar eye-slideshow-delay 3 "Seconds between slide advance.")
(defvar eye-slideshow-command 'eye-find-next "Command to auto advance")
(defvar eye-slideshow-arg 1 "Argument to `eye-slideshow-command'.")
;; preloading
(defvar eye-preload t "Should images be preloaded?
Special value 'all means preload continously.")
(defvar eye-preload-next nil "Image index queued for preload.")
(defun eye-temp-buffer-kill ()
(let ((buf (get-buffer (concat " * eye temp" eye-source))))
(when buf
(kill-buffer buf))))
(defun eye-temp-buffer ()
(get-buffer-create (concat " * eye temp" eye-source)))
;; general
(defun index (el seq)
"Return the index of EL in SEQ, or nil if EL is not in SEQ."
(let ((i 0) done)
(cond
((listp seq)
(while (and (null done) seq)
(if (equal el (car seq))
(setq done t)
(setq seq (cdr seq)
i (1+ i)))))
((vectorp seq)
(while (and (not done) (< i (length seq)))
(if (equal el (elt seq i))
(setq done t)
(setq i (1+ i)))))
(t (error "not a list or vector: %s" seq)))
(if done i nil)))
(defun plist-remove (plist prop)
"Destructively remove PROP from PLIST and returns PLIST."
(if (eq (car plist) prop)
(cddr plist)
(let ((p plist))
(while p
(if (not (eq (caddr p) prop))
(setq p (cddr p))
(setcdr (cdr p) (cddddr p))
(setq p nil))))
plist))
(defun eye-command-to-string (command &rest args)
"Execute command COMMAND with ARGS and return its output as a string."
(with-output-to-string
(with-current-buffer standard-output
(apply 'call-process command nil t nil args))))
(defvar eye-image-size-cache nil "Cache for `eye-image-size'.")
(defun eye-image-size (image)
"Return (WIDTH . HEIGHT) of file IMAGE in pixels."
(setq image (file-truename image))
(or (gethash image eye-image-size-cache)
(let ((str (eye-command-to-string "identify" image)))
(if (string-match "\\([0-9]+\\)x\\([0-9]+\\)" str)
(puthash image (cons (string-to-number (match-string 1 str))
(string-to-number (match-string 2 str)))
eye-image-size-cache)
(error "Size of %s not found in '%s'" image str)))))
(defsubst eye-ratio (size) (/ (float (car size)) (cdr size)))
(defsubst eye-nfiles ()
"Number of files currently being displayed."
(cond ((numberp eye-files) eye-files)
((vectorp eye-files) (length eye-files))
(t 1)))
(defsubst eye-file (&optional index)
"Return the file at INDEX or `eye-index'."
(if (eq (eye-nfiles) 0)
nil
(setq eye-index (mod eye-index (eye-nfiles)))
(if (sequencep eye-files)
(elt eye-files (if index (mod index (eye-nfiles)) eye-index))
eye-source)))
(defun eye-window-size (&optional pixels)
"Return window size as (WIDTH . HEIGHT).
In pixels unless `eye-ansi' is non-nil."
(let ((window (or (get-buffer-window (current-buffer) t)
(car (window-list nil 'no-mini)))))
(if pixels
(let* ((window-size (window-inside-pixel-edges window))
(width (- (caddr window-size) (car window-size)))
(height (- (cadddr window-size) (cadr window-size))))
(cons width height))
(cons (window-width window)
(window-height window)))))
;; resize and extract
(defun eye-sentinel (process state)
"Sentinel for `eye-start-filter-process'"
(let ((buf (process-get process 'buffer)))
;; Check if buffer was killed while we were running
(when (buffer-live-p buf)
(set-buffer buf)
(setq eye-process nil
eye-error (not (equal state "finished\n")))
(eye-mode-line-update)
(when eye-error
(progn
;;; (pop-to-buffer eye-subprocess-buffer)
(message "%s: %s" process state)))
(apply (process-get process 'callback)
(process-get process 'args))
(when (and eye-process-queue (not eye-process))
(let ((args (car eye-process-queue)))
(setq eye-process-queue (cdr eye-process-queue))
(apply 'eye-start-filter-process args))))))
(defun eye-start-filter-process (command filter-or-buffer callback &rest args)
"Run COMMAND list asyncronously, with output to FILTER-OR-BUFFER.
Set CALLBACK as the sentinel, called with ARGS."
;;; (when eye-process (error "One process at a time!"))
(when eye-error
(setq eye-error nil))
(if eye-process
(add-to-list 'eye-process-queue
(nconc (list command filter-or-buffer callback) args)
'append)
(unless eye-temp-dir
(setq eye-temp-dir (concat eye-temp-dir-base
(replace-regexp-in-string
"/" "=" eye-source) "/")))
(make-directory eye-temp-dir t)
(let ((buf (get-buffer-create eye-subprocess-buffer))
(default-directory eye-temp-dir)
(process-connection-type nil))
(setq eye-process
(apply 'start-process (format "eye process: %s" command)
(if (bufferp filter-or-buffer)
filter-or-buffer
eye-subprocess-buffer)
command))
(set-process-sentinel eye-process 'eye-sentinel)
(process-put eye-process 'buffer (current-buffer))
(process-put eye-process 'callback callback)
(process-put eye-process 'args args)
(when (functionp filter-or-buffer)
(set-process-filter eye-process filter-or-buffer))
(print command buf)))
(eye-mode-line-update))
(defun eye-start-process (command callback outfile)
"Start an asyncronous process with COMMAND list.
Process should create OUTFILE. If OUTFILE already exists, just
call callback directly. When it's done, call CALLBACK with OUTFILE"
(unless (file-name-absolute-p outfile)
(setq outfile (concat eye-temp-dir outfile)))
(if (file-exists-p outfile)
(funcall callback outfile)
(eye-start-filter-process command nil callback outfile)))
(defsubst eye-backend-get (fun)
"Return the element of the current backend named FUN"
(let ((backend (assoc-default
eye-source eye-backend-alist 'string-match-p)))
(eye-assert backend "Eye does not support this file type")
(plist-get backend fun)))
(defsubst eye-resize-p () (and eye-resize (not (eq eye-frontend 'ansi))
(not (eye-backend-get :noresize))))
(defsubst eye-rotate-p () (/= 0 eye-rotate))
(defun eye-convert-args ()
"Argument list for ImageMagick convert"
(let (args)
(dolist (x eye-convert-args)
(cond ((eq t x))
((symbolp x) (push (symbol-name x) args))
((stringp x) (push x args))))
(setq args (nreverse args))
(let ((no-bigger t) width height)
(when (eye-resize-p)
(let ((size (eye-window-size t)))
(setq width (car size)
height (cdr size)))
(when (numberp eye-resize)
(setq width (* width eye-resize)
height (* height eye-resize)
no-bigger nil))
(setq args (nconc args (list "-resize"
(format "%dx%d%s" width height
(if no-bigger ">" "")))))))
(when (eye-rotate-p)
(setq args (nconc args (list "-background" (face-background 'default)
"-rotate" (number-to-string eye-rotate)))))
args))
(defun eye-temp-file-name (base args)
"Temp name based on BASE and ARGS
Unique for these variables."
(let* ((basename (file-name-sans-extension (file-name-nondirectory base)))
(argstr (if args
(concat (replace-regexp-in-string "[/:]" "_" (mapconcat 'identity args "_")) ";")
""))
(ext (if (string-match-p "\.gif\\'" base) "gif" "png")))
(format "%s%s.%s" basename argstr ext)))
(defun eye-with-converted-image (image callback &optional pipe)
"Resize/rotate IMAGE appropriately and call CALLBACK.
If PIPE is non-nil, use it as a command that produces the image
on stdout. Then, IMAGE should be a temp file name with the right
extension."
(if (or (not (or (eye-resize-p) (eye-rotate-p)))
(eye-emacs-has-imagemagick))
(if pipe
(eye-start-process
(list "/bin/sh" "-c" (format "%s > %s" pipe image))
callback image)
(funcall callback image))
(let* ((args (eye-convert-args))
(temp-file (eye-temp-file-name image args)))
(if pipe
(eye-start-process
`("/bin/sh" "-c"
,(format "%s | %s - %s %s"
pipe eye-convert-program (mapconcat 'shell-quote-argument args " ")
(shell-quote-argument temp-file)))
callback temp-file)
(eye-start-process (append (list eye-convert-program) args (list image temp-file))
callback temp-file)))))
;; directory backend
(defun eye-dir-with-image (callback dir index)
(eye-with-converted-image (eye-file index) callback))
(defun eye-dir-list-files (path callback)
"Return a vector of files below PATH matching `eye-image-type-regexp'."
(let* ((files (process-lines
"find" (expand-file-name path) "-type" "f"))
(imgfiles (cl-delete-if-not (lambda (x) (string-match eye-image-type-regexp x)) files)))
(funcall callback (sort imgfiles 'string<))))
(defun eye-dir-initial-list-files (path callback)
"Special backend for when we have an initial image.
Basically `eye-dir-list-files', but set up `eye-index' and switch
to the directory backend."
(let ((img eye-source))
(setq eye-source (file-name-directory eye-source))
(eye-dir-list-files eye-source
`(lambda (files)
(setq eye-index (index ,img files))
(rename-buffer (eye-buffer-name eye-source) t)
(funcall ',callback files)))))
;; zip backend
(defun eye-zip-with-image (callback zipfile index)
(let* ((file (replace-regexp-in-string "\\([][*+?]\\)" "\\\\\\1" (eye-file index)))
(tmp (format "page%d.%s" index (file-name-extension file))))
(eye-with-converted-image
tmp callback
(format "%s -p %s '%s'"
eye-unzip (shell-quote-argument (expand-file-name zipfile)) file))))
(defun eye-zip-list-files (zipf callback)
(funcall callback
(sort (cl-delete-if-not
'eye-image-name-p
(process-lines eye-zipinfo "-1" (expand-file-name zipf)))
'string<)))
;; rar backend
(defun eye-rar-with-image (callback rarfile index)
;; FIXME We don't seem to have any way to determine the
;; path of a file in the archive.
(let* ((file (replace-regexp-in-string "\\([*?]\\)" "\\\\\\1" (eye-file index)))
(tmp (format "page%d.%s" index (file-name-extension file))))
(eye-with-converted-image
tmp callback
(format "%s p -ierr %s '%s'" eye-unrar (shell-quote-argument rarfile)
(concat "*" file)))))
(defun eye-rar-list-files (rarf callback)
;; FIXME unrar lists files without directories !!!
(funcall callback
(sort (cl-delete-if-not
'eye-image-name-p
(process-lines eye-unrar "lb" (expand-file-name rarf)))
'string<)))
;; html/web backend
(defun eye-web-temp-file-name (url)
"Temp name based on URL."
(replace-regexp-in-string "[/:]" "_" url))
(defun eye-web-fetch (url callback &rest args)
"Download URL and call (CALLBACK file).
If ARGS are given, call (apply CALLBACK buffer-with-url-source args)."
(if args
(progn
(apply 'eye-start-filter-process
(list eye-curl "--compressed" "--silent" url)
(eye-temp-buffer) callback (cons (eye-temp-buffer) args)))
(let ((outfile (eye-web-temp-file-name url)))
(eye-start-process (list eye-curl "--silent" url "--output" outfile) callback outfile))))
(defvar eye-ignore-link-regexp (regexp-opt (list "bits\." ; wikipedia logos, etc
"/wiki/File:" ; actually a text file...
"thumb"
"4chan-ads"))
"List of regular expressions matching links to ignore when scanning html for image links")
(defun eye-fix-mediawiki (link)
(if (string-match "\\(.*commons/\\)thumb/\\(.*\\)/[^/]+" link)
(concat (match-string 1 link) (match-string 2 link))
link))
(defvar eye-fix-link-functions (list 'eye-fix-mediawiki)
"list of string -> string functions to run on image links before download")
(defun eye-url-domain (url)
"Return just the domain name part of URL (i.e. everything up to the .com)"
(if (string-match "https?://[^/ ]+" url)
(match-string 0 url)
nil))
(defun eye-web-find-images-in-html (source-url)
(let ((linkre (concat "\\(href\\|src\\)=\"\\([^&\"' ]+"
(substring eye-image-type-regexp 0 -2)
"\\)"))
(domain (eye-url-domain source-url))
links link)
(goto-char (point-min))
(while (re-search-forward linkre nil t)
(let ((link (match-string 2)))
(setq link (cond ((string-match-p "^https?://" link) link)
((string-match-p "^//" link) (concat "http:" link))
((string-match-p "^/" link) (concat domain link))
(t (concat source-url "/" link))))
(dolist (fun eye-fix-link-functions)
(setq link (funcall fun link)))
(unless (string-match-p eye-ignore-link-regexp link)
(setq links (cons link links)))))
(delete-dups (nreverse links))))
(defun eye-web-list-links-1 (page callback)
(funcall callback
(let ((source-url eye-source))
(with-current-buffer page
(eye-web-find-images-in-html source-url)))))
(defun eye-web-list-links (url callback)
(eye-web-fetch url 'eye-web-list-links-1 callback))
(defun eye-web-file-list-links (name callback &rest args)
(let ((buf (find-file-noselect name nil)))
(unwind-protect
(apply 'eye-web-list-links-1 buf callback args)
(kill-buffer buf))))
(defun eye-web-with-image (callback url index)
(eye-web-fetch
(eye-file index)
`(lambda (f) (eye-with-converted-image f ',callback))))
(defun eye-w3m-load ()
"Load the current link into an `eye-mode' buffer"
(interactive)
(let ((a (and (fboundp 'w3m-anchor) (w3m-anchor))))
(if a
(save-window-excursion
(with-current-buffer (eye a t)
(eye-preload-all)
(message "Eye is watching %s" a)))
(message "No link"))))
;; pdf backend
(defvar eye-pdf-ratio nil "WIDTH / HEIGHT of PDF pages.")
(defun eye-pdf-pdftoppm-command (file page)
(let* ((window-size (eye-window-size t))
(window-ratio (eye-ratio window-size))
(size (if (> window-ratio eye-pdf-ratio)
(if (> eye-pdf-ratio 1)
(* eye-pdf-ratio (cdr window-size))
(cdr window-size))
(if (> eye-pdf-ratio 1)
(car window-size)
(/ (car window-size) eye-pdf-ratio)))))
(when (numberp eye-resize)
(setq size (* size eye-resize)))
;; pages count from 1
(setq page (1+ (mod page (eye-nfiles))))
(split-string
(format "pdftoppm -f %d -l %d -scale-to %d %s"
page page size (shell-quote-argument file)))))
(defun eye-pdf-with-image (callback pdffile page)
(let* ((pdftoppm-cmd (eye-pdf-pdftoppm-command pdffile page))
(convert-args (eye-convert-args))
(tmpfile (eye-temp-file-name
pdffile (append pdftoppm-cmd convert-args))))
(eye-start-process
(list "/bin/sh" "-c"
(format "%s | %s %s - %s"
(mapconcat 'shell-quote-argument pdftoppm-cmd " ")
eye-convert-program
(mapconcat 'shell-quote-argument convert-args " ")
(shell-quote-argument tmpfile)))
callback tmpfile)))
(defun eye-pdf-pages (file callback)
"Find the number of pages in pdf FILE.
Also set `eye-pdf-ratio'."
(let ((str (eye-command-to-string "pdfinfo" file)))
(if (string-match "^Page size: +\\([0-9.]+\\) x \\([0-9.]+\\)" str)
(let ((w (string-to-number (match-string 1 str)))
(h (string-to-number (match-string 2 str))))
(setq eye-pdf-ratio (/ (float w) h)))
(error "Can't discover page size"))
(if (string-match "^Pages: +\\([0-9]+\\)$" str)
(funcall callback (string-to-number (match-string 1 str)))
(error "Can't discover number of pages"))))
;; (defun eye-with-ps-image (callback psfile page)
;; (eye-start-process
;; `("gs" "-dSAFER" "-r150" "-s" "-dNOPAUSE" "-sDEVICE=png16m"
;; "-dTextAlphaBits=4" "-dBATCH" "-dGRAPHICSAlphaBits=4")
;; callback ))
;; (defun eye-doc-with-image (callback docfile page)
;; (eye-start-process `("convert" "-identify"
;; "-geometry" ,(format "x%d" (frame-pixel-height))
;; ,docfile `tmpfile)
;; callback (format "%s-%d.png"
;; (file-name-sans-extension
;; (file-name-nondirectory docfile))
;; page)))
(defsubst eye-with-image (callback index)
"Call CALLBACK with the file name of the INDEX th file
extracted from `eye-source' and properly resized, rotated, etc.
and ready to display."
(funcall (eye-backend-get :extract) callback eye-source index))
;;; ansi frontend
;;
;; this section borrows from ansi-color.el. I didn't use the
;; ansi-color code because it doesn't support highlight colors and is
;; too slow. We only support the small subset of ansi escape sequences
;; that libcaca uses.
;; global vars
(defconst eye-ansi-escape-regexp "\\[\\([0-9;]*\\)m")
(defvar eye-ansi-faces [default (:weight . bold) (:weight . light)
(:slant . italic) (:underline . t)
(:weight . bold) nil (:inverse-video . t)]
"See `ansi-color-faces-vector'")
(defvar eye-ansi-colors ["black" "firebrick4" "green4" "goldenrod3"
"RoyalBlue3" "VioletRed4" "cyan4" "grey90"]
"See `ansi-color-names-vector'")
(defvar eye-ansi-hi-colors ["grey30" "firebrick2" "green2" "goldenrod1"
"RoyalBlue1" "VioletRed3" "cyan3" "white"]
"High intensity version of `eye-ansi-colors'")
(defvar eye-ansi-code-map nil "ANSI code map.
Set from `eye-ansi-faces', `eye-ansi-colors', and `eye-ansi-hi-colors'
by `eye-ansi-make-map'.")
;; local
;; (defvar eye-ansi-data nil "Escape codes go in here while img2txt is running.")
(defvar eye-ansi-current-face nil "Current face for ANSI escapes.")
(defvar eye-ansi-cache nil
"Cache for propertized ansi images")
(defvar eye-ansi-charset 0 "Index into `eye-ansi-charsets'")
(defconst eye-ansi-charsets ["ascii" "shades" "blocks"])
(defvar eye-ansi-point nil "(X% . Y%) point location.")
(defun eye-ansi-cache-get (name width height)
(gethash (list name width height eye-ansi-charset) eye-ansi-cache))
(defun eye-ansi-cache-put (name width height val)
(puthash (list name width height eye-ansi-charset) val eye-ansi-cache))
(defun eye-ansi-cache-clear ()
(setq eye-ansi-cache (make-hash-table :test 'equal)
eye-image-size-cache (make-hash-table :test 'equal)))
(defun eye-ansi-make-map ()
(let ((vec (make-vector 110 nil)))
(dotimes (i 8) (aset vec i (aref eye-ansi-faces i)))
(dotimes (i 8) (aset vec (+ i 30) (cons :foreground (aref eye-ansi-colors i))))
(dotimes (i 8) (aset vec (+ i 40) (cons :background (aref eye-ansi-colors i))))
(dotimes (i 8) (aset vec (+ i 90) (cons :foreground (aref eye-ansi-hi-colors i))))
(dotimes (i 8) (aset vec (+ i 100) (cons :background (aref eye-ansi-hi-colors i))))
vec))
(defsubst eye-ansi-escape-to-face (escape)
"Convert the n;n;n part of an ANSI escape code to text
properties, updating `eye-ansi-current-face'."
(dolist (code (split-string escape ";" t))
(setq code (string-to-number code))
(unless eye-ansi-code-map
(setq eye-ansi-code-map (eye-ansi-make-map)))
(let ((face (aref eye-ansi-code-map code)))
(case face
(default
(setq eye-ansi-current-face nil))
(nil nil)
(otherwise
(setq eye-ansi-current-face
(plist-put eye-ansi-current-face
(car face) (cdr face))))))))
;; (defun eye-ansi-point-save ()
;; (setq eye-ansi-point
;; (cons (/ (float (current-column))
;; (- (line-end-position) (line-beginning-position)))
;; (/ (float (line-number-at-pos))
;; (line-number-at-pos (point-max)) 2))))
;; (defun eye-ansi-point-restore ()
;; (goto-line (round (* (cdr eye-ansi-point)
;; (line-number-at-pos (point-max)) 2)))
;; (forward-char (round (* (car eye-ansi-point)
;; (- (line-end-position) (line-beginning-position)))))
;; (recenter))
(defsubst eye-insert-rectangle (str)
"Like `insert-rectangle', but STR is a string."
;; (insert-rectangle (split-string str)) is too slow.
(save-match-data
(let ((beg 0)
(column (current-column)))
(while (string-match "\n" str beg)
(insert (substring str beg (match-beginning 0)))
(unless (zerop (forward-line 1))
(insert ?\n))
(move-to-column column)
(setq beg (match-end 0)))
(insert (substring str beg)))))
(defun eye-ansi-colorize (str)
"Interpret ANSI codes in STR and return a propertized string."
(with-temp-buffer
(let ((beg 0)
(start-m (point)))
(while (string-match eye-ansi-escape-regexp str beg)
;; highlight before the escape with the old face
(let ((pre-str (substring str beg (match-beginning 0))))
(insert
(if eye-ansi-current-face
(propertize pre-str 'face eye-ansi-current-face)
pre-str)))
(setq beg (match-end 0))
;; process the escape to setup the new face
(eye-ansi-escape-to-face (match-string 1 str)))
(delete-and-extract-region start-m (point)))))
(defun eye-ansi-init ()
(setq cursor-type t))
(defun eye-ansi-display (data size linep)
"Display DATA. On a new line if LINEP."
(when linep
(goto-char (point-max))
(insert ?\n))
(if eye-manga
(beginning-of-line)
(end-of-line))
(eye-insert-rectangle data)
(forward-line (- 1 (cdr size))))
(defun eye-ansi-load-callback (buf callback file width height)
"Callback for when img2txt is done running."
(funcall callback
(eye-ansi-cache-put
file width height
;; FIXME colorize should take a region
(eye-ansi-colorize
(with-current-buffer buf
(delete-and-extract-region (point-min) (point-max)))))
(cons width height)))
(defun eye-ansi-load (file callback)
"Load FILE using 'img2txt' from libcaca.
Call (callback image size)."
;;; (setq eye-ansi-data nil)
(let* ((i-ratio (* 2 (eye-ratio (eye-image-size file))))
(width (1- (window-width)))
(height (window-height))
(w-ratio (/ (float width) height)))
(when (numberp eye-resize)
(setq width (round (* eye-resize width))
height (round (* eye-resize height))))
(if (> w-ratio i-ratio)
(setq width (round (* height i-ratio)))
(setq height (round (/ width i-ratio))))
(let ((cached (eye-ansi-cache-get file width height)))
(if cached
(funcall callback cached (cons width height))
(eye-start-filter-process
(list eye-img2txt "--format=utf8"
(format "--width=%d" width) (format "--height=%d" height)
;;; (format "--charset=%s" (elt eye-ansi-charsets
;;; eye-ansi-charset))
file)
(eye-temp-buffer) 'eye-ansi-load-callback
(eye-temp-buffer) callback file width height)))))
;; image frontend
(defun eye-image-init ()
(setq cursor-type nil))
(defun eye-image-load (file callback)
(let* ((args (if (eye-emacs-has-imagemagick)
(append
(when (eye-rotate-p) (list :rotation eye-rotate))
(when (eye-resize-p)
(let* ((size (eye-window-size t))
(width (car size))
(height (cdr size)))
(if (numberp eye-resize)
(list :width (* width eye-resize) :height (* height eye-resize))
(list :max-width width :max-height height)))))))
(img (apply 'create-image file nil nil args)))
(image-animate img)
(funcall callback img (image-size img))))
(defun eye-image-display (image size linep)
"Actually display IMAGE."
(let* ((inhibit-read-only t))
(goto-char (point-max))
(when linep
(insert "\n"))
(when eye-manga
(beginning-of-line))
(let ((index (+ eye-index eye-last-ndisplay)))
(insert (propertize
" " 'display image 'eye-index index
'help-echo (format "%s (index %d)"
(abbreviate-file-name
(eye-file index))
index))))))
(defsubst eye-frontend-get (fun)
(let* ((front (assoc eye-frontend eye-frontend-alist))
(fn (plist-get (cdr-safe front) fun))
;; (nth (case fun (:load 1) (:display 2) (:init 3)) front)
)
(unless fn
(error "Frontend or method not found! %s %s" eye-frontend fun))
fn))
;; packing
(defvar eye-pack-remaining-width 0 "Width in pixels left on the screen.")
(defvar eye-pack-remaining-height 0 "Height in pixels left on the screen.")
(defvar eye-pack-row-height 0 "Pixels of height in the last row.")
(defvar eye-pack-displayed 0 "Number of images packed into this window.")
(defun eye-pack-init ()
(setq eye-pack-remaining-width (car (eye-window-size))
eye-pack-remaining-height (cdr (eye-window-size))
eye-pack-row-height 0
eye-pack-displayed 0)
(funcall (eye-frontend-get :init)))
(defun eye-pack (size)
"Pack image of SIZE onto the screen.
Return nil if this image does not fit. Return 'line if we started
a new line. Otherwise, return t."
(let* ((width (car size))
(height (cdr size))
(fits t))
(setq eye-pack-remaining-width (- eye-pack-remaining-width width))
(when (< eye-pack-remaining-width 0)
(setq eye-pack-remaining-width (- (car (eye-window-size)) width)
eye-pack-remaining-height (- eye-pack-remaining-height
eye-pack-row-height)
eye-pack-row-height 0)
(setq fits 'line))
(setq eye-pack-row-height (max eye-pack-row-height height))
(when (or (and (eq fits 'line)
(< eye-pack-remaining-height eye-pack-row-height))
(not eye-multi)
(> eye-pack-displayed (eye-nfiles)))
(setq fits nil))
(when (zerop eye-pack-displayed)
(setq fits t))
(setq eye-pack-displayed (1+ eye-pack-displayed))
fits))
(defun eye-pack-display (image size)
"Use the current frontend to display IMAGE with SIZE."
(when (= eye-last-ndisplay 0)
(eye-pack-init))
(let ((packp (eye-pack size)))
(if packp
(progn
(let ((inhibit-read-only t))
(when (= eye-last-ndisplay 0)
(erase-buffer))
(funcall (eye-frontend-get :display)
image size (eq packp 'line)))
(set-buffer-modified-p nil)
(setq eye-last-ndisplay (1+ eye-last-ndisplay))
(eye-mode-line-update)
(eye-with-image 'eye-display (+ eye-index eye-last-ndisplay)))
(unless (eye-redisplay)
(eye-mode-line-update)
(goto-char (point-min))
(when eye-slideshow-timer
(eye-slideshow-start))
(eye-preload)))))
(defun eye-display (file)
"Start display of FILE using the current frontend."
(funcall (eye-frontend-get :load) file 'eye-pack-display))
(defun eye-files-set (files)
(if (or (null files) (equal files []))
(progn
(setq eye-files nil)
(error "No images found"))
(setq eye-files (if (listp files) (vconcat files) files))
(eye-redisplay)))
(defvar eye-last-config nil "Config when we last finished displaying.")
(defun eye-config ()
"Return a list unique for the current display state."
(list eye-resize eye-rotate eye-index eye-multi eye-manga
(null eye-files) (eye-window-size)
eye-frontend (and (eq eye-frontend 'ansi) eye-ansi-charset)
(eye-convert-args)))
(defun eye-redisplay (&optional force)
"Redisplay the images if something changed.
Only redisplay if some settings actually changed, unless FORCE.
Interactively, FORCE is always true. Return non-nil if we
actually start a process."
(interactive (list t))
(eye-mode-line-update)
;;; (unless (file-readable-p eye-source)
;;; (error "Lost source: %s" (abbreviate-file-name eye-source)))
(and
(or force (not (equal (eye-config) eye-last-config)))
(prog1 t
(setq eye-last-config (eye-config)
eye-last-ndisplay 0)
(if eye-files
(eye-with-image 'eye-display eye-index)
(funcall (eye-backend-get :list) eye-source 'eye-files-set)))))
(defun eye-preload-next (image size)
(if (and eye-preload-next (eye-pack size))
(progn
(setq eye-preload-next (1+ eye-preload-next))
(eye-with-image 'eye-preload-pack eye-preload-next))
(when (eq eye-preload 'all)
(setq eye-preload t))
(setq eye-preload-next nil)))
(defun eye-preload-pack (file)
"Continue preloading if FILE will be displayed next."
(unless (eye-redisplay)
(funcall (eye-frontend-get :load) file 'eye-preload-next)))
(defun eye-preload ()
"Preload `eye-preload-next'."
(when (and eye-preload eye-preload-next)
(eye-pack-init)
(if (eq eye-preload 'all)
(let* ((nfiles (eye-nfiles))
(idx eye-preload-next)
(last (mod (1- eye-preload-next) nfiles)))
(while (not (eq idx last))
(eye-with-image (lambda (file) nil) idx)