From 26f0d4d64ce47d8c674df5280250ab27c8ef77d0 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Thu, 4 Jan 2024 09:31:17 -0100 Subject: [PATCH] blurhash generation Signed-off-by: Maxence Lange --- 3rdparty | 2 +- build/psalm-baseline.xml | 14 ++ .../Listener/GenerateBlurhashMetadata.php | 164 ++++++++++++++++++ lib/private/Server.php | 2 + 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php diff --git a/3rdparty b/3rdparty index 5b8a5fc015968..a71bd8af76fdc 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit 5b8a5fc015968956a000d269561cb5ec9d931870 +Subproject commit a71bd8af76fdcfad78c865d1c60f6dde6e24f1dd diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 273c7ef470946..ba52909148299 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2147,6 +2147,20 @@ $jobList + + + $image + $image + $image + $image + + + $image + + + GdImage|false + + \ArrayAccess diff --git a/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php new file mode 100644 index 0000000000000..c88dcb1d7e095 --- /dev/null +++ b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php @@ -0,0 +1,164 @@ + + * + * @author Maxence Lange + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Blurhash\Listener; + +use GdImage; +use kornrunner\Blurhash\Blurhash; +use OC\Files\Node\File; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\GenericFileException; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\FilesMetadata\AMetadataEvent; +use OCP\FilesMetadata\Event\MetadataBackgroundEvent; +use OCP\FilesMetadata\Event\MetadataLiveEvent; +use OCP\IPreview; +use OCP\Lock\LockedException; + +/** + * Generate a Blurhash string as metadata when image file is uploaded/edited. + * + * @template-implements IEventListener + */ +class GenerateBlurhashMetadata implements IEventListener { + private const RESIZE_BOXSIZE = 300; + + private const COMPONENTS_X = 4; + private const COMPONENTS_Y = 3; + + public function __construct( + private IPreview $preview + ) { + } + + /** + * @throws NotPermittedException + * @throws GenericFileException + * @throws LockedException + */ + public function handle(Event $event): void { + if (!($event instanceof MetadataLiveEvent) + && !($event instanceof MetadataBackgroundEvent)) { + return; + } + + $file = $event->getNode(); + if (!($file instanceof File) || !str_starts_with($file->getMimetype(), 'image/')) { + return; + } + + if ($event instanceof MetadataLiveEvent) { + $event->requestBackgroundJob(); + + return; + } + + try { + $preview = $this->preview->getPreview($file, 256, 256); + $image = imagecreatefromstring($preview->getContent()); + } catch (NotFoundException $e) { + // https://github.com/nextcloud/server/blob/9d70fd3e64b60a316a03fb2b237891380c310c58/lib/private/legacy/OC_Image.php#L668 + // The preview system can fail on huge picture, in that case we use our own resize image. + $image = $this->resizedImageFromFile($file); + } + + if ($image === false) { + return; + } + + $metadata = $event->getMetadata(); + $metadata->setString('blurhash', $this->generateBlurHash($image)); + } + + /** + * @param File $file + * + * @return GdImage|false + * @throws GenericFileException + * @throws NotPermittedException + * @throws LockedException + */ + private function resizedImageFromFile(File $file): GdImage|false { + $image = imagecreatefromstring($file->getContent()); + if ($image === false) { + return false; + } + + $currX = imagesx($image); + $currY = imagesy($image); + + if ($currX > $currY) { + $newX = self::RESIZE_BOXSIZE; + $newY = intval($currY * $newX / $currX); + } else { + $newY = self::RESIZE_BOXSIZE; + $newX = intval($currX * $newY / $currY); + } + + $newImage = imagescale($image, $newX, $newY); + if (false !== $newImage) { + $image = $newImage; + } + + return $image; + } + + /** + * @param GdImage $image + * + * @return string + */ + public function generateBlurHash(GdImage $image): string { + $width = imagesx($image); + $height = imagesy($image); + + $pixels = []; + for ($y = 0; $y < $height; ++$y) { + $row = []; + for ($x = 0; $x < $width; ++$x) { + $index = imagecolorat($image, $x, $y); + $colors = imagecolorsforindex($image, $index); + $row[] = [$colors['red'], $colors['green'], $colors['blue']]; + } + + $pixels[] = $row; + } + + return Blurhash::encode($pixels, self::COMPONENTS_X, self::COMPONENTS_Y); + } + + /** + * @param IEventDispatcher $eventDispatcher + * + * @return void + */ + public static function loadListeners(IEventDispatcher $eventDispatcher): void { + $eventDispatcher->addServiceListener(MetadataLiveEvent::class, self::class); + $eventDispatcher->addServiceListener(MetadataBackgroundEvent::class, self::class); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index acc66b9cb0adb..d026ad4286de5 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -68,6 +68,7 @@ use OC\Authentication\LoginCredentials\Store; use OC\Authentication\Token\IProvider; use OC\Avatar\AvatarManager; +use OC\Blurhash\Listener\GenerateBlurhashMetadata; use OC\Collaboration\Collaborators\GroupPlugin; use OC\Collaboration\Collaborators\MailPlugin; use OC\Collaboration\Collaborators\RemoteGroupPlugin; @@ -1482,6 +1483,7 @@ private function connectDispatcher(): void { $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, BeforeUserDeletedListener::class); FilesMetadataManager::loadListeners($eventDispatcher); + GenerateBlurhashMetadata::loadListeners($eventDispatcher); } /**