131 lines
4.9 KiB
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;
|
|
}
|
|
}
|