Skip to content

Commit 24952b9

Browse files
authored
Merge pull request #41044 from nextcloud/enh/noid/blurhash
2 parents 7502c19 + 4367a5e commit 24952b9

6 files changed

Lines changed: 183 additions & 1 deletion

File tree

build/psalm-baseline.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2147,6 +2147,20 @@
21472147
<code>$jobList</code>
21482148
</MoreSpecificImplementedParamType>
21492149
</file>
2150+
<file src="lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php">
2151+
<InvalidArgument>
2152+
<code>$image</code>
2153+
<code>$image</code>
2154+
<code>$image</code>
2155+
<code>$image</code>
2156+
</InvalidArgument>
2157+
<InvalidReturnStatement>
2158+
<code>$image</code>
2159+
</InvalidReturnStatement>
2160+
<InvalidReturnType>
2161+
<code>GdImage|false</code>
2162+
</InvalidReturnType>
2163+
</file>
21502164
<file src="lib/private/Cache/CappedMemoryCache.php">
21512165
<MissingTemplateParam>
21522166
<code>\ArrayAccess</code>

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,7 @@
953953
'OC\\BackgroundJob\\QueuedJob' => $baseDir . '/lib/private/BackgroundJob/QueuedJob.php',
954954
'OC\\BackgroundJob\\TimedJob' => $baseDir . '/lib/private/BackgroundJob/TimedJob.php',
955955
'OC\\BinaryFinder' => $baseDir . '/lib/private/BinaryFinder.php',
956+
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => $baseDir . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
956957
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
957958
'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php',
958959
'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
986986
'OC\\BackgroundJob\\QueuedJob' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/QueuedJob.php',
987987
'OC\\BackgroundJob\\TimedJob' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/TimedJob.php',
988988
'OC\\BinaryFinder' => __DIR__ . '/../../..' . '/lib/private/BinaryFinder.php',
989+
'OC\\Blurhash\\Listener\\GenerateBlurhashMetadata' => __DIR__ . '/../../..' . '/lib/private/Blurhash/Listener/GenerateBlurhashMetadata.php',
989990
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
990991
'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php',
991992
'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php',
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright 2024 Maxence Lange <[email protected]>
6+
*
7+
* @author Maxence Lange <[email protected]>
8+
*
9+
* @license GNU AGPL version 3 or any later version
10+
*
11+
* This program is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Affero General Public License as
13+
* published by the Free Software Foundation, either version 3 of the
14+
* License, or (at your option) any later version.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
23+
*
24+
*/
25+
26+
namespace OC\Blurhash\Listener;
27+
28+
use GdImage;
29+
use kornrunner\Blurhash\Blurhash;
30+
use OC\Files\Node\File;
31+
use OCP\EventDispatcher\Event;
32+
use OCP\EventDispatcher\IEventDispatcher;
33+
use OCP\EventDispatcher\IEventListener;
34+
use OCP\Files\GenericFileException;
35+
use OCP\Files\NotFoundException;
36+
use OCP\Files\NotPermittedException;
37+
use OCP\FilesMetadata\AMetadataEvent;
38+
use OCP\FilesMetadata\Event\MetadataBackgroundEvent;
39+
use OCP\FilesMetadata\Event\MetadataLiveEvent;
40+
use OCP\IPreview;
41+
use OCP\Lock\LockedException;
42+
43+
/**
44+
* Generate a Blurhash string as metadata when image file is uploaded/edited.
45+
*
46+
* @template-implements IEventListener<AMetadataEvent>
47+
*/
48+
class GenerateBlurhashMetadata implements IEventListener {
49+
private const RESIZE_BOXSIZE = 300;
50+
51+
private const COMPONENTS_X = 4;
52+
private const COMPONENTS_Y = 3;
53+
54+
public function __construct(
55+
private IPreview $preview,
56+
) {
57+
}
58+
59+
/**
60+
* @throws NotPermittedException
61+
* @throws GenericFileException
62+
* @throws LockedException
63+
*/
64+
public function handle(Event $event): void {
65+
if (!($event instanceof MetadataLiveEvent)
66+
&& !($event instanceof MetadataBackgroundEvent)) {
67+
return;
68+
}
69+
70+
$file = $event->getNode();
71+
if (!($file instanceof File)) {
72+
return;
73+
}
74+
75+
// too heavy to run on the live thread, request a rerun as a background job
76+
if ($event instanceof MetadataLiveEvent) {
77+
$event->requestBackgroundJob();
78+
return;
79+
}
80+
81+
$image = false;
82+
try {
83+
// using preview image to generate the blurhash
84+
$preview = $this->preview->getPreview($file, 256, 256);
85+
$image = imagecreatefromstring($preview->getContent());
86+
} catch (NotFoundException $e) {
87+
// https://github.com/nextcloud/server/blob/9d70fd3e64b60a316a03fb2b237891380c310c58/lib/private/legacy/OC_Image.php#L668
88+
// The preview system can fail on huge picture, in that case we use our own image resizer.
89+
if (str_starts_with($file->getMimetype(), 'image/')) {
90+
$image = $this->resizedImageFromFile($file);
91+
}
92+
}
93+
94+
if ($image === false) {
95+
return;
96+
}
97+
98+
$metadata = $event->getMetadata();
99+
$metadata->setString('blurhash', $this->generateBlurHash($image));
100+
}
101+
102+
/**
103+
* @param File $file
104+
*
105+
* @return GdImage|false
106+
* @throws GenericFileException
107+
* @throws NotPermittedException
108+
* @throws LockedException
109+
*/
110+
private function resizedImageFromFile(File $file): GdImage|false {
111+
$image = imagecreatefromstring($file->getContent());
112+
if ($image === false) {
113+
return false;
114+
}
115+
116+
$currX = imagesx($image);
117+
$currY = imagesy($image);
118+
119+
if ($currX > $currY) {
120+
$newX = self::RESIZE_BOXSIZE;
121+
$newY = intval($currY * $newX / $currX);
122+
} else {
123+
$newY = self::RESIZE_BOXSIZE;
124+
$newX = intval($currX * $newY / $currY);
125+
}
126+
127+
$newImage = imagescale($image, $newX, $newY);
128+
return ($newImage !== false) ? $newImage : $image;
129+
}
130+
131+
/**
132+
* @param GdImage $image
133+
*
134+
* @return string
135+
*/
136+
public function generateBlurHash(GdImage $image): string {
137+
$width = imagesx($image);
138+
$height = imagesy($image);
139+
140+
$pixels = [];
141+
for ($y = 0; $y < $height; ++$y) {
142+
$row = [];
143+
for ($x = 0; $x < $width; ++$x) {
144+
$index = imagecolorat($image, $x, $y);
145+
$colors = imagecolorsforindex($image, $index);
146+
$row[] = [$colors['red'], $colors['green'], $colors['blue']];
147+
}
148+
149+
$pixels[] = $row;
150+
}
151+
152+
return Blurhash::encode($pixels, self::COMPONENTS_X, self::COMPONENTS_Y);
153+
}
154+
155+
/**
156+
* @param IEventDispatcher $eventDispatcher
157+
*
158+
* @return void
159+
*/
160+
public static function loadListeners(IEventDispatcher $eventDispatcher): void {
161+
$eventDispatcher->addServiceListener(MetadataLiveEvent::class, self::class);
162+
$eventDispatcher->addServiceListener(MetadataBackgroundEvent::class, self::class);
163+
}
164+
}

lib/private/Server.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
use OC\Authentication\LoginCredentials\Store;
6969
use OC\Authentication\Token\IProvider;
7070
use OC\Avatar\AvatarManager;
71+
use OC\Blurhash\Listener\GenerateBlurhashMetadata;
7172
use OC\Collaboration\Collaborators\GroupPlugin;
7273
use OC\Collaboration\Collaborators\MailPlugin;
7374
use OC\Collaboration\Collaborators\RemoteGroupPlugin;
@@ -1482,6 +1483,7 @@ private function connectDispatcher(): void {
14821483
$eventDispatcher->addServiceListener(BeforeUserDeletedEvent::class, BeforeUserDeletedListener::class);
14831484

14841485
FilesMetadataManager::loadListeners($eventDispatcher);
1486+
GenerateBlurhashMetadata::loadListeners($eventDispatcher);
14851487
}
14861488

14871489
/**

0 commit comments

Comments
 (0)