>From bcd6c010f5fe78d4254d9c5e8181d37415f9744c Mon Sep 17 00:00:00 2001 From: Alan Third Date: Tue, 11 Jun 2019 20:31:24 +0100 Subject: [PATCH] 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. --- doc/lispref/display.texi | 23 +++- src/image.c | 80 ++++++++++++ test/manual/image-transforms-tests.el | 176 ++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 test/manual/image-transforms-tests.el diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi index 93c5217c36..5a5b77e709 100644 --- a/doc/lispref/display.texi +++ b/doc/lispref/display.texi @@ -5181,8 +5181,29 @@ Image Descriptors 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}. diff --git a/src/image.c b/src/image.c index 86f8e8f4bb..6dc14db880 100644 --- a/src/image.c +++ b/src/image.c @@ -1967,6 +1967,86 @@ 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 set of coordinates: + + [x] [m11 m12 tx] [x'] + [y] X [m21 m22 ty] = [y'] + [1] [ 0 0 1] [1 ] + + Luckily 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 larger than 1 make the image larger, values smaller than 1 + make it smaller. Negative values flip the image. + + 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 mathematically + this is 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 diff --git a/test/manual/image-transforms-tests.el b/test/manual/image-transforms-tests.el new file mode 100644 index 0000000000..d601b9397e --- /dev/null +++ b/test/manual/image-transforms-tests.el @@ -0,0 +1,176 @@ +;;; image-transform-tests.el --- Test suite for image transforms. + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Alan Third +;; 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 . + +;;; Commentary: + +;; Type M-x test-transforms RET to generate the test buffer. + +;;; Code: + +(defun test-rotation () + (let ((up "") + (down "") + (left "") + (right "")) + (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 " + + + + + + ") + (top-left " + + ") + (middle " + + + + ") + (bottom-right " + + ")) + (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 " + + + + ") + (large " + + + + ") + (small " + + + + ")) + (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 " + + + ") + (x2-90 " + + + ") + (x2--90 " + + + ") + (x0.5-180 " + + + ")) + (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)))) -- 2.21.0