1// tImageWEBP.cpp 
2// 
3// This knows how to load/save WebPs. It knows the details of the webp file format and loads the data into multiple 
4// tPixel arrays, one for each frame (WebPs may be animated). These arrays may be 'stolen' by tPictures. 
5// 
6// Copyright (c) 2020-2024 Tristan Grimmer. 
7// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
8// granted, provided that the above copyright notice and this permission notice appear in all copies. 
9// 
10// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
11// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
12// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
13// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
14// PERFORMANCE OF THIS SOFTWARE. 
15 
16#include <Foundation/tStandard.h> 
17#include <Foundation/tString.h> 
18#include <System/tFile.h> 
19#include "Image/tImageWEBP.h" 
20#include "Image/tPicture.h" 
21#include "WebP/include/mux.h" 
22#include "WebP/include/demux.h" 
23#include "WebP/include/encode.h" 
24using namespace tSystem
25namespace tImage 
26
27 
28 
29bool tImageWEBP::Load(const tString& webpFile
30
31 Clear(); 
32 
33 if (tSystem::tGetFileType(file: webpFile) != tSystem::tFileType::WEBP
34 return false
35 
36 if (!tFileExists(file: webpFile)) 
37 return false
38 
39 int numBytes = 0
40 uint8* webpFileInMemory = tLoadFile(file: webpFile, buffer: nullptr, fileSize: &numBytes); 
41 bool success = Load(webpFileInMemory, numBytes); 
42 delete[] webpFileInMemory
43 
44 return success
45
46 
47 
48bool tImageWEBP::Load(const uint8* webpFileInMemory, int numBytes
49
50 Clear(); 
51 if ((numBytes <= 0) || !webpFileInMemory
52 return false
53 
54 // Now we load and populate the frames. 
55 WebPData webpData
56 webpData.bytes = webpFileInMemory
57 webpData.size = numBytes
58 
59 WebPDemuxer* demux = WebPDemux(data: &webpData); 
60 uint32 canvasWidth = WebPDemuxGetI(dmux: demux, feature: WEBP_FF_CANVAS_WIDTH); 
61 uint32 canvasHeight = WebPDemuxGetI(dmux: demux, feature: WEBP_FF_CANVAS_HEIGHT); 
62 uint32 flags = WebPDemuxGetI(dmux: demux, feature: WEBP_FF_FORMAT_FLAGS); 
63 uint32 numFrames = WebPDemuxGetI(dmux: demux, feature: WEBP_FF_FRAME_COUNT); 
64  
65 if ((canvasWidth <= 0) || (canvasHeight <= 0) || (numFrames <= 0)) 
66
67 WebPDemuxDelete(dmux: demux); 
68 return false
69
70 
71 bool animated = (numFrames > 1); 
72 if (animated
73
74 // Bits 00 to 07: Alpha. Bits 08 to 15: Red. Bits 16 to 23: Green. Bits 24 to 31: Blue. 
75 uint32 col = WebPDemuxGetI(dmux: demux, feature: WEBP_FF_BACKGROUND_COLOR); 
76 BackgroundColour.R = (col >> 8 ) & 0xFF
77 BackgroundColour.G = (col >> 16) & 0xFF
78 BackgroundColour.B = (col >> 24) & 0xFF
79 BackgroundColour.A = (col >> 0 ) & 0xFF
80
81 
82 // We start by creatng the initial canvas in memory set to the background colour. 
83 // This is our 'working area' where we put the decoded frames. See CopyRegion below. 
84 tPixel4b* canvas = new tPixel4b[canvasWidth*canvasHeight]; 
85 for (int p = 0; p < canvasWidth*canvasHeight; p++) 
86 canvas[p] = tColour4b::transparent
87 
88 // Iterate over all frames. 
89 tPixelFormat srcFormat = tPixelFormat::R8G8B8
90 WebPIterator iter
91 if (WebPDemuxGetFrame(dmux: demux, frame_number: 1, iter: &iter)) 
92
93 do 
94
95 WebPDecoderConfig config
96 WebPInitDecoderConfig(config: &config); 
97 
98 config.output.colorspace = MODE_RGBA
99 config.output.is_external_memory = 0
100 config.options.flip = 1
101 int result = WebPDecode(data: iter.fragment.bytes, data_size: iter.fragment.size, config: &config); 
102 if (result != VP8_STATUS_OK
103 continue
104 
105 // What do we do with the canvas? If not animated it's not going to matter. From WebP source: 
106 // Dispose method (animation only). Indicates how the area used by the current 
107 // frame is to be treated before rendering the next frame on the canvas. 
108 bool dispose = (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND); 
109 if (dispose
110
111 for (int p = 0; p < canvasWidth*canvasHeight; p++) 
112 canvas[p] = tColour4b::transparent
113
114 
115 int fragWidth = config.output.width
116 int fragHeight = config.output.height
117 if ((fragWidth <= 0) || (fragHeight <= 0)) 
118 continue
119 
120 // All frames in tacent are canvas-sized. 
121 tFrame* newFrame = new tFrame
122 newFrame->PixelFormatSrc = iter.has_alpha ? tPixelFormat::R8G8B8A8 : tPixelFormat::R8G8B8
123 
124 // If any frame has alpha we set the main src format to have alpha. 
125 if (iter.has_alpha
126 srcFormat = tPixelFormat::R8G8B8A8
127 newFrame->Width = canvasWidth
128 newFrame->Height = canvasHeight
129 newFrame->Pixels = new tPixel4b[newFrame->Width * newFrame->Height]; 
130 newFrame->Duration = float(iter.duration) / 1000.0f
131 
132 // Next we need to grab the decoded pixels (which may be a sub-region of the canvas) and stick them in the canvas. 
133 // How we stick the pixels in depends on the anim-blend. If not animated, force simple overwrite. 
134 bool blend = false
135 if (iter.blend_method == WEBP_MUX_BLEND
136 blend = true
137 
138 // The flip flag doesn't fix the offsets for WebP so we need the canvasHeight - iter.y_offset - frameHeight. 
139 bool copied = CopyRegion(dst: canvas, dstW: canvasWidth, dstH: canvasHeight, src: (tPixel4b*)config.output.u.RGBA.rgba, srcW: fragWidth, srcH: fragHeight, offsetX: iter.x_offset, offsetY: canvasHeight - iter.y_offset - fragHeight, blend); 
140 if (!copied
141
142 delete newFrame
143 continue
144
145 
146 // Now the canvas is updated. Put the canvas in the new frame. 
147 tStd::tMemcpy(dest: newFrame->Pixels, src: canvas, numBytes: canvasWidth * canvasHeight * sizeof(tPixel4b)); 
148 
149 WebPFreeDecBuffer(buffer: &config.output); 
150 Frames.Append(item: newFrame); 
151
152 while (WebPDemuxNextFrame(iter: &iter)); 
153 
154 WebPDemuxReleaseIterator(iter: &iter); 
155
156 
157 delete[] canvas
158 WebPDemuxDelete(dmux: demux); 
159 if (Frames.GetNumItems() <= 0
160 return false
161 
162 PixelFormatSrc = srcFormat
163 PixelFormat = tPixelFormat::R8G8B8A8
164 
165 // WEBP files are assumed to be in sRGB. 
166 ColourProfileSrc = tColourProfile::sRGB
167 ColourProfile = tColourProfile::sRGB
168 
169 return true
170
171 
172 
173bool tImageWEBP::CopyRegion(tPixel4b* dst, int dstW, int dstH, tPixel4b* src, int srcW, int srcH, int offsetX, int offsetY, bool blend
174
175 // Do nothing if anything wrong. 
176 if (!dst || !src || (dstW*dstH <= 0) || (srcW*srcH <= 0)) 
177 return false
178 
179 // Also check that the entire src region fits inside the dst canvas. 
180 if ((offsetX < 0) || (offsetX >= dstW) || (offsetY < 0) || (offsetY >= dstH)) 
181 return false
182 if ((offsetX+srcW > dstW) || (offsetY+srcH > dstH)) 
183 return false
184 
185 // for each row of the src put it in dest. 
186 for (int sy = 0; sy < srcH; sy++) 
187
188 int rowWidth = srcW
189 tPixel4b* dstRow = dst + ((offsetY+sy)*dstW + offsetX); 
190 tPixel4b* srcRow = src + (sy*srcW); 
191 for (int sx = 0; sx < rowWidth; sx++) 
192
193 if (blend
194
195 tColour4f scol(srcRow[sx]); 
196 tColour4f dcol(dstRow[sx]); 
197 float alpha = scol.A
198 float oneMinusAlpha = 1.0f - alpha
199 
200 tColour4f pixelCol = scol
201 pixelCol.R = pixelCol.R*alpha + dcol.R*oneMinusAlpha
202 pixelCol.G = pixelCol.G*alpha + dcol.G*oneMinusAlpha
203 pixelCol.B = pixelCol.B*alpha + dcol.B*oneMinusAlpha
204 pixelCol.A = alpha > 0.0f ? alpha : dcol.A
205 
206 dstRow[sx].Set(pixelCol); 
207
208 else 
209
210 dstRow[sx] = srcRow[sx]; 
211
212
213
214 
215 return true
216
217 
218 
219bool tImageWEBP::Set(tList<tFrame>& srcFrames, bool stealFrames
220
221 Clear(); 
222 if (srcFrames.GetNumItems() <= 0
223 return false
224 
225 // This assumes the srcFrames all have the same format. 
226 PixelFormatSrc = srcFrames.Head()->PixelFormatSrc
227 PixelFormat = tPixelFormat::R8G8B8A8
228 ColourProfileSrc = tColourProfile::sRGB; // We assume srcFrames must be sRGB. 
229 ColourProfile = tColourProfile::sRGB
230 
231 if (stealFrames
232
233 while (tFrame* frame = srcFrames.Remove()) 
234 Frames.Append(item: frame); 
235
236 else 
237
238 for (tFrame* frame = srcFrames.Head(); frame; frame = frame->Next()) 
239 Frames.Append(item: new tFrame(*frame)); 
240
241 
242 return true
243
244 
245 
246bool tImageWEBP::Set(tPixel4b* pixels, int width, int height, bool steal
247
248 Clear(); 
249 if (!pixels || (width <= 0) || (height <= 0)) 
250 return false
251 
252 tFrame* frame = new tFrame(); 
253 if (steal
254 frame->StealFrom(src: pixels, width, height); 
255 else 
256 frame->Set(srcPixels: pixels, width, height); 
257 Frames.Append(item: frame); 
258 
259 PixelFormatSrc = tPixelFormat::R8G8B8A8
260 PixelFormat = tPixelFormat::R8G8B8A8
261 ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB. 
262 ColourProfile = tColourProfile::sRGB
263 
264 return true
265
266 
267 
268bool tImageWEBP::Set(tFrame* frame, bool steal
269
270 Clear(); 
271 if (!frame || !frame->IsValid()) 
272 return false
273 
274 PixelFormatSrc = frame->PixelFormatSrc
275 PixelFormat = tPixelFormat::R8G8B8A8
276 ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB. 
277 ColourProfile = tColourProfile::sRGB
278 
279 if (steal
280 Frames.Append(item: frame); 
281 else 
282 Frames.Append(item: new tFrame(*frame)); 
283 
284 return true
285
286 
287 
288bool tImageWEBP::Set(tPicture& picture, bool steal
289
290 Clear(); 
291 if (!picture.IsValid()) 
292 return false
293 
294 PixelFormatSrc = picture.PixelFormatSrc
295 PixelFormat = tPixelFormat::R8G8B8A8
296 // We don't know colour profile of tPicture. 
297 
298 // This is worth some explanation. If steal is true the picture becomes invalid and the 
299 // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the 
300 // 'set' call will memcpy them out... which makes sure the picture is still valid after and 
301 // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must 
302 // succeed if picture was valid. 
303 tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels(); 
304 bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal); 
305 tAssert(success); 
306 return true
307
308 
309 
310tFrame* tImageWEBP::GetFrame(bool steal
311
312 if (!IsValid()) 
313 return nullptr
314 
315 return steal ? Frames.Remove() : new tFrame( *Frames.First() ); 
316
317 
318 
319bool tImageWEBP::Save(const tString& webpFile, bool lossy, float qualityCompstr, int overrideFrameDuration) const 
320
321 SaveParams params
322 params.Lossy = lossy
323 params.QualityCompstr = qualityCompstr
324 params.OverrideFrameDuration = overrideFrameDuration
325 return Save(webpFile, params); 
326
327 
328 
329bool tImageWEBP::Save(const tString& webpFile, const SaveParams& params) const 
330
331 if (!IsValid()) 
332 return false
333 
334 if (tSystem::tGetFileType(file: webpFile) != tSystem::tFileType::WEBP
335 return false
336 
337 WebPConfig config
338 int success = WebPConfigPreset(config: &config, preset: WEBP_PRESET_PHOTO, quality: tMath::tClamp(val: params.QualityCompstr, min: 0.0f, max: 100.0f)); 
339 if (!success
340 return false
341 
342 // config.method is the quality/speed trade-off (0=fast, 6=slower-better). 
343 config.lossless = params.Lossy ? 0 : 1
344 
345 // Additional config parameters in lossy mode. 
346 if (params.Lossy
347
348 config.sns_strength = 90
349 config.filter_sharpness = 6
350 config.alpha_quality = 90
351
352 
353 success = WebPValidateConfig(config: &config); 
354 if (!success
355 return false
356 
357 // Setup the muxer so we can put more than one image in a file. 
358 WebPMux* mux = WebPMuxNew(); 
359 
360 WebPMuxAnimParams animParams
361 animParams.bgcolor = 0x00000000
362 animParams.loop_count = 0
363 WebPMuxSetAnimationParams(mux, params: &animParams); 
364 
365 bool animated = Frames.GetNumItems() > 1
366 for (tFrame* frame = Frames.First(); frame; frame = frame->Next()) 
367
368 WebPPicture pic
369 success = WebPPictureInit(picture: &pic); 
370 if (!success
371 continue
372 
373 // This is inefficient here. I'm reversing the rows so I can use the simple 
374 // WebPPictureImportRGBA. But this is a waste of memory and time. 
375 tFrame normFrame(*frame); 
376 normFrame.ReverseRows(); 
377 
378 // Let's get one frame going first. 
379 //tFrame* frame = Frames.Head(); 
380 pic.width = normFrame.Width
381 pic.height = normFrame.Height
382 success = WebPPictureImportRGBA(picture: &pic, rgba: (uint8*)normFrame.Pixels, rgba_stride: normFrame.Width*sizeof(tPixel4b)); 
383 if (!success
384 continue
385 
386 WebPMemoryWriter writer
387 WebPMemoryWriterInit(writer: &writer); 
388 pic.writer = WebPMemoryWrite
389 pic.custom_ptr = &writer
390 
391 success = WebPEncode(config: &config, picture: &pic); 
392 if (!success
393 continue
394 
395 // Done with pic. 
396 WebPPictureFree(picture: &pic); 
397 
398 WebPData webpData;  
399 webpData.bytes = writer.mem
400 webpData.size = writer.size
401 
402 int copyData = 1
403 if (animated
404
405 WebPMuxFrameInfo frameInfo
406 tStd::tMemset(dest: &frameInfo, val: 0, numBytes: sizeof(WebPMuxFrameInfo)); 
407 
408 // Frame duration is an integer in milliseconds. 
409 frameInfo.duration = (params.OverrideFrameDuration >= 0) ? params.OverrideFrameDuration : int(frame->Duration * 1000.0f); 
410 frameInfo.bitstream = webpData
411 frameInfo.id = WEBP_CHUNK_ANMF
412 frameInfo.blend_method = WEBP_MUX_NO_BLEND
413 frameInfo.dispose_method = WEBP_MUX_DISPOSE_BACKGROUND
414 WebPMuxPushFrame(mux, frame: &frameInfo, copy_data: copyData); 
415
416 else 
417
418 // One frame. Not animated. 
419 WebPMuxSetImage(mux, bitstream: &webpData, copy_data: copyData); 
420
421 
422 WebPMemoryWriterClear(writer: &writer); 
423
424 
425 // Get data from mux in WebP RIFF format. 
426 WebPData assembledData
427 tStd::tMemset(dest: &assembledData, val: 0, numBytes: sizeof(WebPData)); 
428 WebPMuxAssemble(mux, assembled_data: &assembledData); 
429 WebPMuxDelete(mux); 
430 
431 bool ok = tCreateFile(file: webpFile, data: (uint8*)assembledData.bytes, length: assembledData.size); 
432 WebPDataClear(webp_data: &assembledData); 
433 return ok
434
435 
436 
437
438