From 4367a5e6330e3baca6c38cdeae73d93c15ed1e6c Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Thu, 4 Jan 2024 21:36:03 -0100 Subject: blurhash generation Signed-off-by: Maxence Lange --- 3rdparty | 2 +- build/psalm-baseline.xml | 14 ++ lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Blurhash/Listener/GenerateBlurhashMetadata.php | 164 +++++++++++++++++++++ lib/private/Server.php | 2 + 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php diff --git a/3rdparty b/3rdparty index 5b8a5fc0159..a71bd8af76f 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 273c7ef4709..ba529091482 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/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index e9a7d9d5705..201b49f3fae 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -953,6 +953,7 @@ return array( 'OC\\BackgroundJob\\QueuedJob' => $baseDir . '/lib/private/BackgroundJob/QueuedJob.php', 'OC\\BackgroundJob\\TimedJob' => $baseDir . '/lib/private/BackgroundJob/TimedJob.php', 'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php', + 'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php', 'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php', 'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 922db7b36ac..ebcccb328c2 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -986,6 +986,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\BackgroundJob\\QueuedJob' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/QueuedJob.php', 'OC\\BackgroundJob\\TimedJob' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/TimedJob.php', 'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php', + 'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php', 'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php', 'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php', diff --git a/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php b/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php new file mode 100644 index 00000000000..7bfe36e9d41 --- /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)) { + return; + } + + // too heavy to run on the live thread, request a rerun as a background job + if ($event instanceof MetadataLiveEvent) { + $event->requestBackgroundJob(); + return; + } + + $image = false; + try { + // using preview image to generate the blurhash + $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 image resizer. + if (str_starts_with($file->getMimetype(), '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); + return ($newImage !== false) ? $newImage : $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 acc66b9cb0a..d026ad4286d 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -68,6 +68,7 @@ use OC\Authentication\Listeners\UserLoggedInListener; 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 @@ class Server extends ServerContainer implements IServerContainer { $eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, BeforeUserDeletedListener::class); FilesMetadataManager::loadListeners($eventDispatcher); + GenerateBlurhashMetadata::loadListeners($eventDispatcher); } /** -- cgit v1.2.3