diff options
| author | David Ponce | 2024-03-30 13:59:41 +0100 |
|---|---|---|
| committer | Eli Zaretskii | 2024-03-30 16:35:11 +0300 |
| commit | cc212ea314d45c98761ae7f68600ad8bf799ea36 (patch) | |
| tree | db4479c9e02708a62ca6ec786002a806e818a8af | |
| parent | 87be53846bfbf5a6387cb5a40105bd0fc5b48b38 (diff) | |
| download | emacs-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.el | 230 | ||||
| -rw-r--r-- | test/lisp/image-tests.el | 6 |
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. | ||
| 1428 | Also return nil if rotation is not a multiples of 90 degrees (0, 90, | ||
| 1429 | 180[-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. |
| 1428 | Return a copy of :original-image transformed based on IMAGE's :scale, | 1436 | Return 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. |
| 1430 | When :rotation is not a multiple of 90, return copy of :original-map." | 1438 | When :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. |
| 1455 | If IMAGE lacks :map property, return nil. | 1472 | If IMAGE lacks :map property, return nil. |
| 1456 | When :rotation is not a multiple of 90, return copy of :map." | 1473 | When 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. |
| 1478 | Destructively modifies and returns MAP." | 1505 | Destructively 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. |
| 1525 | ROTATION must be a non-zero multiple of 90. | ||
| 1499 | Destructively modifies and returns MAP." | 1526 | Destructively 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. |
| 1533 | ANGLE must be a multiple of 90. Returns a cons cell of rounded | 1560 | ANGLE must be a multiple of 90 in [90 180 270]. Returns a cons cell of |
| 1534 | coordinates (X1 Y1)." | 1561 | rounded 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. |
| 1557 | Destructively modifies and returns MAP." | 1584 | Destructively 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") |