diff options
| author | Jim Porter | 2024-06-19 20:59:59 -0700 |
|---|---|---|
| committer | Jim Porter | 2024-06-22 23:09:00 -0700 |
| commit | 5f9b5803bea0f360a91e00cd85d72ea7f56d6095 (patch) | |
| tree | f54e14a1871a2fd5c5f276f299058e4a78749f4a | |
| parent | 6f2036243f24369b0b4c35c9b323eb19dad4e4cd (diff) | |
| download | emacs-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.el | 94 | ||||
| -rw-r--r-- | test/lisp/net/shr-tests.el | 64 |
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. | ||
| 288 | This face should remove any unsightly decorations from sliced images. | ||
| 289 | Otherwise, decorations like underlines from links would normally show on | ||
| 290 | every 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. |
| 604 | The size will be rotated between the default size, the original | 612 | The size will cycle through the default size, the original size, and |
| 605 | size, and full-buffer size." | 613 | full-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. | ||
| 34 | See `shr-test-wait-for'.") | ||
| 35 | |||
| 36 | (defun shr-test-wait-for (predicate &optional message) | ||
| 37 | "Wait until PREDICATE returns non-nil. | ||
| 38 | If this takes longer than `shr-test--max-wait-time', raise an error. | ||
| 39 | MESSAGE 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. |
| 34 | Raise a test failure if the rendered buffer does not match NAME.txt. | 50 | Raise a test failure if the rendered buffer does not match NAME.txt. |
| @@ -68,6 +84,8 @@ validate for the NAME testcase. | |||
| 68 | The `rendering' testcase will test NAME once without altering any | 84 | The `rendering' testcase will test NAME once without altering any |
| 69 | settings, then once more for each (OPTION . VALUE) pair.") | 85 | settings, 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 |