init
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Esun\BlogImport\Model;
|
||||
|
||||
use Magento\Framework\Exception\LocalizedException;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class MediaNormalizer
|
||||
{
|
||||
/** @var array<string, int> */
|
||||
private const EXPECTED_IMAGE_TYPES = [
|
||||
'gif' => IMAGETYPE_GIF,
|
||||
'jpeg' => IMAGETYPE_JPEG,
|
||||
'jpg' => IMAGETYPE_JPEG,
|
||||
'png' => IMAGETYPE_PNG,
|
||||
];
|
||||
|
||||
public function needsNormalization(string $path): bool
|
||||
{
|
||||
$expectedType = $this->getExpectedType($path);
|
||||
if ($expectedType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getActualType($path) !== $expectedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert image data in place so that its real format matches its filename extension.
|
||||
*
|
||||
* @throws LocalizedException
|
||||
*/
|
||||
public function normalize(string $path): bool
|
||||
{
|
||||
$expectedType = $this->getExpectedType($path);
|
||||
if ($expectedType === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actualType = $this->getActualType($path);
|
||||
if ($actualType === $expectedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$createCallback = match ($actualType) {
|
||||
IMAGETYPE_GIF => 'imagecreatefromgif',
|
||||
IMAGETYPE_JPEG => 'imagecreatefromjpeg',
|
||||
IMAGETYPE_PNG => 'imagecreatefrompng',
|
||||
IMAGETYPE_WEBP => 'imagecreatefromwebp',
|
||||
default => null,
|
||||
};
|
||||
if ($createCallback === null || !is_callable($createCallback)) {
|
||||
throw new LocalizedException(
|
||||
__('Cannot convert unsupported image data in %1.', $path)
|
||||
);
|
||||
}
|
||||
|
||||
$sourceImage = @$createCallback($path);
|
||||
|
||||
$temporaryPath = tempnam(dirname($path), '.esun-media-');
|
||||
if ($temporaryPath === false) {
|
||||
if ($sourceImage instanceof \GdImage) {
|
||||
imagedestroy($sourceImage);
|
||||
}
|
||||
throw new LocalizedException(__('Unable to create a temporary image beside %1.', $path));
|
||||
}
|
||||
|
||||
try {
|
||||
$written = $sourceImage instanceof \GdImage
|
||||
? $this->writeImage($sourceImage, $temporaryPath, $expectedType)
|
||||
: $this->writeAnimatedWebp($path, $temporaryPath, $actualType, $expectedType);
|
||||
if (!$written) {
|
||||
throw new LocalizedException(__('Unable to encode image %1.', $path));
|
||||
}
|
||||
$permissions = fileperms($path);
|
||||
if ($permissions !== false) {
|
||||
@chmod($temporaryPath, $permissions & 0777);
|
||||
}
|
||||
if (!@rename($temporaryPath, $path)) {
|
||||
throw new LocalizedException(__('Unable to replace image %1 after conversion.', $path));
|
||||
}
|
||||
} finally {
|
||||
if ($sourceImage instanceof \GdImage) {
|
||||
imagedestroy($sourceImage);
|
||||
}
|
||||
if (is_file($temporaryPath)) {
|
||||
@unlink($temporaryPath);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getExpectedType(string $path): ?int
|
||||
{
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
return self::EXPECTED_IMAGE_TYPES[$extension] ?? null;
|
||||
}
|
||||
|
||||
/** @throws LocalizedException */
|
||||
private function getActualType(string $path): int
|
||||
{
|
||||
$imageInfo = @getimagesize($path);
|
||||
if ($imageInfo === false || !isset($imageInfo[2])) {
|
||||
throw new LocalizedException(__('Unable to determine the real image format of %1.', $path));
|
||||
}
|
||||
|
||||
return (int)$imageInfo[2];
|
||||
}
|
||||
|
||||
private function writeImage(\GdImage $sourceImage, string $path, int $expectedType): bool
|
||||
{
|
||||
if ($expectedType === IMAGETYPE_JPEG) {
|
||||
$flattened = imagecreatetruecolor(imagesx($sourceImage), imagesy($sourceImage));
|
||||
if (!$flattened instanceof \GdImage) {
|
||||
return false;
|
||||
}
|
||||
$white = imagecolorallocate($flattened, 255, 255, 255);
|
||||
imagefill($flattened, 0, 0, $white);
|
||||
imagecopy(
|
||||
$flattened,
|
||||
$sourceImage,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
imagesx($sourceImage),
|
||||
imagesy($sourceImage)
|
||||
);
|
||||
$written = imagejpeg($flattened, $path, 90);
|
||||
imagedestroy($flattened);
|
||||
|
||||
return $written;
|
||||
}
|
||||
if ($expectedType === IMAGETYPE_PNG) {
|
||||
imagealphablending($sourceImage, false);
|
||||
imagesavealpha($sourceImage, true);
|
||||
|
||||
return imagepng($sourceImage, $path, 6);
|
||||
}
|
||||
|
||||
return imagegif($sourceImage, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* GD cannot decode animated WebP. Use ffmpeg as a fallback and keep animation when the target is GIF.
|
||||
*
|
||||
* @throws LocalizedException
|
||||
*/
|
||||
private function writeAnimatedWebp(
|
||||
string $sourcePath,
|
||||
string $destinationPath,
|
||||
int $actualType,
|
||||
int $expectedType
|
||||
): bool {
|
||||
if ($actualType !== IMAGETYPE_WEBP) {
|
||||
throw new LocalizedException(__('Unable to decode image %1.', $sourcePath));
|
||||
}
|
||||
|
||||
$outputArguments = match ($expectedType) {
|
||||
IMAGETYPE_GIF => ['-f', 'gif'],
|
||||
IMAGETYPE_JPEG => ['-frames:v', '1', '-c:v', 'mjpeg', '-f', 'image2'],
|
||||
IMAGETYPE_PNG => ['-frames:v', '1', '-c:v', 'png', '-f', 'image2'],
|
||||
default => [],
|
||||
};
|
||||
$command = array_merge(
|
||||
['ffmpeg', '-v', 'error', '-y', '-i', $sourcePath],
|
||||
$outputArguments,
|
||||
[$destinationPath]
|
||||
);
|
||||
$process = new Process($command);
|
||||
$process->setTimeout(60);
|
||||
try {
|
||||
$process->mustRun();
|
||||
} catch (ProcessFailedException $ffmpegException) {
|
||||
@unlink($destinationPath);
|
||||
try {
|
||||
return $this->writeFirstWebpFrame($sourcePath, $destinationPath, $expectedType);
|
||||
} catch (\Throwable $fallbackException) {
|
||||
throw new LocalizedException(
|
||||
__(
|
||||
'Unable to convert animated WebP image %1. Install ffmpeg or the WebP tools (webpmux and dwebp). %2',
|
||||
$sourcePath,
|
||||
trim($fallbackException->getMessage())
|
||||
),
|
||||
$ffmpegException
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return is_file($destinationPath) && filesize($destinationPath) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a static first frame when ffmpeg cannot decode the animated WebP stream.
|
||||
*/
|
||||
private function writeFirstWebpFrame(string $sourcePath, string $destinationPath, int $expectedType): bool
|
||||
{
|
||||
$frameWebp = tempnam(dirname($sourcePath), '.esun-webp-frame-');
|
||||
$framePng = tempnam(dirname($sourcePath), '.esun-png-frame-');
|
||||
if ($frameWebp === false || $framePng === false) {
|
||||
if (is_string($frameWebp)) {
|
||||
@unlink($frameWebp);
|
||||
}
|
||||
if (is_string($framePng)) {
|
||||
@unlink($framePng);
|
||||
}
|
||||
throw new LocalizedException(__('Unable to create temporary WebP frame files.'));
|
||||
}
|
||||
|
||||
$frameImage = null;
|
||||
try {
|
||||
(new Process(['webpmux', '-get', 'frame', '1', $sourcePath, '-o', $frameWebp]))
|
||||
->setTimeout(60)
|
||||
->mustRun();
|
||||
(new Process(['dwebp', $frameWebp, '-o', $framePng]))
|
||||
->setTimeout(60)
|
||||
->mustRun();
|
||||
$frameImage = @imagecreatefrompng($framePng);
|
||||
if (!$frameImage instanceof \GdImage) {
|
||||
throw new LocalizedException(__('Unable to decode the extracted WebP frame.'));
|
||||
}
|
||||
|
||||
return $this->writeImage($frameImage, $destinationPath, $expectedType);
|
||||
} finally {
|
||||
if ($frameImage instanceof \GdImage) {
|
||||
imagedestroy($frameImage);
|
||||
}
|
||||
@unlink($frameWebp);
|
||||
@unlink($framePng);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user