MDL-84128 core: Flip the image according to its orientation in PHP GD

This commit is contained in:
meirzamoodle
2025-05-09 11:17:28 +07:00
parent 6999197243
commit ffa4c0ea3f
5 changed files with 155 additions and 4 deletions
@@ -63,6 +63,30 @@ class exifremover_service extends service implements file_redactor_service_inter
/** @var bool $useexiftool Flag indicating whether to use ExifTool. */
private bool $useexiftool = false;
/** @var int Normal orientation (no rotation). */
private const TOP_LEFT = 1;
/** @var int Mirrored horizontally. */
private const TOP_RIGHT = 2;
/** @var int Rotated 180° (upside down). */
private const BOTTOM_RIGHT = 3;
/** @var int Mirrored vertically. */
private const BOTTOM_LEFT = 4;
/** @var int Mirrored horizontally and rotated 270° clockwise. */
private const LEFT_TOP = 5;
/** @var int Rotated 90° clockwise. */
private const RIGHT_TOP = 6;
/** @var int Mirrored horizontally and rotated 90° clockwise. */
private const RIGHT_BOTTOM = 7;
/** @var int Rotated 270° clockwise. */
private const LEFT_BOTTOM = 8;
/**
* Initialise the EXIF remover service.
*/
@@ -161,8 +185,12 @@ class exifremover_service extends service implements file_redactor_service_inter
* @throws moodle_exception If the image data is not successfully recreated.
*/
private function execute_gd(string $sourcefile): string {
// Read EXIF data from the temporary file.
$exifdata = @exif_read_data($sourcefile);
$orientation = isset($exifdata['Orientation']) ? $exifdata['Orientation'] : self::TOP_LEFT;
$filecontent = file_get_contents($sourcefile);
$destinationfile = $this->recreate_image_gd($filecontent);
$destinationfile = $this->recreate_image_gd($filecontent, $orientation);
if (!$destinationfile) {
throw new moodle_exception(
errorcode: 'redactor:exifremover:failedprocessgd',
@@ -242,11 +270,13 @@ class exifremover_service extends service implements file_redactor_service_inter
/**
* Recreate the image using PHP GD library to strip all EXIF data.
*
* @param string $content The source file content
* @param string $content The source file content.
* @param int $orientation The orientation value. The default is 1, which means no rotation.
* @return null|string The path to the recreated image, or null on failure.
*/
private function recreate_image_gd(
string $content,
int $orientation = self::TOP_LEFT,
): ?string {
// Fetch the image information for this image.
$imageinfo = @getimagesizefromstring($content);
@@ -256,6 +286,8 @@ class exifremover_service extends service implements file_redactor_service_inter
// Create a new image from the file.
$image = @imagecreatefromstring($content);
$this->flip_gd($image, $orientation);
$destinationfile = make_request_directory() . '/output';
// Capture the image as a string object, rather than straight to file.
@@ -274,6 +306,44 @@ class exifremover_service extends service implements file_redactor_service_inter
return null;
}
/**
* Flips the given GD image resource based on the specified orientation.
*
* @param \GDImage $image The GD image resource to be flipped.
* @param int $orientation The orientation value indicating how the image should be flipped.
*
* @return void
*/
private function flip_gd(\GDImage &$image, int $orientation): void {
switch ($orientation) {
case self::TOP_LEFT:
break;
case self::TOP_RIGHT:
imageflip($image, IMG_FLIP_HORIZONTAL);
break;
case self::BOTTOM_RIGHT:
$image = imagerotate($image, 180, 0);
break;
case self::BOTTOM_LEFT:
imageflip($image, IMG_FLIP_VERTICAL);
break;
case self::LEFT_TOP:
$image = imagerotate($image, -90, 0);
imageflip($image, IMG_FLIP_HORIZONTAL);
break;
case self::RIGHT_TOP:
$image = imagerotate($image, -90, 0);
break;
case self::RIGHT_BOTTOM:
$image = imagerotate($image, 90, 0);
imageflip($image, IMG_FLIP_HORIZONTAL);
break;
case self::LEFT_BOTTOM:
$image = imagerotate($image, 90, 0);
break;
}
}
/**
* Returns true if the service is enabled, and false if it is not.
*
Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

@@ -67,6 +67,87 @@ final class exifremover_service_test extends \advanced_testcase {
$this->assertStringNotContainsString('Orientation', $newexif);
}
/**
* Tests the `exifremover_service` functionality to flip orientation.
*
* @dataProvider exifremover_service_flip_orientation_provider
* @param string $sourcepath the path to the source image.
* @param string $expectedpath the path to the expected image.
* @param bool $expectedresult the expected result of the comparison.
*/
public function test_exifremover_service_flip_orientation_with_gd(
string $sourcepath,
string $expectedpath,
bool $expectedresult
): void {
$this->resetAfterTest(true);
// Ensure that the exif remover tool path is not set.
set_config('exifremovertoolpath', null, 'core_files');
// Flip the orientation.
$service = new exifremover_service();
$newfile = $service->redact_file_by_path('image/jpeg', $sourcepath);
// Compare the actual and expected images.
$this->assertEquals($expectedresult, $this->compare_images($newfile, $expectedpath));
}
/**
* Data provider for test_exifremover_service_flip_orientation().
*
* @return array
*/
public static function exifremover_service_flip_orientation_provider(): array {
return [
'Flip right-top' => [
'sourcepath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
'expectedpath' => self::get_fixture_path('core_files', 'redactor/topleft.jpg'),
'expectedresult' => true,
],
'The image will not be the same after the flip process' => [
'sourcepath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
'expectedpath' => self::get_fixture_path('core_files', 'redactor/righttop.jpg'),
'expectedresult' => false,
],
];
}
/**
* Compares two images pixel by pixel.
*
* @param string $image1path the path to the first image.
* @param string $image2path the path to the second image.
* @return bool True if the images are identical, false otherwise.
*/
private function compare_images(string $image1path, string $image2path): bool {
$image1 = imagecreatefromjpeg($image1path);
$image2 = imagecreatefromjpeg($image2path);
if (!$image1 || !$image2) {
return false;
}
$width1 = imagesx($image1);
$height1 = imagesy($image1);
$width2 = imagesx($image2);
$height2 = imagesy($image2);
if ($width1 !== $width2 || $height1 !== $height2) {
return false;
}
for ($x = 0; $x < $width1; $x++) {
for ($y = 0; $y < $height1; $y++) {
if (imagecolorat($image1, $x, $y) !== imagecolorat($image2, $x, $y)) {
return false;
}
}
}
return true;
}
/**
* Tests the `exifremover_service` functionality using ExifTool.
*
+2 -2
View File
@@ -32,8 +32,8 @@ $string['redactor'] = 'File redaction';
$string['redactor:exifremover'] = 'EXIF remover';
$string['redactor:exifremover:emptyremovetags'] = 'Remove tags can not be empty!';
$string['redactor:exifremover:enabled'] = 'Enable EXIF remover';
$string['redactor:exifremover:enabled_desc'] = 'By default, EXIF Remover only supports JPG files using PHP GD, or ExifTool if it is configured.
This degrades the quality of the image and removes the orientation tag.
$string['redactor:exifremover:enabled_desc'] = 'By default, EXIF Remover only supports JPG files using PHP GD or ExifTool if configured.
Using PHP GD for this purpose can degrade the quality of the image.
To enhance the performance of EXIF Remover, please configure the ExifTool settings below.