aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Ponce2024-03-30 13:59:41 +0100
committerEli Zaretskii2024-03-30 16:35:11 +0300
commitcc212ea314d45c98761ae7f68600ad8bf799ea36 (patch)
treedb4479c9e02708a62ca6ec786002a806e818a8af
parent87be53846bfbf5a6387cb5a40105bd0fc5b48b38 (diff)
downloademacs-cc212ea314d45c98761ae7f68600ad8bf799ea36.tar.gz
emacs-cc212ea314d45c98761ae7f68600ad8bf799ea36.zip
bug#69992: Minor improvement to image map transformation logic
* lisp/image.el (image--compute-rotation): New function. (image--compute-map, image--compute-original-map): Use it. Ensure all transformations are applied or undone according to what Emacs does internally. Call a transformation function only when needed. Fix doc string. (image--scale-map, image--rotate-map): Assume effective scale argument. (image--rotate-coord): Improve doc string. (image--flip-map): Remove no more used argument FLIP. * test/lisp/image-tests.el (image-create-image-with-map): Use a valid SVG image otherwise `image-size' will not return a valid value and calculation of scale could fail. (image-transform-map): Update according to changed signature of `image--flip-map'.
-rw-r--r--lisp/image.el230
-rw-r--r--test/lisp/image-tests.el6
2 files changed, 131 insertions, 105 deletions
diff --git a/lisp/image.el b/lisp/image.el
index d7496485aca..e973dff32c7 100644
--- a/lisp/image.el
+++ b/lisp/image.el
@@ -1423,115 +1423,142 @@ is recomputed to fit the newly transformed image."
1423 :type 'boolean 1423 :type 'boolean
1424 :version "30.1") 1424 :version "30.1")
1425 1425
1426(defsubst image--compute-rotation (image)
1427 "Return the current rotation of IMAGE, or 0 if no rotation.
1428Also return nil if rotation is not a multiples of 90 degrees (0, 90,
1429180[-180] and 270[-90])."
1430 (let ((degrees (or (image-property image :rotation) 0)))
1431 (and (= 0 (mod degrees 1))
1432 (car (memql (truncate (mod degrees 360)) '(0 90 180 270))))))
1433
1426(defun image--compute-map (image) 1434(defun image--compute-map (image)
1427 "Compute map for IMAGE suitable to be used as its :map property. 1435 "Compute map for IMAGE suitable to be used as its :map property.
1428Return a copy of :original-image transformed based on IMAGE's :scale, 1436Return a copy of :original-map transformed based on IMAGE's :scale,
1429:rotation, and :flip. When IMAGE's :original-map is nil, return nil. 1437:rotation, and :flip. When IMAGE's :original-map is nil, return nil.
1430When :rotation is not a multiple of 90, return copy of :original-map." 1438When :rotation is not a multiple of 90, return copy of :original-map."
1431 (pcase-let* ((original-map (image-property image :original-map)) 1439 (when-let ((map (image-property image :original-map)))
1432 (map (copy-tree original-map t)) 1440 (setq map (copy-tree map t))
1433 (scale (or (image-property image :scale) 1)) 1441 (let* ((size (image-size image t))
1434 (rotation (or (image-property image :rotation) 0)) 1442 ;; The image can be scaled for many reasons (:scale,
1435 (flip (image-property image :flip)) 1443 ;; :max-width, etc), so using `image--current-scaling' to
1436 ((and size `(,width . ,height)) (image-size image t))) 1444 ;; calculate the current scaling is the correct method. But,
1437 (when (and ; Handle only 90-degree rotations 1445 ;; since each call to `image_size' is expensive, the code is
1438 (zerop (mod rotation 1)) 1446 ;; duplicated here to save the a call to `image-size'.
1439 (zerop (% (truncate rotation) 90))) 1447 (scale (/ (float (car size))
1440 ;; SIZE fits MAP after transformations. Scale MAP before 1448 (car (image-size
1441 ;; flip and rotate operations, since both need MAP to fit SIZE. 1449 (image--image-without-parameters image) t))))
1442 (image--scale-map map scale) 1450 (rotation (image--compute-rotation image))
1451 ;; Image is flipped only if rotation is a multiple of 90,
1452 ;; including 0.
1453 (flip (and rotation (image-property image :flip))))
1454 ;; SIZE fits MAP after transformations. Scale MAP before flip and
1455 ;; rotate operations, since both need MAP to fit SIZE.
1456 (unless (= scale 1)
1457 (image--scale-map map scale))
1443 ;; In rendered images, rotation is always applied before flip. 1458 ;; In rendered images, rotation is always applied before flip.
1444 (image--rotate-map 1459 (when (memql rotation '(90 180 270))
1445 map rotation (if (or (= 90 rotation) (= 270 rotation)) 1460 (image--rotate-map
1461 map rotation (if (= rotation 180)
1462 size
1446 ;; If rotated ±90°, swap width and height. 1463 ;; If rotated ±90°, swap width and height.
1447 (cons height width) 1464 (cons (cdr size) (car size)))))
1448 size))
1449 ;; After rotation, there's no need to swap width and height. 1465 ;; After rotation, there's no need to swap width and height.
1450 (image--flip-map map flip size)) 1466 (when flip
1467 (image--flip-map map size)))
1451 map)) 1468 map))
1452 1469
1453(defun image--compute-original-map (image) 1470(defun image--compute-original-map (image)
1454 "Return original map for IMAGE. 1471 "Return original map for IMAGE.
1455If IMAGE lacks :map property, return nil. 1472If IMAGE lacks :map property, return nil.
1456When :rotation is not a multiple of 90, return copy of :map." 1473When there is no transformation, return copy of :map."
1457 (when (image-property image :map) 1474 (when-let ((original-map (image-property image :map)))
1458 (let* ((original-map (copy-tree (image-property image :map) t)) 1475 (setq original-map (copy-tree original-map t))
1459 (scale (or (image-property image :scale) 1)) 1476 (let* ((size (image-size image t))
1460 (rotation (or (image-property image :rotation) 0)) 1477 ;; The image can be scaled for many reasons (:scale,
1461 (flip (image-property image :flip)) 1478 ;; :max-width, etc), so using `image--current-scaling' to
1462 (size (image-size image t))) 1479 ;; calculate the current scaling is the correct method. But,
1463 (when (and ; Handle only 90-degree rotations 1480 ;; since each call to `image_size' is expensive, the code is
1464 (zerop (mod rotation 1)) 1481 ;; duplicated here to save the a call to `image-size'.
1465 (zerop (% (truncate rotation) 90))) 1482 (scale (/ (float (car size))
1466 ;; In rendered images, rotation is always applied before flip. 1483 (car (image-size
1467 ;; To undo the transformation, flip before rotating. SIZE fits 1484 (image--image-without-parameters image) t))))
1468 ;; ORIGINAL-MAP before transformations are applied. Therefore, 1485 (rotation (image--compute-rotation image))
1469 ;; scale ORIGINAL-MAP after flip and rotate operations, since 1486 ;; Image is flipped only if rotation is a multiple of 90
1470 ;; both need ORIGINAL-MAP to fit SIZE. 1487 ;; including 0.
1471 (image--flip-map original-map flip size) 1488 (flip (and rotation (image-property image :flip))))
1472 (image--rotate-map original-map (- rotation) size) 1489 ;; In rendered images, rotation is always applied before flip.
1473 (image--scale-map original-map (/ 1.0 scale))) 1490 ;; To undo the transformation, flip before rotating. SIZE fits
1474 original-map))) 1491 ;; ORIGINAL-MAP before transformations are applied. Therefore,
1492 ;; scale ORIGINAL-MAP after flip and rotate operations, since
1493 ;; both need ORIGINAL-MAP to fit SIZE.
1494 ;; In rendered images, rotation is always applied before flip.
1495 (when flip
1496 (image--flip-map original-map size))
1497 (when (memql rotation '(90 180 270))
1498 (image--rotate-map original-map (- rotation) size))
1499 (unless (= scale 1)
1500 (image--scale-map original-map (/ 1.0 scale))))
1501 original-map))
1475 1502
1476(defun image--scale-map (map scale) 1503(defun image--scale-map (map scale)
1477 "Scale MAP according to SCALE. 1504 "Scale MAP according to SCALE.
1478Destructively modifies and returns MAP." 1505Destructively modifies and returns MAP."
1479 (unless (= 1 scale) 1506 (pcase-dolist (`(,`(,type . ,coords) ,_id ,_plist) map)
1480 (pcase-dolist (`(,`(,type . ,coords) ,_id ,_plist) map) 1507 (pcase-exhaustive type
1481 (pcase-exhaustive type 1508 ('rect
1482 ('rect 1509 (setf (caar coords) (round (* (caar coords) scale)))
1483 (setf (caar coords) (round (* (caar coords) scale))) 1510 (setf (cdar coords) (round (* (cdar coords) scale)))
1484 (setf (cdar coords) (round (* (cdar coords) scale))) 1511 (setf (cadr coords) (round (* (cadr coords) scale)))
1485 (setf (cadr coords) (round (* (cadr coords) scale))) 1512 (setf (cddr coords) (round (* (cddr coords) scale))))
1486 (setf (cddr coords) (round (* (cddr coords) scale)))) 1513 ('circle
1487 ('circle 1514 (setf (caar coords) (round (* (caar coords) scale)))
1488 (setf (caar coords) (round (* (caar coords) scale))) 1515 (setf (cdar coords) (round (* (cdar coords) scale)))
1489 (setf (cdar coords) (round (* (cdar coords) scale))) 1516 (setcdr coords (round (* (cdr coords) scale))))
1490 (setcdr coords (round (* (cdr coords) scale)))) 1517 ('poly
1491 ('poly 1518 (dotimes (i (length coords))
1492 (dotimes (i (length coords)) 1519 (aset coords i
1493 (aset coords i 1520 (round (* (aref coords i) scale)))))))
1494 (round (* (aref coords i) scale))))))))
1495 map) 1521 map)
1496 1522
1497(defun image--rotate-map (map rotation size) 1523(defun image--rotate-map (map rotation size)
1498 "Rotate MAP according to ROTATION and SIZE. 1524 "Rotate MAP according to ROTATION and SIZE.
1525ROTATION must be a non-zero multiple of 90.
1499Destructively modifies and returns MAP." 1526Destructively modifies and returns MAP."
1500 (unless (zerop rotation) 1527 (setq rotation (mod rotation 360))
1501 (pcase-dolist (`(,`(,type . ,coords) ,_id ,_plist) map) 1528 (pcase-dolist (`(,`(,type . ,coords) ,_id ,_plist) map)
1502 (pcase-exhaustive type 1529 (pcase-exhaustive type
1503 ('rect 1530 ('rect
1504 (let ( x0 y0 ; New upper left corner 1531 (let ( x0 y0 ; New upper left corner
1505 x1 y1) ; New bottom right corner 1532 x1 y1) ; New bottom right corner
1506 (pcase (truncate (mod rotation 360)) ; Set new corners to... 1533 (pcase rotation ; Set new corners to...
1507 (90 ; ...old bottom left and upper right 1534 (90 ; ...old bottom left and upper right
1508 (setq x0 (caar coords) y0 (cddr coords) 1535 (setq x0 (caar coords) y0 (cddr coords)
1509 x1 (cadr coords) y1 (cdar coords))) 1536 x1 (cadr coords) y1 (cdar coords)))
1510 (180 ; ...old bottom right and upper left 1537 (180 ; ...old bottom right and upper left
1511 (setq x0 (cadr coords) y0 (cddr coords) 1538 (setq x0 (cadr coords) y0 (cddr coords)
1512 x1 (caar coords) y1 (cdar coords))) 1539 x1 (caar coords) y1 (cdar coords)))
1513 (270 ; ...old upper right and bottom left 1540 (270 ; ...old upper right and bottom left
1514 (setq x0 (cadr coords) y0 (cdar coords) 1541 (setq x0 (cadr coords) y0 (cdar coords)
1515 x1 (caar coords) y1 (cddr coords)))) 1542 x1 (caar coords) y1 (cddr coords))))
1516 (setcar coords (image--rotate-coord x0 y0 rotation size)) 1543 (setcar coords (image--rotate-coord x0 y0 rotation size))
1517 (setcdr coords (image--rotate-coord x1 y1 rotation size)))) 1544 (setcdr coords (image--rotate-coord x1 y1 rotation size))))
1518 ('circle 1545 ('circle
1519 (setcar coords (image--rotate-coord 1546 (setcar coords (image--rotate-coord
1520 (caar coords) (cdar coords) rotation size))) 1547 (caar coords) (cdar coords) rotation size)))
1521 ('poly 1548 ('poly
1522 (dotimes (i (length coords)) 1549 (dotimes (i (length coords))
1523 (when (= 0 (% i 2)) 1550 (when (= 0 (% i 2))
1524 (pcase-let ((`(,x . ,y) 1551 (pcase-let ((`(,x . ,y)
1525 (image--rotate-coord 1552 (image--rotate-coord
1526 (aref coords i) (aref coords (1+ i)) rotation size))) 1553 (aref coords i) (aref coords (1+ i)) rotation size)))
1527 (aset coords i x) 1554 (aset coords i x)
1528 (aset coords (1+ i) y)))))))) 1555 (aset coords (1+ i) y)))))))
1529 map) 1556 map)
1530 1557
1531(defun image--rotate-coord (x y angle size) 1558(defun image--rotate-coord (x y angle size)
1532 "Rotate coordinates X and Y by ANGLE in image of SIZE. 1559 "Rotate coordinates X and Y by ANGLE in image of SIZE.
1533ANGLE must be a multiple of 90. Returns a cons cell of rounded 1560ANGLE must be a multiple of 90 in [90 180 270]. Returns a cons cell of
1534coordinates (X1 Y1)." 1561rounded coordinates (X1 Y1)."
1535 (pcase-let* ((radian (* (/ angle 180.0) float-pi)) 1562 (pcase-let* ((radian (* (/ angle 180.0) float-pi))
1536 (`(,width . ,height) size) 1563 (`(,width . ,height) size)
1537 ;; y is positive, but we are in the bottom-right quadrant 1564 ;; y is positive, but we are in the bottom-right quadrant
@@ -1552,25 +1579,24 @@ coordinates (X1 Y1)."
1552 (y1 (- y1))) 1579 (y1 (- y1)))
1553 (cons (round x1) (round y1)))) 1580 (cons (round x1) (round y1))))
1554 1581
1555(defun image--flip-map (map flip size) 1582(defun image--flip-map (map size)
1556 "Horizontally flip MAP according to FLIP and SIZE. 1583 "Horizontally flip MAP according to SIZE.
1557Destructively modifies and returns MAP." 1584Destructively modifies and returns MAP."
1558 (when flip 1585 (pcase-dolist (`(,`(,type . ,coords) ,_id ,_plist) map)
1559 (pcase-dolist (`(,`(,type . ,coords) ,_id ,_plist) map) 1586 (pcase-exhaustive type
1560 (pcase-exhaustive type 1587 ('rect
1561 ('rect 1588 (let ((x0 (- (car size) (cadr coords)))
1562 (let ((x0 (- (car size) (cadr coords))) 1589 (y0 (cdar coords))
1563 (y0 (cdar coords)) 1590 (x1 (- (car size) (caar coords)))
1564 (x1 (- (car size) (caar coords))) 1591 (y1 (cddr coords)))
1565 (y1 (cddr coords))) 1592 (setcar coords (cons x0 y0))
1566 (setcar coords (cons x0 y0)) 1593 (setcdr coords (cons x1 y1))))
1567 (setcdr coords (cons x1 y1)))) 1594 ('circle
1568 ('circle 1595 (setf (caar coords) (- (car size) (caar coords))))
1569 (setf (caar coords) (- (car size) (caar coords)))) 1596 ('poly
1570 ('poly 1597 (dotimes (i (length coords))
1571 (dotimes (i (length coords)) 1598 (when (= 0 (% i 2))
1572 (when (= 0 (% i 2)) 1599 (aset coords i (- (car size) (aref coords i))))))))
1573 (aset coords i (- (car size) (aref coords i)))))))))
1574 map) 1600 map)
1575 1601
1576(provide 'image) 1602(provide 'image)
diff --git a/test/lisp/image-tests.el b/test/lisp/image-tests.el
index 6a5f03e38a0..020781eff50 100644
--- a/test/lisp/image-tests.el
+++ b/test/lisp/image-tests.el
@@ -158,7 +158,7 @@
158(ert-deftest image-create-image-with-map () 158(ert-deftest image-create-image-with-map ()
159 "Test that `create-image' correctly adds :map and/or :original-map." 159 "Test that `create-image' correctly adds :map and/or :original-map."
160 (skip-unless (display-images-p)) 160 (skip-unless (display-images-p))
161 (let ((data "foo") 161 (let ((data "<svg width=\"30\" height=\"30\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"></svg>")
162 (map '(((circle (1 . 1) . 1) a))) 162 (map '(((circle (1 . 1) . 1) a)))
163 (original-map '(((circle (2 . 2) . 2) a))) 163 (original-map '(((circle (2 . 2) . 2) a)))
164 (original-map-other '(((circle (3 . 3) . 3) a)))) 164 (original-map-other '(((circle (3 . 3) . 3) a))))
@@ -282,7 +282,7 @@ corresponding coordinate in B. When nil, TOLERANCE defaults to 5."
282 '(((circle (12 . 4) . 2) "circle") 282 '(((circle (12 . 4) . 2) "circle")
283 ((rect (7 . 3) 9 . 8) "rect") 283 ((rect (7 . 3) 9 . 8) "rect")
284 ((poly . [4 6 2 7 1 2]) "poly")))) 284 ((poly . [4 6 2 7 1 2]) "poly"))))
285 (should (equal (image--flip-map (copy-tree map t) t `(,width . ,height)) 285 (should (equal (image--flip-map (copy-tree map t) `(,width . ,height))
286 '(((circle (6 . 3) . 2) "circle") 286 '(((circle (6 . 3) . 2) "circle")
287 ((rect (2 . 6) 7 . 8) "rect") 287 ((rect (2 . 6) 7 . 8) "rect")
288 ((poly . [4 11 3 13 8 14]) "poly")))) 288 ((poly . [4 11 3 13 8 14]) "poly"))))
@@ -291,7 +291,7 @@ corresponding coordinate in B. When nil, TOLERANCE defaults to 5."
291 ;; Scale size because the map has been scaled. 291 ;; Scale size because the map has been scaled.
292 (image--rotate-map copy 90 `(,(* 2 width) . ,(* 2 height))) 292 (image--rotate-map copy 90 `(,(* 2 width) . ,(* 2 height)))
293 ;; Swap width and height because the map has been flipped. 293 ;; Swap width and height because the map has been flipped.
294 (image--flip-map copy t `(,(* 2 height) . ,(* 2 width))) 294 (image--flip-map copy `(,(* 2 height) . ,(* 2 width)))
295 (should (equal copy 295 (should (equal copy
296 '(((circle (6 . 8) . 4) "circle") 296 '(((circle (6 . 8) . 4) "circle")
297 ((rect (12 . 6) 16 . 16) "rect") 297 ((rect (12 . 6) 16 . 16) "rect")