Skip to content

Commit 4248784

Browse files
authored
Merge pull request #3202 from balping/master
Add exif support for webp images
2 parents 9fd3b66 + 63ed8fa commit 4248784

File tree

3 files changed

+277
-9
lines changed

3 files changed

+277
-9
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"nextcloud/ocp": "dev-master"
3939
},
4040
"require": {
41-
"hexogen/kdtree": "^0.2.5"
41+
"hexogen/kdtree": "^0.2.5",
42+
"woltlab/webp-exif": "^0.1.1"
4243
},
4344
"extra": {
4445
"bamarni-bin": {

composer.lock

Lines changed: 197 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Listener/ExifMetadataProvider.php

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@
88

99
namespace OCA\Photos\Listener;
1010

11+
use Nelexa\Buffer\Buffer;
12+
use Nelexa\Buffer\ResourceBuffer;
1113
use OCA\Photos\AppInfo\Application;
1214
use OCP\EventDispatcher\Event;
1315
use OCP\EventDispatcher\IEventListener;
1416
use OCP\Files\File;
1517
use OCP\FilesMetadata\Event\MetadataBackgroundEvent;
1618
use OCP\FilesMetadata\Event\MetadataLiveEvent;
1719
use Psr\Log\LoggerInterface;
20+
use WoltLab\WebpExif\Chunk\Chunk;
21+
use WoltLab\WebpExif\Decoder;
22+
use WoltLab\WebpExif\Exception\FileSizeMismatch;
23+
use WoltLab\WebpExif\Exception\NotEnoughData;
24+
use WoltLab\WebpExif\Exception\UnrecognizedFileFormat;
1825

1926
/**
2027
* Extract EXIF, IFD0, and GPS data from a picture file.
@@ -71,7 +78,12 @@ public function handle(Event $event): void {
7178
// This is to trigger this condition: https://github.com/php/php-src/blob/d64aa6f646a7b5e58359dc79479860164239580a/main/streams/streams.c#L710
7279
// But I don't understand yet why 1 as a special meaning.
7380
$oldBufferSize = stream_set_chunk_size($fileDescriptor, 1);
74-
$rawExifData = @exif_read_data($fileDescriptor, 'EXIF, GPS', true);
81+
if ($node->getMimeType() == 'image/webp') {
82+
$rawExifData = $this->getExifFromWebP($fileDescriptor);
83+
} else {
84+
$rawExifData = @exif_read_data($fileDescriptor, 'EXIF, GPS', true);
85+
}
86+
7587
// We then revert the change after having read the exif data.
7688
stream_set_chunk_size($fileDescriptor, $oldBufferSize);
7789
} catch (\Exception $ex) {
@@ -110,6 +122,71 @@ public function handle(Event $event): void {
110122
}
111123
}
112124

125+
/**
126+
* Decodes a WebP image from binary data.
127+
* @author Alexander Ebert
128+
* @copyright 2025 WoltLab GmbH
129+
* @license The MIT License <https://opensource.org/license/mit>
130+
*
131+
* @param $fileDescriptor
132+
* @return array|false|null
133+
* @throws \Nelexa\Buffer\BufferException
134+
*
135+
* @psalm-suppress InternalClass
136+
* @psalm-suppress InternalMethod
137+
*/
138+
private function getExifFromWebP($fileDescriptor): array|false|null {
139+
// override the close() function in order to prevent the file being closed when the buffer object is destructed
140+
$buffer = new class($fileDescriptor) extends ResourceBuffer {
141+
public function close() {
142+
143+
}
144+
};
145+
146+
$buffer->setOrder(Buffer::LITTLE_ENDIAN);
147+
$buffer->setReadOnly(true);
148+
149+
// A RIFF container at its minimum contains the "RIFF" header, a
150+
// uint32LE representing the chunk size, the "WEBP" type and the data
151+
// section. The data section of a WebP at minimum contains one chunk
152+
// (header + uint32LE + data).
153+
//
154+
// The shortest possible WebP image is a simple VP8L container that
155+
// contains only the magic byte, a uint32 for the flags and dimensions,
156+
// and at last a single byte of data. This takes up 26 bytes in total.
157+
$expectedMinimumFileSize = 26;
158+
if ($buffer->size() < $expectedMinimumFileSize) {
159+
throw new NotEnoughData($expectedMinimumFileSize, $buffer->size());
160+
}
161+
162+
$riff = $buffer->getString(4);
163+
$length = $buffer->getUnsignedInt();
164+
$format = $buffer->getString(4);
165+
if ($riff !== 'RIFF' || $format !== 'WEBP') {
166+
throw new UnrecognizedFileFormat();
167+
}
168+
169+
// The length in the header does not include "RIFF" and the length
170+
// itself. It must therefore be exactly 8 bytes shorter than the total
171+
// size.
172+
$actualLength = $buffer->size() - 8;
173+
if ($length !== $actualLength) {
174+
throw new FileSizeMismatch($length, $actualLength);
175+
}
176+
177+
$decoder = new Decoder();
178+
$chunk = null;
179+
do {
180+
$chunk = $decoder->decodeChunk($buffer);
181+
} while ($buffer->hasRemaining() && !($chunk instanceof \WoltLab\WebpExif\Chunk\Exif));
182+
183+
if ($chunk instanceof \WoltLab\WebpExif\Chunk\Exif) {
184+
return $chunk->getParsedExif();
185+
} else {
186+
return false;
187+
}
188+
}
189+
113190
/**
114191
* @param array|string $coordinates
115192
*/

0 commit comments

Comments
 (0)