Globinours/app/Services/ImageService.php

131 lines
4.9 KiB
PHP

<?php
declare(strict_types=1);
final class ImageService
{
private const ALLOWED = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
public static function inspect(string $path, int $maxBytes = 20_000_000): array
{
if (!is_file($path) || filesize($path) > $maxBytes) {
throw new RuntimeException(t('service.image.missing_large'));
}
$info = @getimagesize($path);
$mime = (string) ($info['mime'] ?? '');
if (!$info || !isset(self::ALLOWED[$mime])) {
throw new RuntimeException(t('service.image.format'));
}
if ((int) $info[0] * (int) $info[1] > 60_000_000) {
throw new RuntimeException(t('service.image.dimensions'));
}
return [
'mime' => $mime,
'extension' => self::ALLOWED[$mime],
'width' => (int) $info[0],
'height' => (int) $info[1],
'size' => (int) filesize($path),
];
}
public static function storeUploaded(
string $tmp,
string $directory,
string $prefix = 'photo',
string $profile = 'photo',
): array {
if (!is_uploaded_file($tmp)) {
throw new RuntimeException(t('service.image.invalid_upload'));
}
return self::store($tmp, $directory, $prefix, $profile, true);
}
public static function storeLocal(
string $source,
string $directory,
string $prefix = 'photo',
string $profile = 'photo',
): array {
return self::store($source, $directory, $prefix, $profile, false);
}
private static function store(
string $tmp,
string $directory,
string $prefix,
string $profile,
bool $moveSource,
): array {
$source = self::inspect($tmp);
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
throw new RuntimeException(t('service.image.directory_failed'));
}
$prefix = preg_replace('/[^a-z0-9_-]+/i', '_', trim($prefix)) ?: 'photo';
$useWebp = $profile !== 'logo';
$extension = $useWebp ? 'webp' : $source['extension'];
$filename = $prefix . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
$target = rtrim($directory, '/') . '/' . $filename;
$stage = rtrim($directory, '/') . '/.' . $filename . '.part.' . $extension;
$optimized = false;
$binary = self::magickBinary();
if ($binary !== null) {
$max = $profile === 'logo' ? '1800x1800>' : '2560x2560>';
$quality = $profile === 'medical' ? '88' : '82';
$command = [$binary, $tmp . '[0]', '-auto-orient', '-strip', '-resize', $max];
if ($extension === 'png') {
$command = array_merge($command, ['-define', 'png:compression-level=9']);
} else {
$command = array_merge($command, ['-quality', (string) $quality]);
}
$command[] = $stage;
$process = proc_open($command, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (is_resource($process)) {
fclose($pipes[0]);
stream_get_contents($pipes[1]);
fclose($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[2]);
$code = proc_close($process);
$optimized = $code === 0 && is_file($stage) && filesize($stage) > 0 && @getimagesize($stage) !== false;
}
}
if ($optimized) {
if (!rename($stage, $target)) {
@unlink($stage);
throw new RuntimeException(t('service.image.optimized_failed'));
}
} else {
if (is_file($stage)) {
@unlink($stage);
}
$extension = $source['extension'];
$filename = $prefix . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
$target = rtrim($directory, '/') . '/' . $filename;
$saved = $moveSource ? move_uploaded_file($tmp, $target) : copy($tmp, $target);
if (!$saved) {
throw new RuntimeException(t('service.image.save_failed'));
}
}
@chmod($target, 0644);
$stored = self::inspect($target);
return array_merge($stored, [
'filename' => $filename,
'path' => $target,
'original_size' => $source['size'],
'optimized' => $optimized,
]);
}
private static function magickBinary(): ?string
{
foreach (
['/usr/bin/magick', '/usr/local/bin/magick', '/usr/bin/convert', '/usr/local/bin/convert']
as $binary
) {
if (is_executable($binary)) {
return $binary;
}
}
return null;
}
}