|
8 | 8 |
|
9 | 9 | namespace OCA\Photos\Listener; |
10 | 10 |
|
| 11 | +use Nelexa\Buffer\Buffer; |
| 12 | +use Nelexa\Buffer\ResourceBuffer; |
11 | 13 | use OCA\Photos\AppInfo\Application; |
12 | 14 | use OCP\EventDispatcher\Event; |
13 | 15 | use OCP\EventDispatcher\IEventListener; |
14 | 16 | use OCP\Files\File; |
15 | 17 | use OCP\FilesMetadata\Event\MetadataBackgroundEvent; |
16 | 18 | use OCP\FilesMetadata\Event\MetadataLiveEvent; |
17 | 19 | 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; |
18 | 25 |
|
19 | 26 | /** |
20 | 27 | * Extract EXIF, IFD0, and GPS data from a picture file. |
@@ -71,7 +78,12 @@ public function handle(Event $event): void { |
71 | 78 | // This is to trigger this condition: https://github.com/php/php-src/blob/d64aa6f646a7b5e58359dc79479860164239580a/main/streams/streams.c#L710 |
72 | 79 | // But I don't understand yet why 1 as a special meaning. |
73 | 80 | $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 | + |
75 | 87 | // We then revert the change after having read the exif data. |
76 | 88 | stream_set_chunk_size($fileDescriptor, $oldBufferSize); |
77 | 89 | } catch (\Exception $ex) { |
@@ -110,6 +122,71 @@ public function handle(Event $event): void { |
110 | 122 | } |
111 | 123 | } |
112 | 124 |
|
| 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 | + |
113 | 190 | /** |
114 | 191 | * @param array|string $coordinates |
115 | 192 | */ |
|
0 commit comments