= 8 && \PHP_MINOR_VERSION >= 2; private bool $strip_redraw; private array $exif_readers; private int $image_max_width; private int $image_max_height; private string $thumb_mime; private static function imageCreateFrom(string $file, string $mime): mixed { switch ($mime) { case 'image/jpeg': return \imagecreatefromjpeg($file); case 'image/png': return \imagecreatefrompng($file); case 'image/gif': return \imagecreatefromgif($file); case 'image/webp': return \imagecreatefromwbmp($file); case 'image/bmp': return \imagecreatefrombmp($file); case 'image/avif': return self::PHP_AVIF_SUPPORT ? \imagecreatefromavif($file) : false; } return false; } private static function imageSaveTo(mixed $gd, string $file, string $mime) { // Somebody should tune the quality and speed values... // Won't be me. switch ($mime) { case 'image/jpeg': return \imagejpeg($gd, $file, 50); case 'image/png': return \imagepng($gd, $file, 7); case 'image/gif': return \imagegif($gd, $file); case 'image/webp': return \imagewebp($gd, $file, 50); case 'image/bmp': return \imagebmp($gd, $file, true); case 'image/avif': return self::PHP_AVIF_SUPPORT ? \imageavif($gd, $file, 30, 6) : false; } return false; } private static function enableTransparency(mixed $gd, string $mime) { switch ($mime) { case 'image/png': \imagecolortransparent($gd, \imagecolorallocatealpha($gd, 0, 0, 0, 0)); \imagesavealpha($gd, true); \imagealphablending($gd, false); break; case 'image/gif': \imagecolortransparent($gd, \imagecolorallocatealpha($gd, 0, 0, 0, 0)); \imagesavealpha($gd, true); break; } } private static function createCanvas(string $mime, int $width, int $height) { $gd = \imagecreatetruecolor($width, $height); self::enableTransparency($gd, $mime); return $gd; } private function getOrientation(string $file_path, string $file_mime): ?int { $exif_reader = $this->exif_readers[$file_mime] ?? null; if ($exif_reader !== null) { return $exif_reader->getOrientation($file_path); } return null; } private function generateThumbImpl( mixed $gd, string $source_file_path, string $source_file_mime, string $source_file_kind, string $preferred_out_file_basepath, int $max_width, int $max_height ) { $width = \imagesx($gd); $height = \imagesy($gd); if ($width <= $max_width && $height <= $max_height) { $out_path = $preferred_out_file_basepath . '.' . Metadata\mime_to_ext($source_file_mime); $this->move_or_link_or_copy($source_file_kind, $source_file_path, $out_path); return new ThumbGenerationResult( $out_path, $source_file_mime, $width, $height ); } else { $out_path = $preferred_out_file_basepath . '.' . Metadata\mime_to_ext($this->thumb_mime); $gd_other = self::createCanvas($this->thumb_mime, $max_width, $max_height); \imagecopyresampled($gd_other, $gd, 0, 0, 0, 0, $max_width, $max_height, $width, $height); if (!self::imageSaveTo($gd_other, $out_path, $this->thumb_mime)) { \imagedestroy($gd_other); throw new MediaException("Could not create thumbnail file at '$out_path'", MediaException::ERR_IO_ERR); } \imagedestroy($gd_other); return new ThumbGenerationResult( $out_path, $this->thumb_mime, $max_width, $max_height ); } } /** * @param bool $strip_redraw If the EXIF metadata should be stripped by redrawing it. * May cause the loss of color profiles. Orientation is still handled in JPG and PNG. * @param array $exif_readers A map of mime types to {@link Vichan\Data\Driver\Metadata\ExifReader} instances. */ public function __construct(bool $strip_redraw, array $exif_readers, int $max_width, int $max_height, string $thumb_mime) { $this->strip_redraw = $strip_redraw; $this->exif_readers = $exif_readers; $this->image_max_width = $max_width; $this->image_max_height = $max_height; $this->thumb_mime = $thumb_mime; } public function supportsMime(string $mime): bool { $info = \gd_info(); return ($mime === 'image/jpeg' && $info['JPEG Support']) || ($mime === 'image/png' && $info['PNG Support']) || ($mime === 'image/gif' && $info['GIF Read Support'] && $info['GIF Create Support']) || ($mime === 'image/webp' && $info['WebP Support']) || $mime === 'image/bmp' || ($mime === 'image/avif' && self::PHP_AVIF_SUPPORT && $info['AVIF Support']); } public function openHandle(string $file_path, string $file_mime, int $file_kind): mixed { $exif_orientation = null; $exif_orientation_read = false; try { list($width, $height, $mime) = Metadata\sniff_image($file_path); } catch (\RuntimeException $e) { throw new MediaException("Could not sniff '$file_path'", MediaException::ERR_NO_OPEN, $e); } if ($mime !== $file_mime) { throw new MediaException("Mime type mismatch, expected $file_mime, got $mime", MediaException::ERR_COMPUTE_ERR); } if ($width > $this->image_max_width && $height > $this->image_max_height) { // Too big in all directions. throw new MediaException("Image too big", MediaException::ERR_IMAGE_TOO_LARGE); } elseif ($width > $this->image_max_width || $height > $this->image_max_height) { // Too big in just one direction. // Suppose the image is rotated by the exif orientation, would it fit? if ($width <= $this->image_max_height && $height <= $this->image_max_width) { $exif_orientation_read = true; $exif_orientation = $this->getOrientation($file_path, $file_mime); // If width and height aren't inverted, then it wouldn't fit. if ($exif_orientation === null || !Exif::exifOrientationOnSide($exif_orientation)) { throw new MediaException("Image too big", MediaException::ERR_IMAGE_TOO_LARGE); } } else { throw new MediaException("Image too big", MediaException::ERR_IMAGE_TOO_LARGE); } } $gd = self::imageCreateFrom($file_path, $file_mime); if ($gd === false) { throw new MediaException("Could not open '$file_path'", MediaException::ERR_NO_OPEN); } // Fix the orientation once and for all. if (!$exif_orientation_read) { $exif_orientation = $this->getOrientation($file_path, $file_mime); } if ($exif_orientation !== null) { $degrees = Exif::exifOrientationDegrees($exif_orientation); $flipped = Exif::exifOrientationIsFlipped($exif_orientation); if ($degrees !== 0) { self::enableTransparency($gd, $file_mime); $gd_other = \imagerotate($gd, 360 - $degrees, \imagecolorallocatealpha($gd, 0, 0, 0, 127)); \imagedestroy($gd); if ($gd_other === false) { throw new MediaException("Error while correcting rotation of '$file_path'", MediaException::ERR_COMPUTE_ERR); } $gd = $gd_other; } if ($flipped) { if (!\imageflip($gd, \IMG_FLIP_HORIZONTAL)) { throw new MediaException("Error while correcting flipping of '$file_path'", MediaException::ERR_COMPUTE_ERR); } } } return [ $gd, $file_path, $file_mime, $file_kind ]; } public function closeHandle(mixed $handle) { \imagedestroy($handle[0]); } public function installMediaAndGenerateThumb( mixed $handle, string $media_preferred_out_file_basepath, string $thumb_preferred_out_file_basepath, int $thumb_max_width, int $thumb_max_height ): MediaInstallResult { list($gd, $source_file_path, $source_file_mime, $source_file_kind) = $handle; $out_path = $media_preferred_out_file_basepath . '.' . Metadata\mime_to_ext($source_file_mime); if ($this->strip_redraw) { if (!self::imageSaveTo($gd, $out_path, $source_file_mime)) { throw new MediaException("Could not create media file at '$out_path'", MediaException::ERR_IO_ERR); } } else { $this->move_or_link_or_copy($source_file_kind, $source_file_path, $out_path); } $thumb = $this->generateThumbImpl( $gd, $source_file_path, $source_file_mime, $source_file_kind, $thumb_preferred_out_file_basepath, $thumb_max_width, $thumb_max_height ); return new MediaInstallResult($thumb, $out_path); } public function generateThumb( mixed $handle, string $preferred_out_file_basepath, int $max_width, int $max_height ): ThumbGenerationResult { list($gd, $source_file_path, $source_file_mime, $source_file_kind) = $handle; return $this->generateThumbImpl( $gd, $source_file_path, $source_file_mime, $source_file_kind, $preferred_out_file_basepath, $max_width, $max_height ); } }