diff --git a/appdata/modules/Framelix/src/Utils/ImageUtils.php b/appdata/modules/Framelix/src/Utils/ImageUtils.php index e59854f..d3b9556 100644 --- a/appdata/modules/Framelix/src/Utils/ImageUtils.php +++ b/appdata/modules/Framelix/src/Utils/ImageUtils.php @@ -4,11 +4,13 @@ use Framelix\Framelix\Exception\FatalError; -use function end; use function explode; +use function file_exists; +use function file_put_contents; use function implode; use function is_numeric; use function strtolower; +use function unlink; class ImageUtils { @@ -40,26 +42,49 @@ public static function getImageData(string $path): ?array * @param string $dstPath destination path of image * @param int $maxWidth Max width of image in pixels * @param int $maxHeight Max height of image in pixels + * @param string $fit Sharp parameter, see https://sharp.pixelplumbing.com/api-resize#resize + * @param string $position Sharp parameter, see https://sharp.pixelplumbing.com/api-resize#resize * @return Shell Return the shell command that have been executed */ - public static function resize(string $srcPath, string $dstPath, int $maxWidth, int $maxHeight): Shell + public static function resize(string $srcPath, string $dstPath, int $maxWidth, int $maxHeight, string $fit = "inside", string $position = "centre"): Shell { - $pathParts = explode(".", mb_strtolower($srcPath)); - $extension = end($pathParts); - - $shell = Shell::prepare( - 'convert {*}', - [ - $srcPath . ($extension == "gif" ? '[0]' : ''), - '-resize', - "{$maxWidth}x{$maxHeight}>", - '-quality', - '85', - $dstPath, - ] + if (file_exists($dstPath)) { + unlink($dstPath); + } + return self::executeNodeJsSharpCmd( + /**@lang JavaScript */ ' + sharp(' . JsonUtils::encode($srcPath) . ').resize(' . JsonUtils::encode( + ["width" => $maxWidth, "height" => $maxHeight, "fit" => $fit, "position" => $position] + ) . ').toFile(' . JsonUtils::encode($dstPath) . ') + ' ); - $shell->execute(); - return $shell; + } + + /** + * Compress an image, can also convert on the file to new filetype + * @param string $srcPath source path of image + * @param string $dstPath destination path of image Only png, jpeg, jpg, gif and webp is supported + * @return Shell Return the shell command that have been executed + */ + public static function compress(string $srcPath, string $dstPath): Shell + { + $extension = strtolower(pathinfo($dstPath, PATHINFO_EXTENSION)); + $code = /**@lang JavaScript */ + 'const img = sharp(' . JsonUtils::encode($srcPath) . ');'; + if ($extension === "png") { + $code .= "img.png({quality:80});"; + } + if ($extension === "jpg" || $extension === "jpeg") { + $code .= "img.jpeg({quality:80, mozjpeg:true});"; + } + if ($extension === "gif") { + $code .= "img.gif();"; + } + if ($extension === "webp") { + $code .= "img.webp({quality:80});"; + } + $code .= "img.toFile(" . JsonUtils::encode($dstPath) . ");"; + return self::executeNodeJsSharpCmd($code); } /** @@ -121,4 +146,17 @@ public static function compareImages( return $diff <= $threshold; } + public static function executeNodeJsSharpCmd(string $jsCode): Shell + { + $tmpFolder = FileUtils::getTmpFolder(); + $tmpFile = $tmpFolder . "/sharp.js"; + + $jsCode = 'const sharp = require("' . (__DIR__ . "/../../node_modules/sharp/lib/index.js") . '");' . "\n" . $jsCode; + file_put_contents($tmpFile, $jsCode); + + $shell = Shell::prepare("node {*}", [$tmpFile]); + $shell->execute(); + return $shell; + } + } diff --git a/appdata/modules/FramelixTests/test-files/imageutils/test-image-uncompressed.jpg b/appdata/modules/FramelixTests/test-files/imageutils/test-image-uncompressed.jpg new file mode 100644 index 0000000..2116c18 Binary files /dev/null and b/appdata/modules/FramelixTests/test-files/imageutils/test-image-uncompressed.jpg differ diff --git a/appdata/modules/FramelixTests/test-files/imageutils/test-image-uncompressed.png b/appdata/modules/FramelixTests/test-files/imageutils/test-image-uncompressed.png new file mode 100644 index 0000000..d0a4384 Binary files /dev/null and b/appdata/modules/FramelixTests/test-files/imageutils/test-image-uncompressed.png differ diff --git a/appdata/modules/FramelixTests/tests/Utils/ImageUtilsTest.php b/appdata/modules/FramelixTests/tests/Utils/ImageUtilsTest.php index 8e70e80..4202523 100644 --- a/appdata/modules/FramelixTests/tests/Utils/ImageUtilsTest.php +++ b/appdata/modules/FramelixTests/tests/Utils/ImageUtilsTest.php @@ -5,12 +5,14 @@ use Framelix\Framelix\Utils\ImageUtils; use Framelix\FramelixTests\TestCase; +use function filesize; use function unlink; use const FRAMELIX_TMP_FOLDER; final class ImageUtilsTest extends TestCase { + public function testCompare(): void { $this->assertTrue( @@ -47,12 +49,11 @@ public function testResize(): void { $testImage = __DIR__ . "/../../test-files/imageutils/test-image.jpg"; $testResizedImage = FRAMELIX_TMP_FOLDER . "/test-image-resized.jpg"; - ImageUtils::resize($testImage, $testResizedImage, 100, 100); + $shell = ImageUtils::resize($testImage, $testResizedImage, 100, 100); $this->assertEquals(['type' => 'jpeg', 'width' => 100, 'height' => 67], ImageUtils::getImageData($testResizedImage)); unlink($testResizedImage); - $testImage = __DIR__ . "/../../test-files/imageutils/test-image.png"; $testResizedImage = FRAMELIX_TMP_FOLDER . "/test-image-resized.png"; ImageUtils::resize($testImage, $testResizedImage, 100, 100); @@ -61,6 +62,33 @@ public function testResize(): void unlink($testResizedImage); } + public function testCompress(): void + { + $testImage = __DIR__ . "/../../test-files/imageutils/test-image-uncompressed.jpg"; + $testImageCompressed = FRAMELIX_TMP_FOLDER . "/test-image-compressed.jpg"; + ImageUtils::compress($testImage, $testImageCompressed); + $this->assertTrue(filesize($testImageCompressed) < filesize($testImage)); + unlink($testImageCompressed); + + $testImage = __DIR__ . "/../../test-files/imageutils/test-image-uncompressed.png"; + $testImageCompressed = FRAMELIX_TMP_FOLDER . "/test-image-compressed.png"; + ImageUtils::compress($testImage, $testImageCompressed); + $this->assertTrue(filesize($testImageCompressed) < filesize($testImage)); + unlink($testImageCompressed); + + $testImage = __DIR__ . "/../../test-files/imageutils/test-image-uncompressed.png"; + $testImageCompressed = FRAMELIX_TMP_FOLDER . "/test-image-compressed.webp"; + ImageUtils::compress($testImage, $testImageCompressed); + $this->assertTrue(filesize($testImageCompressed) < filesize($testImage)); + unlink($testImageCompressed); + + $testImage = __DIR__ . "/../../test-files/imageutils/test-image-uncompressed.png"; + $testImageCompressed = FRAMELIX_TMP_FOLDER . "/test-image-compressed.gif"; + ImageUtils::compress($testImage, $testImageCompressed); + $this->assertTrue(filesize($testImageCompressed) < filesize($testImage)); + unlink($testImageCompressed); + } + public function testPdfToImage(): void { $testPdf = __DIR__ . "/../../test-files/imageutils/test-pdf.pdf"; @@ -76,4 +104,5 @@ public function testPdfToImage(): void ImageUtils::getImageData($testResizedImage)); unlink($testResizedImage); } + }