diff --git a/inc/Service/Media/LibMagickMediaHandler.php b/inc/Service/Media/LibMagickMediaHandler.php new file mode 100644 index 00000000..26a0086c --- /dev/null +++ b/inc/Service/Media/LibMagickMediaHandler.php @@ -0,0 +1,258 @@ += 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 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) { + $orientation = $imagick->getImageOrientation(); + $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(); + } + return $degrees == 0 || $degrees == 180; + } + + 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) { + $this->strip_metadata = $strip_metadata; + $this->frames_for_gif_thumbs = $frames_for_gif_thumbs; + } + + public function supportsMime(string $mime): bool { + $ext = Mime\mime_to_ext($mime); + if ($ext === null) { + return false; + } + + return !empty(\Imagick::queryFormats(\strtoupper($ext))); + } + + public function openHandle(string $file_path, string $file_mime, int $file_kind): mixed { + $ext = Mime\mime_to_ext($file_mime); + $path = \realpath($file_path); + // Open it as the supplied mime type. + $imagick = new \Imagick("$ext:$path"); + + return [ $imagick, $file_path, $file_mime, $file_kind ]; + } + + public function closeHandle(mixed $handle) { + $handle[0]->clear(); + } + + public function generateThumb( + mixed $handle, + string $preferred_out_file_dir, + string $preferred_out_file_name, + string $preferred_out_mime, + int $max_width, + int $max_height + ): ThumbGenerationResult { + list($imagick, $source_file_path, $source_file_mime, $source_file_kind) = $handle; + + $width = $imagick->getImageWidth(); + $height = $imagick->getImageHeight(); + + if (!$this->strip_metadata && $width <= $max_width && $height <= $max_height) { + $out_path = $preferred_out_file_dir . \DIRECTORY_SEPARATOR . $preferred_out_file_name . '.' . Mime\mime_to_ext($source_file_mime); + + if ($source_file_kind === self::FILE_KIND_UPLOADED) { + if (!Fs\move_or_copy_uploaded($source_file_path, $out_path)) { + throw new \RuntimeException("Could not move or copy uploaded file '$source_file_path' to '$out_path'"); + } + } else { + if (!Fs\link_or_copy($source_file_path, $out_path)) { + throw new \RuntimeException("Could not link or copy '$source_file_path' to '$out_path'"); + } + } + + return new ThumbGenerationResult( + $out_path, + $source_file_mime, + false, + $max_width, + $max_height + ); + } else { + $swap = self::adjustOrientation($imagick); + if ($swap) { + $tmp = $width; + $width = $height; + $height = $tmp; + } + + if ( + $source_file_mime === 'image/gif' + && $this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_NO + && $imagick->getNumberImages() > 1 + ) { + $out_path = $preferred_out_file_dir . \DIRECTORY_SEPARATOR . $preferred_out_file_name . '.gif'; + + if ($width > $max_width || $height > $max_height) { + $thumb_width = $max_width; + $thumb_height = $max_height; + } else { + $thumb_width = $width; + $thumb_height = $height; + } + + if ($this->frames_for_gif_thumbs !== self::THUMB_KEEP_FRAMES_ALL) { + $other = new \Imagick(); + try { + $other->setFormat('gif'); + + $step = \floor($imagick->getNumberImages() / $this->frames_for_gif_thumbs); + + 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 { + $imagick->stripImage(); + $imagick->optimizeImageLayers(); + + $imagick->setImageCompressionQuality(70); + $imagick->writeImage("gif:$out_path"); + } + + return new ThumbGenerationResult( + $out_path, + 'image/gif', + false, + $thumb_width, + $thumb_height + ); + } else { + if ($width > $max_width || $height > $max_height) { + $thumb_width = $max_width; + $thumb_height = $max_height; + + $imagick->thumbnailImage($max_width, $max_height, true); + } else { + $thumb_width = $width; + $thumb_height = $height; + } + + $out_ext = Mime\mime_to_ext($preferred_out_mime); + $out_path = $preferred_out_file_dir . \DIRECTORY_SEPARATOR . $preferred_out_file_name . '.' . $out_ext; + + $imagick->stripImage(); + $imagick->setImageCompressionQuality(70); + $imagick->writeImage("$out_ext:$out_path"); + + return new ThumbGenerationResult( + $out_path, + $preferred_out_mime, + false, + $thumb_width, + $thumb_height + ); + } + } + } + + public function installMediaAndGenerateThumb( + mixed $handle, + string $media_preferred_out_file_dir, + string $media_preferred_out_file_name, + string $thumb_preferred_out_file_dir, + string $thumb_preferred_out_file_name, + string $thumb_preferred_out_mime, + int $thumb_max_width, + int $thumb_max_height + ): ThumbGenerationResult { + list($imagick, $file_path, $file_mime, $file_kind) = $handle; + } +}