* lisp/image-mode.el: Fit to width/height for rotated images.

(image-transform-scale, image-transform-right-angle-fudge): New vars.
(image-transform-width, image-transform-fit-width): New functions.
(image-transform-properties): Use them.
(image-transform-check-size): New function.
(image-toggle-display-image): Use it (for testing).
(image-transform-set-rotation): Reduce angle mod 360.
Delete obsolete comment.

Fixes: debbugs:11431
This commit is contained in:
Wolfgang Jenkner 2012-05-14 01:19:46 -04:00 committed by Stefan Monnier
parent 7102e6d0a7
commit 2d21d7f676
2 changed files with 147 additions and 16 deletions

View file

@ -1,3 +1,14 @@
2012-05-14 Wolfgang Jenkner <wjenkner@inode.at>
* image-mode.el: Fit to width/height for rotated images (bug#11431).
(image-transform-scale, image-transform-right-angle-fudge): New vars.
(image-transform-width, image-transform-fit-width): New functions.
(image-transform-properties): Use them.
(image-transform-check-size): New function.
(image-toggle-display-image): Use it (for testing).
(image-transform-set-rotation): Reduce angle mod 360.
Delete obsolete comment.
2012-05-14 Wolfgang Jenkner <wjenkner@inode.at>
* image-mode.el: Fix scaling (bug#11399).

View file

@ -532,6 +532,7 @@ was inserted."
(setq image-type type)
(if (eq major-mode 'image-mode)
(setq mode-name (format "Image[%s]" type)))
(image-transform-check-size)
(if (called-interactively-p 'any)
(message "Repeat this command to go back to displaying the file as text"))))
@ -636,9 +637,122 @@ Its value should be one of the following:
- `fit-width', meaning to fit the image to the window width.
- A number, which is a scale factor (the default size is 1).")
(defvar image-transform-scale 1.0
"The scale factor of the image being displayed.")
(defvar image-transform-rotation 0.0
"Rotation angle for the image in the current Image mode buffer.")
(defvar image-transform-right-angle-fudge 0.0001
"Snap distance to a multiple of a right angle.
There's no deep theory behind the default value, it should just
be somewhat larger than ImageMagick's MagickEpsilon.")
(defsubst image-transform-width (width height)
"Return the bounding box width of a rotated WIDTH x HEIGHT rectangle.
The rotation angle is the value of `image-transform-rotation' in degrees."
(let ((angle (degrees-to-radians image-transform-rotation)))
;; Assume, w.l.o.g., that the vertices of the rectangle have the
;; coordinates (+-w/2, +-h/2) and that (0, 0) is the center of the
;; rotation by the angle A. The projections onto the first axis
;; of the vertices of the rotated rectangle are +- (w/2) cos A +-
;; (h/2) sin A, and the difference between the largest and the
;; smallest of the four values is the expression below.
(+ (* width (abs (cos angle))) (* height (abs (sin angle))))))
;; The following comment and code snippet are from
;; ImageMagick-6.7.4-4/magick/distort.c
;; /* Set the output image geometry to calculated 'bestfit'.
;; Yes this tends to 'over do' the file image size, ON PURPOSE!
;; Do not do this for DePolar which needs to be exact for virtual tiling.
;; */
;; if ( fix_bounds ) {
;; geometry.x = (ssize_t) floor(min.x-0.5);
;; geometry.y = (ssize_t) floor(min.y-0.5);
;; geometry.width=(size_t) ceil(max.x-geometry.x+0.5);
;; geometry.height=(size_t) ceil(max.y-geometry.y+0.5);
;; }
;; Other parts of the same file show that here the origin is in the
;; left lower corner of the image rectangle, the center of the
;; rotation is the center of the rectangle and min.x and max.x
;; (resp. min.y and max.y) are the smallest and the largest of the
;; projections of the vertices onto the first (resp. second) axis.
(defun image-transform-fit-width (width height length)
"Return (w . h) so that a rotated w x h image has exactly width LENGTH.
The rotation angle is the value of `image-transform-rotation'.
Write W for WIDTH and H for HEIGHT. Then the w x h rectangle is
an \"approximately uniformly\" scaled W x H rectangle, which
currently means that w is one of floor(s W) + {0, 1, -1} and h is
floor(s H), where s can be recovered as the value of `image-transform-scale'.
The value of `image-transform-rotation' may be replaced by
a slightly different angle. Currently this is done for values
close to a multiple of 90, see `image-transform-right-angle-fudge'."
(cond ((< (abs (- (mod (+ image-transform-rotation 90) 180) 90))
image-transform-right-angle-fudge)
(assert (not (zerop width)) t)
(setq image-transform-rotation
(float (round image-transform-rotation))
image-transform-scale (/ (float length) width))
(cons length nil))
((< (abs (- (mod (+ image-transform-rotation 45) 90) 45))
image-transform-right-angle-fudge)
(assert (not (zerop height)) t)
(setq image-transform-rotation
(float (round image-transform-rotation))
image-transform-scale (/ (float length) height))
(cons nil length))
(t
(assert (not (and (zerop width) (zerop height))) t)
(setq image-transform-scale
(/ (float (1- length)) (image-transform-width width height)))
;; Assume we have a w x h image and an angle A, and let l =
;; l(w, h) = w |cos A| + h |sin A|, which is the actual width
;; of the bounding box of the rotated image, as calculated by
;; `image-transform-width'. The code snippet quoted above
;; means that ImageMagick puts the rotated image in
;; a bounding box of width L = 2 ceil((w+l+1)/2) - w.
;; Elementary considerations show that this is equivalent to
;; L - w being even and L-3 < l(w, h) <= L-1. In our case, L is
;; the given `length' parameter and our job is to determine
;; reasonable values for w and h which satisfy these
;; conditions.
(let ((w (floor (* image-transform-scale width)))
(h (floor (* image-transform-scale height))))
;; Let w and h as bound above. Then l(w, h) <= l(s W, s H)
;; = L-1 < l(w+1, h+1) = l(w, h) + l(1, 1) <= l(w, h) + 2,
;; hence l(w, h) > (L-1) - 2 = L-3.
(cons
(cond ((= (mod w 2) (mod length 2))
w)
;; l(w+1, h) >= l(w, h) > L-3, but does l(w+1, h) <=
;; L-1 hold?
((<= (image-transform-width (1+ w) h) (1- length))
(1+ w))
;; No, it doesn't, but this implies that l(w-1, h) =
;; l(w+1, h) - l(2, 0) >= l(w+1, h) - 2 > (L-1) -
;; 2 = L-3. Clearly, l(w-1, h) <= l(w, h) <= L-1.
(t
(1- w)))
h)))))
(defun image-transform-check-size ()
"Check that the image exactly fits the width/height of the window."
(unless (numberp image-transform-resize)
(let ((size (image-display-size (image-get-display-property) t)))
(cond ((eq image-transform-resize 'fit-width)
(assert (= (car size)
(- (nth 2 (window-inside-pixel-edges))
(nth 0 (window-inside-pixel-edges))))
t))
((eq image-transform-resize 'fit-height)
(assert (= (cdr size)
(- (nth 3 (window-inside-pixel-edges))
(nth 1 (window-inside-pixel-edges))))
t))))))
(defun image-transform-properties (spec)
"Return rescaling/rotation properties for image SPEC.
These properties are determined by the Image mode variables
@ -647,27 +761,35 @@ return value is suitable for appending to an image spec.
Rescaling and rotation properties only take effect if Emacs is
compiled with ImageMagick support."
(setq image-transform-scale 1.0)
(when (or image-transform-resize
(not (equal image-transform-rotation 0.0)))
(/= image-transform-rotation 0.0))
;; Note: `image-size' looks up and thus caches the untransformed
;; image. There's no easy way to prevent that.
(let* ((size (image-size spec t))
(height
(resized
(cond
((numberp image-transform-resize)
(unless (= image-transform-resize 1)
(floor (* image-transform-resize (cdr size)))))
(setq image-transform-scale image-transform-resize)
(cons nil (floor (* image-transform-resize (cdr size))))))
((eq image-transform-resize 'fit-width)
(image-transform-fit-width
(car size) (cdr size)
(- (nth 2 (window-inside-pixel-edges))
(nth 0 (window-inside-pixel-edges)))))
((eq image-transform-resize 'fit-height)
(- (nth 3 (window-inside-pixel-edges))
(nth 1 (window-inside-pixel-edges))))))
(width (if (eq image-transform-resize 'fit-width)
(- (nth 2 (window-inside-pixel-edges))
(nth 0 (window-inside-pixel-edges))))))
;;TODO fit-to-* should consider the rotation angle
`(,@(if height (list :height height))
,@(if width (list :width width))
,@(if (not (equal 0.0 image-transform-rotation))
(list :rotation image-transform-rotation))))))
(let ((res (image-transform-fit-width
(cdr size) (car size)
(- (nth 3 (window-inside-pixel-edges))
(nth 1 (window-inside-pixel-edges))))))
(cons (cdr res) (car res)))))))
`(,@(when (car resized)
(list :width (car resized)))
,@(when (cdr resized)
(list :height (cdr resized)))
,@(unless (= 0.0 image-transform-rotation)
(list :rotation image-transform-rotation))))))
(defun image-transform-set-scale (scale)
"Prompt for a number, and resize the current image by that amount.
@ -698,9 +820,7 @@ ImageMagick support."
ROTATION should be in degrees. This command has no effect unless
Emacs is compiled with ImageMagick support."
(interactive "nRotation angle (in degrees): ")
;;TODO 0 90 180 270 degrees are the only reasonable angles here
;;otherwise combining with rescaling will get very awkward
(setq image-transform-rotation (float rotation))
(setq image-transform-rotation (float (mod rotation 360)))
(image-toggle-display-image))
(provide 'image-mode)