From 4358656c558dd9f43ed7a9f2dfb46c488d88dd7b Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Fri, 16 Apr 2021 10:46:53 +0000 Subject: [PATCH] [ImageThumbnail] Implement image resizing with Intervention/Image --- components/Media/Utils.php | 30 ++++++++++++ .../Controller/ImageThumbnail.php | 17 +++++-- plugins/ImageThumbnail/ImageThumbnail.php | 49 +++++++++++++++++++ .../{Media => ImageThumbnail}/composer.json | 0 src/Entity/AttachmentThumbnail.php | 24 +++++++++ 5 files changed, 116 insertions(+), 4 deletions(-) rename plugins/{Media => ImageThumbnail}/composer.json (100%) diff --git a/components/Media/Utils.php b/components/Media/Utils.php index 20fcf87dc0..1f720a8160 100644 --- a/components/Media/Utils.php +++ b/components/Media/Utils.php @@ -206,4 +206,34 @@ abstract class Utils return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null]; } } + + // ------------------------------ + + /** + * Get the minor part of a mimetype. image/webp -> image + */ + public static function mimetypeMajor(string $mime) + { + return explode('/', self::mimeBare($mime))[0]; + } + + /** + * Get the minor part of a mimetype. image/webp -> webp + */ + public static function mimetypeMinor(string $mime) + { + return explode('/', self::mimeBare($mime))[1]; + } + + /** + * Get only the mimetype and not additional info (separated from bare mime with semi-colon) + */ + public static function mimeBare(string $mimetype) + { + $mimetype = mb_strtolower($mimetype); + if (($semicolon = mb_strpos($mimetype, ';')) !== false) { + $mimetype = mb_substr($mimetype, 0, $semicolon); + } + return trim($mimetype); + } } diff --git a/plugins/ImageThumbnail/Controller/ImageThumbnail.php b/plugins/ImageThumbnail/Controller/ImageThumbnail.php index daa2ad651b..65b1147726 100644 --- a/plugins/ImageThumbnail/Controller/ImageThumbnail.php +++ b/plugins/ImageThumbnail/Controller/ImageThumbnail.php @@ -23,7 +23,10 @@ namespace Plugin\ImageThumbnail\Controller; use App\Core\Controller; use App\Core\DB\DB; +use App\Entity\AttachmentThumbnail; use App\Util\Common; +use Component\Media\Media; +use Plugin\ImageThumbnail\ImageThumbnail as ThisPlugin; use Symfony\Component\HttpFoundation\Request; class ImageThumbnail extends Controller @@ -33,9 +36,9 @@ class ImageThumbnail extends Controller */ public function thumbnail(Request $request, int $id) { - $attachemnt = DB::findOneBy('attachment', ['id' => $id]); - if (!is_null($attachemnt->getScope())) { - // && ($attachemnt->scope | VisibilityScope::PUBLIC) != 0 + $attachment = DB::findOneBy('attachment', ['id' => $id]); + if (!is_null($attachment->getScope())) { + // && ($attachment->scope | VisibilityScope::PUBLIC) != 0 // $user = Common::ensureLoggedIn(); assert(false, 'Attachment scope not implemented'); } @@ -48,6 +51,12 @@ class ImageThumbnail extends Controller $height = Common::clamp($this->int('h') ?? $max_height, min: 0, max: $max_height); $crop = $this->bool('c') ?? false; - dd($width, $height, $crop); + $filename = $attachment->getFilename(); + $filepath = ThisPlugin::getPath($attachment); + + $thumbnail = AttachmentThumbnail::getOrCreate(attachment: $attachment, width: $width, height: $height, crop: $crop); + dd($thumbnail); + + return Media::sendFile(filepath: $filepath, mimetype: $attachment->getMimetype(), output_filename: $filename, disposition: 'inline'); } } diff --git a/plugins/ImageThumbnail/ImageThumbnail.php b/plugins/ImageThumbnail/ImageThumbnail.php index fb59331b86..d6a8f4cde5 100644 --- a/plugins/ImageThumbnail/ImageThumbnail.php +++ b/plugins/ImageThumbnail/ImageThumbnail.php @@ -20,8 +20,12 @@ namespace Plugin\ImageThumbnail; use App\Core\Event; +use function App\Core\I18n\_m; use App\Core\Modules\Module; use App\Core\Router\RouteLoader; +use App\Entity\Attachment; +use App\Util\Common; +use Intervention\Image\Image; class ImageThumbnail extends Module { @@ -30,4 +34,49 @@ class ImageThumbnail extends Module $r->connect('thumbnail', '/thumbnail/{id<\d+>}', [Controller\ImageThumbnail::class, 'thumbnail']); return Event::next; } + + public static function getPath(Attachment $attachment) + { + return Common::config('attachments', 'dir') . $attachment->getFilename(); + } + + /** + * Resizes an image. It will reencode the image in the + * `self::prefferedType()` format. This only applies henceforward, + * not retroactively + * + * Increases the 'memory_limit' to the one in the 'attachments' section in the config, to + * enable the handling of bigger images, which can cause a peak of memory consumption, while + * encoding + * + * @throws Exception + */ + public function onResizeImage(Attachment $attachment, string $outpath, int $width, int $height, bool $crop) + { + $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit')); + + // try { + // $img = Image::make($this->filepath); + // } catch (Exception $e) { + // Log::error(__METHOD__ . ' encountered exception: ' . print_r($e, true)); + // // TRANS: Exception thrown when trying to resize an unknown file type. + // throw new Exception(_m('Unknown file type')); + // } + + // if (self::getPath($attachment) === $outpath) { + // @unlink($outpath); + // } + + // // Fit image to dimensions and optionally prevent upscaling + // if (!$crop) $img->fit($width, $height, function ($constraint) { $constraint->upsize();}); + // else $img->crop($width, $height); + + // $img->save($outpath, 100, 'webp'); + // $img->destroy(); + + // ini_set('memory_limit', $old_limit); // Restore the old memory limit + + // return $outpath; + return Event::next; + } } diff --git a/plugins/Media/composer.json b/plugins/ImageThumbnail/composer.json similarity index 100% rename from plugins/Media/composer.json rename to plugins/ImageThumbnail/composer.json diff --git a/src/Entity/AttachmentThumbnail.php b/src/Entity/AttachmentThumbnail.php index 942bee2a0b..693571ca66 100644 --- a/src/Entity/AttachmentThumbnail.php +++ b/src/Entity/AttachmentThumbnail.php @@ -21,7 +21,13 @@ namespace App\Entity; +use App\Core\DB\DB; use App\Core\Entity; +use App\Core\Event; +use App\Core\Log; +use App\Util\Exception\NotFoundException; +use App\Util\Exception\ServerException; +use Component\Media\Media; use DateTimeInterface; /** @@ -92,6 +98,24 @@ class AttachmentThumbnail extends Entity // }}} Autocode + public static function getOrCreate(Attachment $attachment, ?int $width = null, ?int $height = null, ?bool $crop = null) + { + try { + return DB::findOneBy('attachment_thumbnail', ['attachment_id' => $attachment->getId(), 'width' => $width, 'height' => $height]); + } catch (NotFoundException $e) { + $outpath = $attachment->getFileHash() . '-' . $width . 'x' . $height; + + $event_map = ['image' => 'ResizeImage', 'video' => 'ResizeVideo']; + $major_mime = Media::mimetypeMajor($attachment->getMimetype()); + if (in_array($major_mime, array_keys($event_map))) { + Event::handle($event_map[$major_mime], [$attachment, $outpath, $width, $height, $crop]); + } else { + Log::debug($m = ('Cannot resize attachment with mimetype ' . $attachment->getMimetype())); + throw new ServerException($m); + } + } + } + /** * Delete a attachment thumbnail. This table doesn't own all the attachments, only itself */