1313namespace Contao \Image \Metadata ;
1414
1515use Contao \Image \Exception \InvalidImageContainerException ;
16+ use Contao \Image \Exception \RuntimeException ;
1617
1718class WebpContainer extends AbstractContainer
1819{
@@ -24,34 +25,97 @@ public function getMagicBytes(): string
2425 public function apply ($ inputStream , $ outputStream , ImageMetadata $ metadata , array $ preserveKeysByFormat ): void
2526 {
2627 $ head = fread ($ inputStream , 12 );
27- $ size = unpack ('V ' , substr ($ head , 4 , 4 ))[1 ];
28+ $ outputSize = $ size = unpack ('V ' , substr ($ head , 4 , 4 ))[1 ];
2829
2930 if ($ size % 2 || !str_starts_with ($ head , 'RIFF ' ) || 'WEBP ' !== substr ($ head , 8 )) {
3031 throw new InvalidImageContainerException ('Invalid WEBP head ' );
3132 }
3233
33- $ xmpChunk = '' ;
34+ $ vp8xMetaFlags = 0 ;
3435 $ exifChunk = '' ;
36+ $ xmpChunk = '' ;
37+
38+ if ($ exif = $ this ->metadataReaderWriter ->serializeFormat (ExifFormat::NAME , $ metadata , $ preserveKeysByFormat [ExifFormat::NAME ] ?? [])) {
39+ $ exifChunk = $ this ->buildChunk ('EXIF ' , $ exif );
40+ // Exif metadata (E)
41+ $ vp8xMetaFlags |= 0b00001000 ;
42+ $ outputSize += \strlen ($ exifChunk );
43+ }
3544
3645 if ($ xmp = $ this ->metadataReaderWriter ->serializeFormat (XmpFormat::NAME , $ metadata , $ preserveKeysByFormat [XmpFormat::NAME ] ?? [])) {
3746 $ xmpChunk = $ this ->buildChunk ('XMP ' , $ xmp );
47+ // XMP metadata (X)
48+ $ vp8xMetaFlags |= 0b00000100 ;
49+ $ outputSize += \strlen ($ xmpChunk );
3850 }
3951
40- if ($ exif = $ this ->metadataReaderWriter ->serializeFormat (ExifFormat::NAME , $ metadata , $ preserveKeysByFormat [ExifFormat::NAME ] ?? [])) {
41- $ exifChunk = $ this ->buildChunk ('EXIF ' , $ exif );
52+ // Keep the original file if there are no metadata chunks to add
53+ if (!$ exifChunk && !$ xmpChunk ) {
54+ fwrite ($ outputStream , $ head );
55+ stream_copy_to_stream ($ inputStream , $ outputStream );
56+
57+ return ;
4258 }
4359
44- // Update file size field
45- $ head = pack ('A4VA4 ' , 'RIFF ' , $ size + \strlen ($ xmpChunk ) + \strlen ($ exifChunk ), 'WEBP ' );
60+ $ size -= 4 ;
61+ $ firstChunk = true ;
62+
63+ while ($ size >= 8 ) {
64+ $ chunkFourcc = fread ($ inputStream , 4 );
65+ $ chunkSize = unpack ('V ' , fread ($ inputStream , 4 ))[1 ];
66+ $ size -= 8 + $ chunkSize ;
67+
68+ if ($ firstChunk ) {
69+ $ firstChunk = false ;
70+
71+ if ('VP8X ' !== $ chunkFourcc ) {
72+ $ outputSize += 18 ;
73+ }
74+
75+ fwrite ($ outputStream , pack ('A4VA4 ' , 'RIFF ' , $ outputSize , 'WEBP ' ));
76+
77+ if ('VP8 ' === $ chunkFourcc ) {
78+ $ vp8Header = fread ($ inputStream , 10 );
79+ fwrite ($ outputStream , $ this ->buildVp8xChunkFromVp8 ($ vp8Header , $ vp8xMetaFlags ));
80+ fwrite ($ outputStream , pack ('A4V ' , $ chunkFourcc , $ chunkSize ));
81+ fwrite ($ outputStream , $ vp8Header );
82+ stream_copy_to_stream ($ inputStream , $ outputStream , $ chunkSize - 10 );
83+ } elseif ('VP8L ' === $ chunkFourcc ) {
84+ $ vp8lHeader = fread ($ inputStream , 5 );
85+ fwrite ($ outputStream , $ this ->buildVp8xChunkFromVp8l ($ vp8lHeader , $ vp8xMetaFlags ));
86+ fwrite ($ outputStream , pack ('A4V ' , $ chunkFourcc , $ chunkSize ));
87+ fwrite ($ outputStream , $ vp8lHeader );
88+ stream_copy_to_stream ($ inputStream , $ outputStream , $ chunkSize - 5 );
89+ } elseif ('VP8X ' === $ chunkFourcc ) {
90+ fwrite ($ outputStream , pack ('A4V ' , $ chunkFourcc , $ chunkSize ));
91+ $ vp8xFlags = \ord (fread ($ inputStream , 1 )) | $ vp8xMetaFlags ;
92+ fwrite ($ outputStream , \chr ($ vp8xFlags ));
93+ stream_copy_to_stream ($ inputStream , $ outputStream , $ chunkSize - 1 );
94+ } else {
95+ throw new InvalidImageContainerException ('Invalid WEBP chunk at start position ' );
96+ }
97+ } else {
98+ if (\in_array ($ chunkFourcc , ['EXIF ' , 'XMP ' , "XMP \x00" ], true )) {
99+ throw new RuntimeException ('Overwriting existing metadata chunks in WEBP is not supported ' );
100+ }
46101
47- fwrite ($ outputStream , $ head );
102+ fwrite ($ outputStream , pack ('A4V ' , $ chunkFourcc , $ chunkSize ));
103+ stream_copy_to_stream ($ inputStream , $ outputStream , $ chunkSize );
104+ }
48105
49- stream_copy_to_stream ($ inputStream , $ outputStream , $ size - 4 );
106+ // RIFF chunks are padded to an even number
107+ if ($ chunkSize % 2 ) {
108+ stream_copy_to_stream ($ inputStream , $ outputStream , 1 );
109+ --$ size ;
110+ }
111+ }
50112
51- fwrite ($ outputStream , $ xmpChunk );
52- fwrite ($ outputStream , $ exifChunk );
113+ if (0 !== $ size ) {
114+ throw new InvalidImageContainerException ('Invalid WEBP chunks ' );
115+ }
53116
54- stream_copy_to_stream ($ inputStream , $ outputStream );
117+ fwrite ($ outputStream , $ exifChunk );
118+ fwrite ($ outputStream , $ xmpChunk );
55119 }
56120
57121 public function parse ($ stream ): array
@@ -77,8 +141,19 @@ public function parse($stream): array
77141 $ size = unpack ('V ' , substr ($ marker , 4 , 4 ))[1 ];
78142
79143 if ('EXIF ' === $ type ) {
80- $ metadata [ExifFormat::NAME ] = $ this ->parseFormat (ExifFormat::NAME , fread ($ stream , $ size ));
81- } elseif ('XMP ' === $ type ) {
144+ $ exif = fread ($ stream , $ size );
145+
146+ // Older ImageMagick versions incorrectly included the JPEG APP1
147+ // Exif identifier string 45 78 69 66 00 00
148+ // see https://github.com/ImageMagick/ImageMagick/issues/3140
149+ if (str_starts_with ($ exif , "Exif \x00\x00" )) {
150+ $ exif = substr ($ exif , 6 );
151+ }
152+
153+ $ metadata [ExifFormat::NAME ] = $ this ->parseFormat (ExifFormat::NAME , $ exif );
154+ } elseif ('XMP ' === $ type || "XMP \x00" === $ type ) {
155+ // Older ImageMagick versions incorrectly used XMP followed by a null byte: 58 4D 50 00
156+ // see https://github.com/ImageMagick/ImageMagick/commit/da99bea66a4ad0bcf6149170eda81e6dcc229af0
82157 $ metadata [XmpFormat::NAME ] = $ this ->parseFormat (XmpFormat::NAME , fread ($ stream , $ size ));
83158 } else {
84159 // Skip to the next chunk
@@ -94,6 +169,75 @@ public function parse($stream): array
94169 return $ metadata ;
95170 }
96171
172+ private function buildVp8xChunkFromVp8 (string $ header , int $ vp8xMetaFlags ): string
173+ {
174+ if (\ord ($ header [0 ]) & 0b00000001 ) {
175+ throw new InvalidImageContainerException ('Expected VP8 keyframe ' );
176+ }
177+
178+ if ("\x9D\x01\x2A" !== substr ($ header , 3 , 3 )) {
179+ throw new InvalidImageContainerException ('Invalid VP8 keyframe sync code ' );
180+ }
181+
182+ $ width = unpack ('v ' , substr ($ header , 6 , 2 ))[1 ] & 0b0011111111111111 ;
183+ $ height = unpack ('v ' , substr ($ header , 8 , 2 ))[1 ] & 0b0011111111111111 ;
184+
185+ return $ this ->buildVp8xChunk (
186+ 0b00000000 | $ vp8xMetaFlags ,
187+ $ width ,
188+ $ height ,
189+ );
190+ }
191+
192+ private function buildVp8xChunkFromVp8l (string $ header , int $ vp8xMetaFlags ): string
193+ {
194+ if ("\x2F" !== $ header [0 ]) {
195+ throw new InvalidImageContainerException ('Invalid VP8L signature ' );
196+ }
197+
198+ $ data = unpack ('V ' , substr ($ header , 1 , 4 ))[1 ];
199+
200+ if (0 !== $ data >> 29 ) {
201+ throw new InvalidImageContainerException ('Invalid VP8L version ' );
202+ }
203+
204+ $ width = ($ data & 0b0011111111111111 ) + 1 ;
205+ $ height = ($ data >> 14 & 0b0011111111111111 ) + 1 ;
206+
207+ // Alpha (L)
208+ if ($ data >> 28 & 0b01 ) {
209+ $ vp8xMetaFlags |= 0b00010000 ;
210+ }
211+
212+ return $ this ->buildVp8xChunk (
213+ 0b00000000 | $ vp8xMetaFlags ,
214+ $ width ,
215+ $ height ,
216+ );
217+ }
218+
219+ private function buildVp8xChunk (int $ vp8xFlags , int $ width , int $ height ): string
220+ {
221+ --$ width ;
222+ --$ height ;
223+
224+ $ vp8x = pack (
225+ 'C* ' ,
226+ $ vp8xFlags , // Bit flags: |R|R|I|L|E|X|A|R|
227+ 0 ,
228+ 0 ,
229+ 0 ,
230+ $ width & 0xFF ,
231+ $ width >> 8 & 0xFF ,
232+ $ width >> 16 & 0xFF ,
233+ $ height & 0xFF ,
234+ $ height >> 8 & 0xFF ,
235+ $ height >> 16 & 0xFF
236+ );
237+
238+ return $ this ->buildChunk ('VP8X ' , $ vp8x );
239+ }
240+
97241 private function buildChunk ($ type , $ content ): string
98242 {
99243 $ size = \strlen ($ content );
0 commit comments