2

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:

Photoshop vs GD rotate

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

Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
w00
  • 26,172
  • 30
  • 101
  • 147
  • 2
    It's just a matter of simple math: You need to cut out a rectangle the size of the original image. The rectangle's center would be the center of the new image – Pekka Nov 06 '11 at 16:11
  • have you considered using the browser's CSS rotation instead? (yes, it can be done in all browsers, even old versions of IE) – Spudley Nov 06 '11 at 17:40
  • Use CSS for the rotation; place the image inside a container; give the container a fixed width, and set its CSS property "overflow" to "hidden"; that'll take care of the problem. – Kneel-Before-ZOD Aug 24 '12 at 18:27

3 Answers3

7

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);
hakre
  • 193,403
  • 52
  • 435
  • 836
3

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:

diagram depicting cropping area

(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);
Community
  • 1
  • 1
Daniel Trebbien
  • 38,421
  • 18
  • 121
  • 193
1

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);
Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171