diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2024-01-04 21:36:03 -0100 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2024-01-05 11:50:51 +0100 |
commit | 4367a5e6330e3baca6c38cdeae73d93c15ed1e6c (patch) | |
tree | b9d974b43b61ef1fcf3cf70bedb24df4098440d2 | |
parent | 7c3936b6f8f9b2c2b2c1cf7e917c885b58ef9d72 (diff) | |
download | nextcloud-server-4367a5e6330e3baca6c38cdeae73d93c15ed1e6c.tar.gz nextcloud-server-4367a5e6330e3baca6c38cdeae73d93c15ed1e6c.zip |
blurhash generation
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
m--------- | 3rdparty | 0 | ||||
-rw-r--r-- | build/psalm-baseline.xml | 14 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php | 164 | ||||
-rw-r--r-- | lib/private/Server.php | 2 |
6 files changed, 182 insertions, 0 deletions
diff --git a/3rdparty b/3rdparty -Subproject 5b8a5fc015968956a000d269561cb5ec9d93187 +Subproject a71bd8af76fdcfad78c865d1c60f6dde6e24f1d 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 @@ <code>$jobList</code> </MoreSpecificImplementedParamType> </file> + <file src="lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php"> + <InvalidArgument> + <code>$image</code> + <code>$image</code> + <code>$image</code> + <code>$image</code> + </InvalidArgument> + <InvalidReturnStatement> + <code>$image</code> + </InvalidReturnStatement> + <InvalidReturnType> + <code>GdImage|false</code> + </InvalidReturnType> + </file> <file src="lib/private/Cache/CappedMemoryCache.php"> <MissingTemplateParam> <code>\ArrayAccess</code> 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 @@ +<?php + +declare(strict_types=1); +/** + * @copyright 2024 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +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<AMetadataEvent> + */ +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); } /** |