With the GD library of PHP you can rotate an image with the imagerotate
function. The downside of this function is that it doesn't clip the edges and that is exactly what I need.
Here's an example image that shows my problem:
As you can see, in Photoshop the edges are clipped. In PHP the size of the image just increased because of the rotation. I really want to get the same result as I have in Photoshop. Any idea how to do this in PHP?
(I only have access to the GD library.)
If you're too lazy to calculate the new size of the rotated image, just use a GD based image library that supports these calculations out of the box.
One such library is Wideimage. You load your original image, get it's width and height, then rotate it, then crop it with so called smart coordinates from center
, middle
and with the original images width and height:
$image = WideImage::load('big.png');
$width = $image->getWidth();
$height = $image->getHeight();
$image->rotate(120)->crop("center", "middle", $width, $height);
The current answer only serves as a way to get around the problem. It does not discuss the math needed to calculate the size of the new box.
You will need to crop the image with imagecrop
after rotating it. To do this, you can use the following formula to keep it centered.
rotated_dimension * (1 - source_dimension / rotated_dimension) * 0.5
Here's a working example. The placehold.it URL can be replaced with a local file path.
<?php
$filename = 'http://placehold.it/200x200';
$degrees = -45;
header('Content-type: image/png');
$source = imagecreatefrompng($filename);
$sw = imagesx($source);
$sh = imagesy($source);
$rotate = imagerotate($source, $degrees, 0);
$rw = imagesx($rotate);
$rh = imagesy($rotate);
$crop = imagecrop($rotate, array(
'x' => $rw * (1 - $sw / $rw) * 0.5,
'y' => $rh * (1 - $sh / $rh) * 0.5,
'width' => $sw,
'height'=> $sh
));
imagepng($crop);
If angle
is the rotation angle, then the width and height of the rotated image, width′
and height′
, is given by:
width′ = height * s + width * c height′ = height * c + width * s
where width
is the source image width, height
is the source image height, and:
s = abs(sin(angle)) c = abs(cos(angle))
Note that because of the trigonometric identities sin(- θ) = - sin(θ) and cos(- θ) = cos(θ), it does not matter in the above formulas if, when measuring angle
, the positive direction is clockwise or counterclockwise.
One thing you know is that the center point of the source image is mapped to the center of the rotated image. Thus, if width′ ≥ width
and height′ ≥ height
, you have that the coordinates of the top-left point within the rotated image are:
x = rotated_width / 2 - width / 2 y = rotated_height / 2 - height / 2
So, if width′ ≥ width
and height′ ≥ height
, the following PHP code will crop the image as desired:
$cropped = imagecrop($rotated, array(
'x' => $rotated_width / 2 - $width / 2,
'y' => $rotated_height / 2 - $height / 2,
'width' => $width,
'height' => $height
));
However, this only works when width′ ≥ width
and height′ ≥ height
. For example, this holds if the dimensions of the source image are square, because then:
length′ = length * (abs(sin(angle)) + abs(cos(angle)))
and abs(sin(angle)) + abs(cos(angle)) ≥ 1
.
See "y = abs(sin(theta)) + abs(cos(theta)) minima" on WolframAlpha.
If width′ ≥ width
and height′ ≥ height
does not hold (e.g. a 250×40 image rotated clockwise by 50°), then the resulting image will be entirely black (as an invalid crop rectangle is passed to imagecrop()).
These issues can be fixed with the following code:
$cropped = imagecrop($rotated, array(
'x' => max(0, $rotated_width / 2 - $width / 2),
'y' => max(0, $rotated_height / 2 - $height / 2),
'width' => min($width, $rotated_width),
'height'=> min($height, $rotated_height)
));
The result of this code is the blue-tinted area in the following diagram:
(See http://fiddle.jshell.net/5jf3wqn4/show/ for an SVG version.)
In the diagram, the translucent red rectangle represents the original 250×40 image. The red rectangle represents the rotation of the image. The dashed rectangle represents the bounds of the image created by imagerotate().
Putting this all together, here is PHP code to rotate and crop the image:
$filename = 'http://placehold.it/250x40';
$degrees = -50;
$source = imagecreatefrompng($filename);
$width = imagesx($source);
$height = imagesy($source);
$rotated = imagerotate($source, $degrees, 0);
imagedestroy($source);
$rotated_width = imagesx($rotated);
$rotated_height = imagesy($rotated);
$cropped = imagecrop($rotated, array(
'x' => max(0, (int)(($rotated_width - $width) / 2)),
'y' => max(0, (int)(($rotated_height - $height) / 2)),
'width' => min($width, $rotated_width),
'height'=> min($height, $rotated_height)
));
imagedestroy($rotated);
imagepng($cropped);
EDIT: There appears to be a bug in imagecrop() where a 1px black line is added to the bottom of the cropped image. See imagecrop() alternative for PHP < 5.5 for a work-around.
EDIT2: I have found that imageaffine() can result in much better quality than imagerotate(). User "abc at ed48 dot com" has commented with the affine transform that you would use to rotate by a given angle counterclockwise.
Here is code to use imageaffine() instead of imagerotate():
// Crops the $source image, avoiding the black line bug in imagecrop()
// See:
// - https://bugs.php.net/bug.php?id=67447
// - https://stackoverflow.com/questions/26722811/imagecrop-alternative-for-php-5-5
function fixedcrop($source, array $rect)
{
$cropped = imagecreate($rect['width'], $rect['height']);
imagecopyresized(
$cropped,
$source,
0,
0,
$rect['x'],
$rect['y'],
$rect['width'],
$rect['height'],
$rect['width'],
$rect['height']
);
return $cropped;
}
$filename = 'http://placehold.it/250x40';
$degrees = -50;
$source = imagecreatefrompng($filename);
$width = imagesx($source);
$height = imagesy($source);
$radians = deg2rad($degrees);
$cos = cos($radians);
$sin = sin($radians);
$affine = [ $cos, -$sin, $sin, $cos, 0, 0 ];
$rotated = imageaffine($source, $affine);
imagedestroy($source);
$rotated_width = imagesx($rotated);
$rotated_height = imagesy($rotated);
$cropped = fixedcrop($rotated, array(
'x' => max(0, (int)(($rotated_width - $width) / 2)),
'y' => max(0, (int)(($rotated_height - $height) / 2)),
'width' => min($width, $rotated_width),
'height'=> min($height, $rotated_height)
));
imagedestroy($rotated);
imagepng($cropped);