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);