| 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"  |
| 24 | using namespace tSystem;  |
| 25 | namespace tImage  |
| 26 | {  |
| 27 |   |
| 28 |   |
| 29 | bool 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 |   |
| 48 | bool 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 |   |
| 173 | bool 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 |   |
| 219 | bool 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 |   |
| 246 | bool 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 |   |
| 268 | bool 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 |   |
| 288 | bool 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 |   |
| 310 | tFrame* tImageWEBP::GetFrame(bool steal)  |
| 311 | {  |
| 312 | if (!IsValid())  |
| 313 | return nullptr;  |
| 314 |   |
| 315 | return steal ? Frames.Remove() : new tFrame( *Frames.First() );  |
| 316 | }  |
| 317 |   |
| 318 |   |
| 319 | bool 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 |   |
| 329 | bool 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 | |