180, 6 => -90, 8 => 90, default => null, }; if ($angle !== null) { $image = imagerotate($image, $angle, 0); } } if (function_exists('imagepalettetotruecolor')) { imagepalettetotruecolor($image); } imagealphablending($image, true); imagesavealpha($image, true); // resize / crop before encoding $w = imagesx($image); $h = imagesy($image); if (! empty($params['width']) || ! empty($params['height'])) { [$nw, $nh] = $this->fitSize($w, $h, (int) ($params['width'] ?? 0), (int) ($params['height'] ?? 0)); $scaled = imagecreatetruecolor(max(1, $nw), max(1, $nh)); imagealphablending($scaled, false); imagesavealpha($scaled, true); imagecopyresampled($scaled, $image, 0, 0, 0, 0, $nw, $nh, $w, $h); imagedestroy($image); $image = $scaled; $update(55, 'resizing'); } if (! empty($params['crop_width']) && ! empty($params['crop_height'])) { $cw = min(imagesx($image), (int) $params['crop_width']); $ch = min(imagesy($image), (int) $params['crop_height']); $cx = max(0, min(imagesx($image) - $cw, (int) ($params['crop_x'] ?? 0))); $cy = max(0, min(imagesy($image) - $ch, (int) ($params['crop_y'] ?? 0))); $cropped = imagecreatetruecolor($cw, $ch); imagealphablending($cropped, false); imagesavealpha($cropped, true); imagecopy($cropped, $image, 0, 0, $cx, $cy, $cw, $ch); imagedestroy($image); $image = $cropped; $update(70, 'cropping'); } if (($params['rotate'] ?? 0) != 0) { $deg = in_array(abs((int) $params['rotate']), [90, 180, 270], true) ? (int) $params['rotate'] : 90; $image = imagerotate($image, -$deg, 0); // GD rotates counter-clockwise $update(70, 'rotating'); } if (! empty($params['flip'])) { imageflip($image, $params['flip'] === 'vertical' ? IMG_FLIP_VERTICAL : IMG_FLIP_HORIZONTAL); } $ext = strtolower((string) ($params['format'] ?? $tool['primary_output_format'] ?? pathinfo($job['input_file'], PATHINFO_EXTENSION))); if ($ext === 'jpeg') { $ext = 'jpg'; } $quality = (int) max(30, min(100, (int) ($params['quality'] ?? 85))); [, $out] = $this->outputPath($job, $ext); $ok = match ($ext) { 'jpg' => imagejpeg($image, $out, $quality), 'png' => imagepng($image, $out, (int) round(9 * (1 - $quality / 100))), 'webp' => function_exists('imagewebp') ? imagewebp($image, $out, $quality) : false, 'avif' => function_exists('imageavif') ? imageavif($image, $out, $quality) : false, 'gif' => imagegif($image, $out), default => throw new \RuntimeException("Format {$ext} is not supported."), }; imagedestroy($image); if (! $ok) { throw new \RuntimeException("Saving as {$ext} is not supported by this server."); } $update(95, 'finalizing'); return ['file' => basename($out), 'name' => $this->downloadName($job, $ext), 'ext' => $ext]; } private function fitSize(int $w, int $h, int $targetW, int $targetH): array { if ($targetW > 0 && $targetH > 0) { return [min($targetW, $w * 4), min($targetH, $h * 4)]; } if ($targetW > 0) { return [$targetW, max(1, (int) round($h * $targetW / $w))]; } return [max(1, (int) round($w * $targetH / $h)), $targetH]; } private function outputPath(array $job, string $ext): array { $file = \App\Libraries\Pipeline::safeName($job['id'], $ext); return [$file, \App\Libraries\Pipeline::storageDir() . '/' . $file]; } private function downloadName(array $job, string $ext): string { $base = preg_replace('/[^A-Za-z0-9 _.-]/u', '_', pathinfo((string) ($job['input_name'] ?? 'image'), PATHINFO_FILENAME)) ?: 'image'; return mb_substr($base, 0, 80) . '.' . $ext; } }