Skip to content

Commit 26f0d4d

Browse files
committed
blurhash generation
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
1 parent 11124c7 commit 26f0d4d

4 files changed

Lines changed: 181 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>
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 <maxence@artificial-owl.com>
6+
*
7+
* @author Maxence Lange <maxence@artificial-owl.com>
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) || !str_starts_with($file->getMimetype(), 'image/')) {
72+
return;
73+
}
74+
75+
if ($event instanceof MetadataLiveEvent) {
76+
$event->requestBackgroundJob();
77+
78+
return;
79+
}
80+
81+
try {
82+
$preview = $this->preview->getPreview($file, 256, 256);
83+
$image = imagecreatefromstring($preview->getContent());
84+
} catch (NotFoundException $e) {
85+
// https://github.com/nextcloud/server/blob/9d70fd3e64b60a316a03fb2b237891380c310c58/lib/private/legacy/OC_Image.php#L668
86+
// The preview system can fail on huge picture, in that case we use our own resize image.
87+
$image = $this->resizedImageFromFile($file);
88+
}
89+
90+
if ($image === false) {
91+
return;
92+
}
93+
94+
$metadata = $event->getMetadata();
95+
$metadata->setString('blurhash', $this->generateBlurHash($image));
96+
}
97+
98+
/**
99+
* @param File $file
100+
*
101+
* @return GdImage|false
102+
* @throws GenericFileException
103+
* @throws NotPermittedException
104+
* @throws LockedException
105+
*/
106+
private function resizedImageFromFile(File $file): GdImage|false {
107+
$image = imagecreatefromstring($file->getContent());
108+
if ($image === false) {
109+
return false;
110+
}
111+
112+
$currX = imagesx($image);
113+
$currY = imagesy($image);
114+
115+
if ($currX > $currY) {
116+
$newX = self::RESIZE_BOXSIZE;
117+
$newY = intval($currY * $newX / $currX);
118+
} else {
119+
$newY = self::RESIZE_BOXSIZE;
120+
$newX = intval($currX * $newY / $currY);
121+
}
122+
123+
$newImage = imagescale($image, $newX, $newY);
124+
if (false !== $newImage) {
125+
$image = $newImage;
126+
}
127+
128+
return $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)