Commit 11b0e334 authored by Alan Third's avatar Alan Third

Document image transforms

* doc/lispref/display.texi (Image Descriptors): Document :crop and
update :rotation.
* src/image.c: Describe the image transform matrix layout.
* test/manual/image-transforms-tests.el: New file.
parent a1508e8d
......@@ -5181,8 +5181,29 @@ values. If both @code{:scale} and @code{:height}/@code{:width} are
specified, the height/width will be adjusted by the specified scaling
factor.
@item :crop @var{geometry}
This should be a list of the form @code{(@var{width} @var{height}
@var{x} @var{y})}. @var{width} and @var{height} specify the width
and height of the cropped image. If @var{x} is a positive number it
specifies the offset of the cropped area from the left of the original
image, and if negative the offset from the right. If @var{y} is a
positive number it specifies the offset from the top of the original
image, and if negative from the bottom. If @var{x} or @var{y} are
@code{nil} or unspecified the crop area will be centred on the
original image.
If the crop area is outside or overlaps the edge of the image it will
be reduced to exclude any areas outside of the image. This means it
is not possible to use @code{:crop} to increase the size of the image
by entering large @var{width} or @var{height} values.
Cropping is performed after scaling but before rotation.
@item :rotation @var{angle}
Specifies a rotation angle in degrees.
Specifies a rotation angle in degrees. Only multiples of 90 degrees
are supported, unless the image type is @code{imagemagick}. Positive
values rotate clockwise, negative values counter-clockwise. Rotation
is performed after scaling and cropping.
@item :index @var{frame}
@xref{Multi-Frame Images}.
......
......@@ -1967,6 +1967,87 @@ compute_image_size (size_t width, size_t height,
}
#endif /* HAVE_IMAGEMAGICK || HAVE_NATIVE_TRANSFORMS */
/* image_set_rotation, image_set_crop, image_set_size and
image_set_transform use affine transformation matrices to perform
various transforms on the image. The matrix is a 2D array of
doubles. It is laid out like this:
m[0][0] = m11 | m[1][0] = m12 | m[2][0] = tx
--------------+---------------+-------------
m[0][1] = m21 | m[1][1] = m22 | m[2][1] = ty
--------------+---------------+-------------
m[0][2] = 0 | m[1][2] = 0 | m[2][2] = 1
tx and ty represent translations, m11 and m22 represent scaling
transforms and m21 and m12 represent shear transforms. Most
graphics toolkits don't require the third row, however it is
necessary for multiplication.
Transforms are done by creating a matrix for each action we wish to
take, then multiplying the transformation matrix by each of those
matrices in order (matrix multiplication is not commutative).
After we’ve done that we can use our modified transformation matrix
to transform points. We take the x and y coordinates and convert
them into a 3x1 matrix and multiply that by the transformation
matrix and it gives us a new, transformed, set of coordinates:
[m11 m12 tx] [x] [m11*x+m12*y+tx*1] [x']
[m21 m22 ty] X [y] = [m21*x+m22*y+ty*1] = [y']
[ 0 0 1] [1] [ 0*x+0*y+1*1] [ 1]
We don’t have to worry about the last step as the graphics toolkit
will do it for us.
The three transforms we are concerned with are translation, scaling
and rotation. The translation matrix looks like this:
[1 0 tx]
[0 1 ty]
[0 0 1]
Where tx and ty are the amount to translate the origin in the x and
y coordinates, respectively. Since we are translating the origin
and not the image data itself, it can appear backwards in use, for
example to move the image 10 pixels to the right, you would set tx
to -10.
To scale we use:
[x 0 0]
[0 y 0]
[0 0 1]
Where x and y are the amounts to scale in the x and y dimensions.
Values smaller than 1 make the image larger, values larger than 1
make it smaller. Negative values flip the image. For example to
double the image size set x and y to 0.5.
To rotate we use:
[ cos(r) sin(r) 0]
[-sin(r) cos(r) 0]
[ 0 0 1]
Where r is the angle of rotation required. Rotation occurs around
the origin, not the centre of the image. Note that this is
normally considered a counter-clockwise rotation, however because
our y axis is reversed, (0, 0) at the top left, it works as a
clockwise rotation.
The full process of rotating an image is to move the origin to the
centre of the image (width/2, height/2), perform the rotation, and
finally move the origin back to the top left of the image, which
may now be a different corner.
Cropping is easier as we just move the origin to the top left of
where we want to crop and set the width and height accordingly.
The matrices don’t know anything about width and height.
It's possible to pre-calculate the matrix multiplications and just
generate one transform matrix that will do everything we need in a
single step, but the maths for each element is much more complex
and I thought it was better to perform the steps separately. */
typedef double matrix3x3[3][3];
static void
......
;;; image-transform-tests.el --- Test suite for image transforms.
;; Copyright (C) 2019 Free Software Foundation, Inc.
;; Author: Alan Third <alan@idiocy.org>
;; Keywords: internal
;; Human-Keywords: internal
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Type M-x test-transforms RET to generate the test buffer.
;;; Code:
(defun test-rotation ()
(let ((up "<svg height='9' width='9'><polygon points='0,8 4,0 8,8'/></svg>")
(down "<svg height='9' width='9'><polygon points='0,0 4,8 8,0'/></svg>")
(left "<svg height='9' width='9'><polygon points='8,0 0,4 8,8'/></svg>")
(right "<svg height='9' width='9'><polygon points='0,0 8,4 0,8'/></svg>"))
(insert-header "Test Rotation: rotating an image")
(insert-test "0" up up '(:rotation 0))
(insert-test "360" up up '(:rotation 360))
(insert-test "180" down up '(:rotation 180))
(insert-test "-90" left up '(:rotation -90))
(insert-test "90" right up '(:rotation 90))
(insert-test "90.0" right up '(:rotation 90.0))
;; This should log a message and display the unrotated image.
(insert-test "45" up up '(:rotation 45)))
(insert "\n\n"))
(defun test-cropping ()
(let ((image "<svg height='30' width='30'>
<rect x='0' y='0' width='10' height='10'/>
<rect x='10' y='10' width='10' height='10'
style='fill:none;stroke-width:1;stroke:#000'/>
<line x1='10' y1='10' x2='20' y2='20' style='stroke:#000'/>
<line x1='20' y1='10' x2='10' y2='20' style='stroke:#000'/>
<rect x='20' y='20' width='10' height='10'
style='fill:none;stroke-width:1;stroke:#000'/>
</svg>")
(top-left "<svg height='10' width='10'>
<rect x='0' y='0' width='10' height='10'/>
</svg>")
(middle "<svg height='10' width='10'>
<rect x='0' y='0' width='10' height='10'
style='fill:none;stroke-width:1;stroke:#000'/>
<line x1='0' y1='0' x2='10' y2='10' style='stroke:#000'/>
<line x1='10' y1='0' x2='0' y2='10' style='stroke:#000'/>
</svg>")
(bottom-right "<svg height='10' width='10'>
<rect x='0' y='0' width='10' height='10'
style='fill:none;stroke-width:1;stroke:#000'/>
</svg>"))
(insert-header "Test Crop: cropping an image")
(insert-test "all params" top-left image '(:crop (10 10 0 0)))
(insert-test "width/height only" middle image '(:crop (10 10)))
(insert-test "negative x y" middle image '(:crop (10 10 -10 -10)))
(insert-test "all params" bottom-right image '(:crop (10 10 20 20))))
(insert "\n\n"))
(defun test-scaling ()
(let ((image "<svg height='10' width='10'>
<rect x='0' y='0' width='10' height='10'
style='fill:none;stroke-width:1;stroke:#000'/>
<line x1='0' y1='0' x2='10' y2='10' style='stroke:#000'/>
<line x1='10' y1='0' x2='0' y2='10' style='stroke:#000'/>
</svg>")
(large "<svg height='20' width='20'>
<rect x='0' y='0' width='20' height='20'
style='fill:none;stroke-width:2;stroke:#000'/>
<line x1='0' y1='0' x2='20' y2='20'
style='stroke-width:2;stroke:#000'/>
<line x1='20' y1='0' x2='0' y2='20'
style='stroke-width:2;stroke:#000'/>
</svg>")
(small "<svg height='5' width='5'>
<rect x='0' y='0' width='4' height='4'
style='fill:none;stroke-width:1;stroke:#000'/>
<line x1='0' y1='0' x2='4' y2='4' style='stroke:#000'/>
<line x1='4' y1='0' x2='0' y2='4' style='stroke:#000'/>
</svg>"))
(insert-header "Test Scaling: resize an image (pixelization may occur)")
(insert-test "1x" image image '(:scale 1))
(insert-test "2x" large image '(:scale 2))
(insert-test "0.5x" image large '(:scale 0.5))
(insert-test ":max-width" image large '(:max-width 10))
(insert-test ":max-height" image large '(:max-height 10))
(insert-test "width, height" image large '(:width 10 :height 10)))
(insert "\n\n"))
(defun test-scaling-rotation ()
(let ((image "<svg height='20' width='20'>
<rect x='0' y='0' width='20' height='20'
style='fill:none;stroke-width:1;stroke:#000'/>
<rect x='0' y='0' width='10' height='10'
style='fill:#000'/>
</svg>")
(x2-90 "<svg height='40' width='40'>
<rect x='0' y='0' width='40' height='40'
style='fill:none;stroke-width:1;stroke:#000'/>
<rect x='20' y='0' width='20' height='20'
style='fill:#000'/>
</svg>")
(x2--90 "<svg height='40' width='40'>
<rect x='0' y='0' width='40' height='40'
style='fill:none;stroke-width:1;stroke:#000'/>
<rect x='0' y='20' width='20' height='20'
style='fill:#000'/>
</svg>")
(x0.5-180 "<svg height='10' width='10'>
<rect x='0' y='0' width='10' height='10'
style='fill:none;stroke-width:1;stroke:#000'/>
<rect x='5' y='5' width='5' height='5'
style='fill:#000'/>
</svg>"))
(insert-header "Test Scaling and Rotation: resize and rotate an image (pixelization may occur)")
(insert-test "1x, 0 degrees" image image '(:scale 1 :rotation 0))
(insert-test "2x, 90 degrees" x2-90 image '(:scale 2 :rotation 90.0))
(insert-test "2x, -90 degrees" x2--90 image '(:scale 2 :rotation -90.0))
(insert-test "0.5x, 180 degrees" x0.5-180 image '(:scale 0.5 :rotation 180.0)))
(insert "\n\n"))
(defun insert-header (description)
(insert description)
(insert "\n")
(indent-to 38)
(insert "expected")
(indent-to 48)
(insert "result")
(when (fboundp #'imagemagick-types)
(indent-to 58)
(insert "ImageMagick"))
(insert "\n"))
(defun insert-test (description expected image params)
(indent-to 2)
(insert description)
(indent-to 40)
(insert-image (create-image expected 'svg t))
(indent-to 50)
(insert-image (apply #'create-image image 'svg t params))
(when (fboundp #'imagemagick-types)
(indent-to 60)
(insert-image (apply #'create-image image 'imagemagick t params)))
(insert "\n"))
(defun test-transforms ()
(interactive)
(let ((buf (get-buffer "*Image Transform Test*")))
(if buf
(kill-buffer buf))
(switch-to-buffer (get-buffer-create "*Image Transform Test*"))
(erase-buffer)
(unless #'imagemagick-types
(insert "ImageMagick not detected. ImageMagick tests will be skipped.\n\n"))
(test-rotation)
(test-cropping)
(test-scaling)
(test-scaling-rotation)
(goto-char (point-min))))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment