'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; } }