aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJim Porter2024-06-19 20:59:59 -0700
committerJim Porter2024-06-22 23:09:00 -0700
commit5f9b5803bea0f360a91e00cd85d72ea7f56d6095 (patch)
treef54e14a1871a2fd5c5f276f299058e4a78749f4a
parent6f2036243f24369b0b4c35c9b323eb19dad4e4cd (diff)
downloademacs-5f9b5803bea0f360a91e00cd85d72ea7f56d6095.tar.gz
emacs-5f9b5803bea0f360a91e00cd85d72ea7f56d6095.zip
Fix zooming images in SHR
Previously, for images with no alt-text, the zoomed image wouldn't get properly inserted. For images with alt-text, both the zoomed and unzoomed image would be displayed at once (bug#71666). * lisp/net/shr.el (shr-sliced-image): New face. (shr-zoom-image): Reimplement using 'next/previous-single-property-change', and don't bother deleting any of the text. (shr-image-fetched): Clean up any overlays when deleting the old region. (shr-put-image): Ensure we always have a non-empty string to put the image on. For sliced images, just use "*", since we'll repeat it, so we can't preserve the original buffer text exactly anyway. Apply an overlay to sliced images to prevent unsightly text decorations. (shr-tag-img): Move the placeholder space insertion where it should be and explain what it's doing. * test/lisp/net/shr-tests.el (shr-test--max-wait-time) (shr-test-wait-for): New helper functions. (shr-test/zoom-image): New test.
-rw-r--r--lisp/net/shr.el94
-rw-r--r--test/lisp/net/shr-tests.el64
2 files changed, 116 insertions, 42 deletions
diff --git a/lisp/net/shr.el b/lisp/net/shr.el
index 14b3f7aa163..3dadcb9a09b 100644
--- a/lisp/net/shr.el
+++ b/lisp/net/shr.el
@@ -282,6 +282,14 @@ temporarily blinks with this face."
282 "Face used for <mark> elements." 282 "Face used for <mark> elements."
283 :version "29.1") 283 :version "29.1")
284 284
285(defface shr-sliced-image
286 '((t :underline nil :overline nil))
287 "Face used for sliced images.
288This face should remove any unsightly decorations from sliced images.
289Otherwise, decorations like underlines from links would normally show on
290every slice."
291 :version "30.1")
292
285(defcustom shr-inhibit-images nil 293(defcustom shr-inhibit-images nil
286 "If non-nil, inhibit loading images." 294 "If non-nil, inhibit loading images."
287 :version "28.1" 295 :version "28.1"
@@ -600,38 +608,34 @@ the URL of the image to the kill buffer instead."
600 t)))) 608 t))))
601 609
602(defun shr-zoom-image () 610(defun shr-zoom-image ()
603 "Toggle the image size. 611 "Cycle the image size.
604The size will be rotated between the default size, the original 612The size will cycle through the default size, the original size, and
605size, and full-buffer size." 613full-buffer size."
606 (interactive) 614 (interactive)
607 (let ((url (get-text-property (point) 'image-url)) 615 (let ((url (get-text-property (point) 'image-url)))
608 (size (get-text-property (point) 'image-size))
609 (buffer-read-only nil))
610 (if (not url) 616 (if (not url)
611 (message "No image under point") 617 (message "No image under point")
612 ;; Delete the old picture. 618 (let* ((end (or (next-single-property-change (point) 'image-url)
613 (while (get-text-property (point) 'image-url) 619 (point-max)))
614 (forward-char -1)) 620 (start (or (previous-single-property-change end 'image-url)
615 (forward-char 1) 621 (point-min)))
616 (let ((start (point))) 622 (size (get-text-property (point) 'image-size))
617 (while (get-text-property (point) 'image-url) 623 (next-size (cond ((or (eq size 'default)
618 (forward-char 1)) 624 (null size))
619 (forward-char -1) 625 'original)
620 (put-text-property start (point) 'display nil) 626 ((eq size 'original)
621 (when (> (- (point) start) 2) 627 'full)
622 (delete-region start (1- (point))))) 628 ((eq size 'full)
623 (message "Inserting %s..." url) 629 'default)))
624 (url-retrieve url #'shr-image-fetched 630 (buffer-read-only nil))
625 (list (current-buffer) (1- (point)) (point-marker) 631 ;; Delete the old picture.
626 (list (cons 'size 632 (put-text-property start end 'display nil)
627 (cond ((or (eq size 'default) 633 (message "Inserting %s..." url)
628 (null size)) 634 (url-retrieve url #'shr-image-fetched
629 'original) 635 `(,(current-buffer) ,start
630 ((eq size 'original) 636 ,(set-marker (make-marker) end)
631 'full) 637 ((size . ,next-size)))
632 ((eq size 'full) 638 t)))))
633 'default)))))
634 t))))
635 639
636;;; Utility functions. 640;;; Utility functions.
637 641
@@ -1070,6 +1074,7 @@ the mouse click event."
1070 ;; We don't want to record these changes. 1074 ;; We don't want to record these changes.
1071 (buffer-undo-list t) 1075 (buffer-undo-list t)
1072 (inhibit-read-only t)) 1076 (inhibit-read-only t))
1077 (remove-overlays start end)
1073 (delete-region start end) 1078 (delete-region start end)
1074 (goto-char start) 1079 (goto-char start)
1075 (funcall shr-put-image-function data alt flags) 1080 (funcall shr-put-image-function data alt flags)
@@ -1144,7 +1149,8 @@ element is the data blob and the second element is the content-type."
1144 ;; putting any space after inline images. 1149 ;; putting any space after inline images.
1145 ;; ALT may be nil when visiting image URLs in eww 1150 ;; ALT may be nil when visiting image URLs in eww
1146 ;; (bug#67764). 1151 ;; (bug#67764).
1147 (setq alt (if alt (string-trim alt) "*")) 1152 (setq alt (string-trim (or alt "")))
1153 (when (length= alt 0) (setq alt "*"))
1148 ;; When inserting big-ish pictures, put them at the 1154 ;; When inserting big-ish pictures, put them at the
1149 ;; beginning of the line. 1155 ;; beginning of the line.
1150 (let ((inline (shr--inline-image-p image))) 1156 (let ((inline (shr--inline-image-p image)))
@@ -1153,7 +1159,16 @@ element is the data blob and the second element is the content-type."
1153 (insert "\n")) 1159 (insert "\n"))
1154 (let ((image-pos (point))) 1160 (let ((image-pos (point)))
1155 (if (eq size 'original) 1161 (if (eq size 'original)
1156 (insert-sliced-image image alt nil 20 1) 1162 ;; Normally, we try to keep the buffer text the same
1163 ;; by preserving ALT. With a sliced image, we have to
1164 ;; repeat the text for each line, so we can't do that.
1165 ;; Just use "*" for the string to insert instead.
1166 (progn
1167 (insert-sliced-image image "*" nil 20 1)
1168 (let ((overlay (make-overlay start (point))))
1169 ;; Avoid displaying unsightly decorations on the
1170 ;; image slices.
1171 (overlay-put overlay 'face 'shr-sliced-image)))
1157 (insert-image image alt)) 1172 (insert-image image alt))
1158 (put-text-property start (point) 'image-size size) 1173 (put-text-property start (point) 'image-size size)
1159 (when (and (not inline) shr-max-inline-image-size) 1174 (when (and (not inline) shr-max-inline-image-size)
@@ -1854,17 +1869,12 @@ The preference is a float determined from `shr-prefer-media-type'."
1854 (let ((file (url-cache-create-filename url))) 1869 (let ((file (url-cache-create-filename url)))
1855 (when (file-exists-p file) 1870 (when (file-exists-p file)
1856 (delete-file file)))) 1871 (delete-file file))))
1857 (when (image-type-available-p 'svg) 1872 (if (image-type-available-p 'svg)
1858 (insert-image 1873 (insert-image
1859 (shr-make-placeholder-image dom) 1874 (shr-make-placeholder-image dom)
1860 (or (string-trim alt) ""))) 1875 (or (string-trim alt) ""))
1861 ;; Paradoxically this space causes shr not to insert spaces after 1876 ;; No SVG support. Just use a space as our placeholder.
1862 ;; inline images. Since the image is temporary it seem like there 1877 (insert " "))
1863 ;; should be no downside to not inserting it but since I don't
1864 ;; understand the code well and for the sake of backward compatibility
1865 ;; we preserve it unless user has set `shr-max-inline-image-size'.
1866 (unless shr-max-inline-image-size
1867 (insert " "))
1868 (url-queue-retrieve 1878 (url-queue-retrieve
1869 url #'shr-image-fetched 1879 url #'shr-image-fetched
1870 (list (current-buffer) start (set-marker (make-marker) (point)) 1880 (list (current-buffer) start (set-marker (make-marker) (point))
diff --git a/test/lisp/net/shr-tests.el b/test/lisp/net/shr-tests.el
index 17138053450..b6552674b27 100644
--- a/test/lisp/net/shr-tests.el
+++ b/test/lisp/net/shr-tests.el
@@ -29,6 +29,22 @@
29 29
30(declare-function libxml-parse-html-region "xml.c") 30(declare-function libxml-parse-html-region "xml.c")
31 31
32(defvar shr-test--max-wait-time 5
33 "The maximum amount of time to wait for a condition to resolve, in seconds.
34See `shr-test-wait-for'.")
35
36(defun shr-test-wait-for (predicate &optional message)
37 "Wait until PREDICATE returns non-nil.
38If this takes longer than `shr-test--max-wait-time', raise an error.
39MESSAGE is an optional message to use if this times out."
40 (let ((start (current-time))
41 (message (or message "timed out waiting for condition")))
42 (while (not (funcall predicate))
43 (when (> (float-time (time-since start))
44 shr-test--max-wait-time)
45 (error message))
46 (sit-for 0.1))))
47
32(defun shr-test--rendering-check (name &optional context) 48(defun shr-test--rendering-check (name &optional context)
33 "Render NAME.html and compare it to NAME.txt. 49 "Render NAME.html and compare it to NAME.txt.
34Raise a test failure if the rendered buffer does not match NAME.txt. 50Raise a test failure if the rendered buffer does not match NAME.txt.
@@ -68,6 +84,8 @@ validate for the NAME testcase.
68The `rendering' testcase will test NAME once without altering any 84The `rendering' testcase will test NAME once without altering any
69settings, then once more for each (OPTION . VALUE) pair.") 85settings, then once more for each (OPTION . VALUE) pair.")
70 86
87;;; Tests:
88
71(ert-deftest rendering () 89(ert-deftest rendering ()
72 (skip-unless (fboundp 'libxml-parse-html-region)) 90 (skip-unless (fboundp 'libxml-parse-html-region))
73 (dolist (file (directory-files (ert-resource-directory) nil "\\.html\\'")) 91 (dolist (file (directory-files (ert-resource-directory) nil "\\.html\\'"))
@@ -114,6 +132,52 @@ settings, then once more for each (OPTION . VALUE) pair.")
114 (should (equal (shr--parse-srcset "https://example.org/1,2\n\n 10w , https://example.org/2 20w ") 132 (should (equal (shr--parse-srcset "https://example.org/1,2\n\n 10w , https://example.org/2 20w ")
115 '(("https://example.org/2" 20) ("https://example.org/1,2" 10))))) 133 '(("https://example.org/2" 20) ("https://example.org/1,2" 10)))))
116 134
135(ert-deftest shr-test/zoom-image ()
136 "Test that `shr-zoom-image' properly replaces the original image."
137 (let ((image (expand-file-name "data/image/blank-100x200.png"
138 (getenv "EMACS_TEST_DIRECTORY"))))
139 (dolist (alt '(nil "" "nothing to see here"))
140 (with-temp-buffer
141 (ert-info ((format "image with alt=%S" alt))
142 (let ((attrs (if alt (format " alt=\"%s\"" alt) "")))
143 (insert (format "<img src=\"file://%s\" %s" image attrs)))
144 (cl-letf* (;; Pretend we're a graphical display.
145 ((symbol-function 'display-graphic-p) #'always)
146 ((symbol-function 'url-queue-retrieve)
147 (lambda (&rest args)
148 (apply #'run-at-time 0 nil #'url-retrieve args)))
149 (put-image-calls 0)
150 (shr-put-image-function
151 (lambda (&rest args)
152 (cl-incf put-image-calls)
153 (apply #'shr-put-image args)))
154 (shr-width 80)
155 (shr-use-fonts nil)
156 (shr-image-animate nil)
157 (inhibit-message t)
158 (dom (libxml-parse-html-region (point-min) (point-max))))
159 ;; Render the document.
160 (erase-buffer)
161 (shr-insert-document dom)
162 (shr-test-wait-for (lambda () (= put-image-calls 1)))
163 ;; Now zoom the image.
164 (goto-char (point-min))
165 (shr-zoom-image)
166 (shr-test-wait-for (lambda () (= put-image-calls 2)))
167 ;; Check that we got a sliced image.
168 (let ((slice-count 0))
169 (goto-char (point-min))
170 (while (< (point) (point-max))
171 (when-let ((display (get-text-property (point) 'display)))
172 ;; If this is nil, we found a non-sliced image, but we
173 ;; should have replaced that!
174 (should (assq 'slice display))
175 (cl-incf slice-count))
176 (goto-char (or (next-single-property-change (point) 'display)
177 (point-max))))
178 ;; Make sure we actually saw a slice.
179 (should (> slice-count 1)))))))))
180
117(require 'shr) 181(require 'shr)
118 182
119;;; shr-tests.el ends here 183;;; shr-tests.el ends here