diff --git a/core/lib/Drupal/Component/Utility/Rectangle.php b/core/lib/Drupal/Component/Utility/Rectangle.php index 46a08c9..a3e6a06 100644 --- a/core/lib/Drupal/Component/Utility/Rectangle.php +++ b/core/lib/Drupal/Component/Utility/Rectangle.php @@ -18,7 +18,7 @@ * This class implements a calculation algorithm that returns, given input * width, height and rotation angle, dimensions of the expected image after * rotation that are consistent with those produced by the GD rotate image - * toolkit operation using PHP 5.5 and above. + * toolkit operation using PHP 7.0.25 or 7.1.10 and above. * * @see \Drupal\system\Plugin\ImageToolkit\Operation\gd\Rotate */ @@ -81,95 +81,110 @@ public function __construct($width, $height) { * @return $this */ public function rotate($angle) { - // PHP 5.5 GD bug: https://bugs.php.net/bug.php?id=65148: To prevent buggy - // behavior on negative multiples of 30 degrees we convert any negative - // angle to a positive one between 0 and 360 degrees. - $angle -= floor($angle / 360) * 360; - - // For some rotations that are multiple of 30 degrees, we need to correct - // an imprecision between GD that uses C floats internally, and PHP that - // uses C doubles. Also, for rotations that are not multiple of 90 degrees, - // we need to introduce a correction factor of 0.5 to match the GD - // algorithm used in PHP 5.5 (and above) to calculate the width and height - // of the rotated image. if ((int) $angle == $angle && $angle % 90 == 0) { - $imprecision = 0; - $correction = 0; + // For rotations that are multiple of 90 degrees, no trigonometry is + // needed. + if (abs($angle) % 180 == 0) { + $this->boundingWidth = $this->width; + $this->boundingHeight = $this->height; + } + else { + $this->boundingWidth = $this->height; + $this->boundingHeight = $this->width; + } } else { - $imprecision = -0.00001; - $correction = 0.5; + $rotate_affine_transform = $this->gdAffineRotate($angle); + $bounding_box = $this->gdTransformAffineBoundingBox($this->width, $this->height, $rotate_affine_transform); + $this->boundingWidth = $bounding_box['width']; + $this->boundingHeight = $bounding_box['height']; } - - // Do the trigonometry, applying imprecision fixes where needed. - $rad = deg2rad($angle); - $cos = cos($rad); - $sin = sin($rad); - $a = $this->width * $cos; - $b = $this->height * $sin + $correction; - $c = $this->width * $sin; - $d = $this->height * $cos + $correction; - if ((int) $angle == $angle && in_array($angle, [60, 150, 300])) { - $a = $this->fixImprecision($a, $imprecision); - $b = $this->fixImprecision($b, $imprecision); - $c = $this->fixImprecision($c, $imprecision); - $d = $this->fixImprecision($d, $imprecision); - } - - // This is how GD on PHP5.5 calculates the new dimensions. - $this->boundingWidth = abs((int) $a) + abs((int) $b); - $this->boundingHeight = abs((int) $c) + abs((int) $d); - return $this; } /** - * Performs an imprecision check on the input value and fixes it if needed. + * Set up a rotation affine transform. * - * GD that uses C floats internally, whereas we at PHP level use C doubles. - * In some cases, we need to compensate imprecision. + * @param float $angle + * Rotation angle. * - * @param float $input - * The input value. - * @param float $imprecision - * The imprecision factor. + * @return array + * The resulting affine transform. * - * @return float - * A value, where imprecision is added to input if the delta part of the - * input is lower than the absolute imprecision. + * @see https://libgd.github.io/manuals/2.2.2/files/gd_matrix-c.html#gdAffineRotate */ - protected function fixImprecision($input, $imprecision) { - if ($this->delta($input) < abs($imprecision)) { - return $input + $imprecision; - } - return $input; + protected function gdAffineRotate($angle) { + $rad = deg2rad($angle); + $sin_t = sin($rad); + $cos_t = cos($rad); + return [$cos_t, $sin_t, -$sin_t, $cos_t, 0, 0]; } /** - * Returns the fractional part of a float number, unsigned. + * Applies an affine transformation to a point. * - * @param float $input - * The input value. + * @param array $src + * The source point. + * @param array $affine + * The affine transform to apply. * - * @return float - * The fractional part of the input number, unsigned. + * @return array + * The resulting point. + * + * @see https://libgd.github.io/manuals/2.2.2/files/gd_matrix-c.html#gdAffineApplyToPointF */ - protected function fraction($input) { - return abs((int) $input - $input); + protected function gdAffineApplyToPointF(array $src, array $affine) { + return [ + 'x' => $src['x'] * $affine[0] + $src['y'] * $affine[2] + $affine[4], + 'y' => $src['x'] * $affine[1] + $src['y'] * $affine[3] + $affine[5], + ]; } /** - * Returns the difference of a fraction from the closest between 0 and 1. + * Returns the bounding box of an affine transform applied to a rectangle. * - * @param float $input - * The input value. + * @param int $width + * The width of the rectangle. + * @param int $height + * The height of the rectangle. + * @param array $affine + * The affine transform to apply. * - * @return float - * the difference of a fraction from the closest between 0 and 1. + * @return array + * The resulting bounding box. + * + * @see https://libgd.github.io/manuals/2.1.1/files/gd_interpolation-c.html#gdTransformAffineBoundingBox */ - protected function delta($input) { - $fraction = $this->fraction($input); - return $fraction > 0.5 ? (1 - $fraction) : $fraction; + protected function gdTransformAffineBoundingBox($width, $height, $affine) { + $extent = []; + $extent[0]['x'] = 0.0; + $extent[0]['y'] = 0.0; + $extent[1]['x'] = $width; + $extent[1]['y'] = 0.0; + $extent[2]['x'] = $width; + $extent[2]['y'] = $height; + $extent[3]['x'] = 0.0; + $extent[3]['y'] = $height; + + for ($i = 0; $i < 4; $i++) { + $extent[$i] = $this->gdAffineApplyToPointF($extent[$i], $affine); + } + $min = $extent[0]; + $max = $extent[0]; + + for ($i = 1; $i < 4; $i++) { + $min['x'] = $min['x'] > $extent[$i]['x'] ? $extent[$i]['x'] : $min['x']; + $min['y'] = $min['y'] > $extent[$i]['y'] ? $extent[$i]['y'] : $min['y']; + $max['x'] = $max['x'] < $extent[$i]['x'] ? $extent[$i]['x'] : $max['x']; + $max['y'] = $max['y'] < $extent[$i]['y'] ? $extent[$i]['y'] : $max['y']; + } + + return [ + 'x' => (int) $min['x'], + 'y' => (int) $min['y'], + 'width' => (int) ceil(($max['x'] - $min['x'])) + 1, + 'height' => (int) ceil($max['y'] - $min['y']) + 1, + ]; } /** diff --git a/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php b/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php index 49d0833..c1e8252 100644 --- a/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/RectangleTest.php @@ -49,7 +49,7 @@ public function testWrongHeight() { * @covers ::getBoundingWidth * @covers ::getBoundingHeight * - * @dataProvider providerPhp55RotateDimensions + * @dataProvider providerPhp7025RotateDimensions */ public function testRotateDimensions($width, $height, $angle, $exp_width, $exp_height) { $rect = new Rectangle($width, $height); @@ -61,7 +61,8 @@ public function testRotateDimensions($width, $height, $angle, $exp_width, $exp_h /** * Provides data for image dimension rotation tests. * - * This dataset sample was generated by running on PHP 5.5 the function below + * This dataset sample was generated by running on PHP 7.0.25 the function + * below * - first, for all integer rotation angles (-360 to 360) on a rectangle * 40x20; * - second, for 500 random float rotation angle in the range -360 to 360 on @@ -84,6 +85,7 @@ public function testRotateDimensions($width, $height, $angle, $exp_width, $exp_h * if (is_resource($old_res)) { * imagedestroy($old_res); * } + * $image = NULL; * } * @endcode * @@ -97,7 +99,7 @@ public function testRotateDimensions($width, $height, $angle, $exp_width, $exp_h * * @see testRotateDimensions() */ - public function providerPhp55RotateDimensions() { + public function providerPhp7025RotateDimensions() { // The dataset is stored in a .json file because it is very large and causes // problems for PHPCS. return json_decode(file_get_contents(__DIR__ . '/fixtures/RectangleTest.json'));