= 2.3.0 and ImageMagick >= 6.4.0 private const MIN_IMAGICK_VERSION = '2.3.0'; private const MIN_IMAGEMAGICK_VERSION = '6.4.0'; public const THUMB_KEEP_FRAMES_NO = 0; public const THUMB_KEEP_FRAMES_ALL = -1; private bool $strip_metadata; private bool $frames_for_gif_thumbs; private int $image_max_width; private int $image_max_height; private string $static_thumb_mime; private static function degreesFromOrientation(int $orientation): int { switch ($orientation) { case \Imagick::ORIENTATION_UNDEFINED: case \Imagick::ORIENTATION_TOPLEFT: case \Imagick::ORIENTATION_TOPRIGHT: default: return 0; case \Imagick::ORIENTATION_BOTTOMRIGHT: case \Imagick::ORIENTATION_BOTTOMLEFT: return 180; case \Imagick::ORIENTATION_LEFTTOP: case \Imagick::ORIENTATION_RIGHTTOP: return 90; case \Imagick::ORIENTATION_RIGHTBOTTOM: case \Imagick::ORIENTATION_LEFTBOTTOM: return 270; } } public static function isFlippedFromOrientation(int $orientation): bool { switch ($orientation) { case \Imagick::ORIENTATION_UNDEFINED: case \Imagick::ORIENTATION_TOPLEFT: default: return false; case \Imagick::ORIENTATION_TOPRIGHT: return true; case \Imagick::ORIENTATION_BOTTOMRIGHT: return false; case \Imagick::ORIENTATION_BOTTOMLEFT: return true; case \Imagick::ORIENTATION_LEFTTOP: return false; case \Imagick::ORIENTATION_RIGHTTOP: return true; case \Imagick::ORIENTATION_RIGHTBOTTOM: return false; case \Imagick::ORIENTATION_LEFTBOTTOM: return true; } } /** * @return bool Returns if width and height were swapped. */ private static function adjustOrientation(\Imagick $imagick, int $orientation): void { $degrees = self::degreesFromOrientation($orientation); $flipped = self::isFlippedFromOrientation($orientation); if ($degrees !== 0) { // Used to return an int, make it always a bool. $has_alpha = !!$imagick->getImageAlphaChannel(); $background = $has_alpha ? '#0000' : '#000'; $imagick->rotateImage($background, $degrees); } if ($flipped) { $imagick->flopImage(); } } private function generateThumbImpl( \Imagick $imagick, string $source_file_mime, string $preferred_out_file_basepath, int $width, int $height, int $max_width, int $max_height ) { // Special handling for gifs with multiple frames. if ( $source_file_mime === 'image/gif' && $this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_NO && $imagick->getNumberImages() > 1 ) { $out_path = $preferred_out_file_basepath . '.gif'; if ($width > $max_width || $height > $max_height) { $thumb_width = $max_width; $thumb_height = $max_height; } else { $thumb_width = $width; $thumb_height = $height; } // By now $this->frames_for_gif_thumbs !== 0. $step = \floor($imagick->getNumberImages() / $this->frames_for_gif_thumbs); if ($this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_ALL && $step > 1) { // Reduce the number of frames. $other = new \Imagick(); try { $other->setFormat('gif'); for ($i = 0, $j = 0; $i < $imagick->getNumberImages(); $i += $step, $j++) { $imagick->setIteratorIndex($i); $delay = $imagick->getImageDelay(); $imagick->sampleImage($thumb_width, $thumb_height); $imagick->setImagePage($thumb_width, $thumb_height, 0, 0); $imagick->setImageDelay($delay); $other->addImage($imagick->getImage()); } $other->optimizeImageLayers(); $other->setImageCompressionQuality(70); $other->writeImage("gif:$out_path"); } finally { $other->clear(); } } else { // Just try to optimize it a little. $imagick->stripImage(); $imagick->optimizeImageLayers(); $imagick->setImageCompressionQuality(70); $imagick->writeImage("gif:$out_path"); } return new ThumbGenerationResult( $out_path, 'image/gif', $thumb_width, $thumb_height ); } else { if ($width > $max_width || $height > $max_height) { $thumb_width = $max_width; $thumb_height = $max_height; // Unreliable behavior on some versions if the target width/height are under the limit? $imagick->thumbnailImage($max_width, $max_height, true); } else { $thumb_width = $width; $thumb_height = $height; } $out_ext = Metadata\mime_to_ext($this->static_thumb_mime); $out_path = $preferred_out_file_basepath . '.' . $out_ext; $imagick->stripImage(); $imagick->setImageCompressionQuality(70); $imagick->writeImage("$out_ext:$out_path"); return new ThumbGenerationResult( $out_path, $this->static_thumb_mime, $thumb_width, $thumb_height ); } } /** * Checks if the Imagick and ImageMagick are recent enough to support the required features. * @return bool */ public static function checkImagickVersion(): bool { $imagick_ver = \phpversion('imagick'); if ($imagick_ver !== false && \version_compare($imagick_ver, self::MIN_IMAGICK_VERSION, '>=')) { $str = \Imagick::getVersion()['versionString']; if (\preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $str, $matches)) { return \version_compare($matches[1], self::MIN_IMAGEMAGICK_VERSION, '>='); } } return false; } public function __construct(bool $strip_metadata, int $frames_for_gif_thumbs, int $max_width, int $max_height, string $static_thumb_mime) { $this->strip_metadata = $strip_metadata; $this->frames_for_gif_thumbs = $frames_for_gif_thumbs; $this->image_max_width = $max_width; $this->image_max_height = $max_height; $this->static_thumb_mime = $static_thumb_mime; } public function supportsMime(string $mime): bool { if (!\in_array($mime, Metadata\SUPPORTED_IMAGE_MIME_TYPES)) { return false; } $ext = Metadata\mime_to_ext($mime); if ($ext === null) { return false; } $ext = \strtoupper($ext); return !empty(\Imagick::queryFormats("$ext*")); } public function openHandle(string $file_path, string $file_mime, int $file_kind): mixed { $ext = Metadata\mime_to_ext($file_mime); $path = \realpath($file_path); // Open it as the supplied mime type. $imagick = new \Imagick("$ext:$path"); $orientation = $imagick->getImageOrientation(); if (self::isFlippedFromOrientation($orientation)) { $width = $imagick->getImageHeight(); $height = $imagick->getImageWidth(); } else { $width = $imagick->getImageWidth(); $height = $imagick->getImageHeight(); } if ($width > $this->image_max_width && $height > $this->image_max_height) { // Too big even if rotated. $imagick->clear(); throw new MediaException("Image too big", MediaException::ERR_IMAGE_TOO_LARGE); } return [ $imagick, $file_path, $file_mime, $file_kind, $width, $height, $orientation ]; } public function closeHandle(mixed $handle) { $handle[0]->clear(); } public function generateThumb( mixed $handle, string $preferred_out_file_basepath, int $max_width, int $max_height ): ThumbGenerationResult { list($imagick, $source_file_path, $source_file_mime, $source_file_kind, $width, $height, $orientation) = $handle; if (!$this->strip_metadata && $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, $max_width, $max_height ); } else { self::adjustOrientation($imagick, $orientation); return self::generateThumbImpl( $imagick, $source_file_mime, $preferred_out_file_basepath, $width, $height, $max_width, $max_height ); } } 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($imagick, $media_file_path, $media_file_mime, $media_file_kind, $width, $height, $orientation) = $handle; if (!$this->strip_metadata) { $media_out_path = $media_preferred_out_file_basepath . '.' . Metadata\mime_to_ext($media_file_mime); $this->move_or_link_or_copy($media_file_kind, $media_file_path, $media_out_path); if ($width <= $thumb_max_width && $height >= $thumb_max_height) { $thumb_out_path = $thumb_preferred_out_file_basepath . '.' . Metadata\mime_to_ext($media_file_mime); if (!Fs\link_or_copy($media_out_path, $thumb_out_path)) { throw new MediaException("Could not link or copy '$media_out_path' to '$thumb_out_path'", MediaException::ERR_IO_ERR); } $thumb = new ThumbGenerationResult($thumb_out_path, $media_file_mime, $width, $height); } else { self::adjustOrientation($imagick, $orientation); $thumb = self::generateThumbImpl( $imagick, $media_file_mime, $thumb_preferred_out_file_basepath, $width, $height, $thumb_max_width, $thumb_max_height ); } return new MediaInstallResult($thumb, $media_out_path); } else { self::adjustOrientation($imagick, $orientation); // Backup the color profile, then re-apply it. $profiles = $imagick->getImageProfiles('icc', true); $imagick->stripImage(); if (!empty($profiles)) { $imagick->profileImage('icc', $profiles['icc']); } $out_ext = Metadata\mime_to_ext($media_file_mime); $out_path = $media_preferred_out_file_basepath . '.' . $out_ext; $imagick->writeImage("$out_ext:$out_path"); $thumb = self::generateThumbImpl( $imagick, $media_file_mime, $thumb_preferred_out_file_basepath, $width, $height, $thumb_max_width, $thumb_max_height ); return new MediaInstallResult($thumb, $out_path); } } }