| 1 | // tImagePNG.cpp  |
| 2 | //  |
| 3 | // This class knows how to load and save PNG files. It does zero processing of image data. It knows the details of the  |
| 4 | // png file format and loads the data into a tPixel array. These tPixels may be 'stolen' by the tPicture's constructor  |
| 5 | // if a png file is specified. After the array is stolen the tImagePNG is invalid. This is purely for performance.  |
| 6 | //  |
| 7 | // Copyright (c) 2020, 2022-2024 Tristan Grimmer.  |
| 8 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 9 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 10 | //  |
| 11 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 12 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 13 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 14 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 15 | // PERFORMANCE OF THIS SOFTWARE.  |
| 16 | //  |
| 17 | // The loading and saving code in here is roughly based on the example code from the LibPNG and SPNG libraries. The  |
| 18 | // licences may be found in Licence_LibPNG.txt and Licence_LibSPNG.txt.  |
| 19 |   |
| 20 | #include <Foundation/tArray.h>  |
| 21 | #include <System/tFile.h>  |
| 22 |   |
| 23 | // This define chooses between using LibPNG (the original png library) or the slightly cleaner/newer LibSPNG. While the  |
| 24 | // interface for SPNG isn't that much different, I did notice SPNG loads 16-bpc PNG files without multiplying the alpha  |
| 25 | // channel into the colours -- this is the desired behaviour and so SPNG is enabled. Note that apngasm and apngdis  |
| 26 | // depend on LibPNG, so we are keeping LibPNG around until SPNG supports animated PNGs. This is apparently work in  |
| 27 | // progress as of 2024.02.06.  |
| 28 | #define USE_SPNG_LIBRARY  |
| 29 | #ifdef USE_SPNG_LIBRARY  |
| 30 | #include "spng.h"  |
| 31 | #else  |
| 32 | #include "png.h"  |
| 33 | #endif  |
| 34 |   |
| 35 | #include "Image/tImagePNG.h"  |
| 36 | #include "Image/tImageJPG.h" // Because some jpg/jfif files have a png extension in the wild. Scary but true.  |
| 37 | #include "Image/tPicture.h"  |
| 38 | using namespace tSystem;  |
| 39 | namespace tImage  |
| 40 | {  |
| 41 |   |
| 42 |   |
| 43 | bool tImagePNG::Load(const tString& pngFile, const LoadParams& params)  |
| 44 | {  |
| 45 | Clear();  |
| 46 |   |
| 47 | if (tSystem::tGetFileType(file: pngFile) != tSystem::tFileType::PNG)  |
| 48 | return false;  |
| 49 |   |
| 50 | if (!tFileExists(file: pngFile))  |
| 51 | return false;  |
| 52 |   |
| 53 | int numBytes = 0;  |
| 54 | uint8* pngFileInMemory = tLoadFile(file: pngFile, buffer: nullptr, fileSize: &numBytes);  |
| 55 | bool success = Load(pngFileInMemory, numBytes, params);  |
| 56 | delete[] pngFileInMemory;  |
| 57 |   |
| 58 | return success;  |
| 59 | }  |
| 60 |   |
| 61 |   |
| 62 | #ifndef USE_SPNG_LIBRARY  |
| 63 | bool tImagePNG::Load(const uint8* pngFileInMemory, int numBytes, const LoadParams& paramsIn)  |
| 64 | {  |
| 65 | Clear();  |
| 66 | if ((numBytes <= 0) || !pngFileInMemory)  |
| 67 | return false;  |
| 68 |   |
| 69 | LoadParams params(paramsIn);   |
| 70 | png_image pngImage;  |
| 71 | tStd::tMemset(&pngImage, 0, sizeof(pngImage));  |
| 72 | pngImage.version = PNG_IMAGE_VERSION;  |
| 73 |   |
| 74 | int successCode = png_image_begin_read_from_memory(&pngImage, pngFileInMemory, numBytes);  |
| 75 | if (!successCode)  |
| 76 | {  |
| 77 | png_image_free(&pngImage);  |
| 78 | if ((params.Flags & LoadFlag_AllowJPG))  |
| 79 | {  |
| 80 | tImageJPG jpg;  |
| 81 | bool success = jpg.Load(pngFileInMemory, numBytes);  |
| 82 | if (!success)  |
| 83 | return false;  |
| 84 |   |
| 85 | PixelFormatSrc = tPixelFormat::R8G8B8;  |
| 86 | PixelFormat = PixelFormatSrc;  |
| 87 | ColourProfileSrc = tColourProfile::sRGB;  |
| 88 | ColourProfile = ColourProfileSrc;  |
| 89 | Width = jpg.GetWidth();  |
| 90 | Height = jpg.GetHeight();  |
| 91 | Pixels8 = jpg.StealPixels();  |
| 92 | return true;  |
| 93 | }  |
| 94 |   |
| 95 | return false;  |
| 96 | }  |
| 97 |   |
| 98 | // This should only return 1 or 2. If 2 it means the data is in lRGB space (PNG_FORMAT_FLAG_LINEAR).  |
| 99 | int bytesPerComponent = PNG_IMAGE_SAMPLE_COMPONENT_SIZE(pngImage.format);  |
| 100 | if (bytesPerComponent == 2)  |
| 101 | PixelFormatSrc = (pngImage.format & PNG_FORMAT_FLAG_ALPHA) ? tPixelFormat::R16G16B16A16 : tPixelFormat::R16G16B16;  |
| 102 | else  |
| 103 | PixelFormatSrc = (pngImage.format & PNG_FORMAT_FLAG_ALPHA) ? tPixelFormat::R8G8B8A8 : tPixelFormat::R8G8B8;  |
| 104 | PixelFormat = PixelFormatSrc;  |
| 105 | ColourProfileSrc = (bytesPerComponent == 2) ? tColourProfile::lRGB : tColourProfile::sRGB;  |
| 106 | ColourProfile = ColourProfileSrc;  |
| 107 |   |
| 108 | // Are we being asked to do auto-gamma-compression?  |
| 109 | if (params.Flags & LoadFlag_AutoGamma)  |
| 110 | {  |
| 111 | // Clear all related flags.  |
| 112 | params.Flags &= ~(LoadFlag_AutoGamma | LoadFlag_SRGBCompression | LoadFlag_GammaCompression);  |
| 113 | if (ColourProfileSrc == tColourProfile::lRGB)  |
| 114 | params.Flags |= LoadFlag_SRGBCompression;  |
| 115 | }  |
| 116 |   |
| 117 | // We need to modify the format to specify what to decode to. If bytesPerComponent is 1 or we are forcing 8bpc we  |
| 118 | // decode into an 8-bpc buffer -- otherwise we decode into a 16-bpc buffer keeping the additional precision.  |
| 119 | pngImage.format = (bytesPerComponent == 1) ? PNG_FORMAT_RGBA : PNG_FORMAT_LINEAR_RGB_ALPHA;  |
| 120 | Width = pngImage.width;  |
| 121 | Height = pngImage.height;  |
| 122 |   |
| 123 | int numPixels = Width * Height;  |
| 124 | int destBytesPC = (pngImage.format == PNG_FORMAT_RGBA) ? 1 : 2;  |
| 125 | int rawPixelsSize = numPixels * 4 * destBytesPC;  |
| 126 | uint8* rawPixels = new uint8[rawPixelsSize];  |
| 127 | successCode = png_image_finish_read(&pngImage, nullptr, rawPixels, 0, nullptr);  |
| 128 | if (!successCode)  |
| 129 | {  |
| 130 | png_image_free(&pngImage);  |
| 131 | delete[] rawPixels;  |
| 132 | Clear();  |
| 133 | return false;  |
| 134 | }  |
| 135 |   |
| 136 | // Reverse rows as we copy into our final buffer.  |
| 137 | if (pngImage.format == PNG_FORMAT_RGBA)  |
| 138 | {  |
| 139 | Pixels8 = new tPixel4b[numPixels];  |
| 140 | int bytesPerRow = Width*sizeof(tPixel4b);  |
| 141 | for (int y = Height-1; y >= 0; y--)  |
| 142 | tStd::tMemcpy((uint8*)Pixels8 + ((Height-1)-y)*bytesPerRow, rawPixels + y*bytesPerRow, bytesPerRow);  |
| 143 |   |
| 144 | // For no reversal we would just do this:  |
| 145 | // tStd::tMemcpy((uint8*)Pixels8, rawPixels, rawPixelsSize);  |
| 146 | }  |
| 147 | else  |
| 148 | {  |
| 149 | Pixels16 = new tPixel4s[numPixels];  |
| 150 | int bytesPerRow = Width*sizeof(tPixel4s);  |
| 151 | for (int y = Height-1; y >= 0; y--)  |
| 152 | tStd::tMemcpy((uint8*)Pixels16 + ((Height-1)-y)*bytesPerRow, rawPixels + y*bytesPerRow, bytesPerRow);  |
| 153 |   |
| 154 | // For no reversal we would just do this:  |
| 155 | // tStd::tMemcpy((uint8*)Pixels16, rawPixels, rawPixelsSize);  |
| 156 | }  |
| 157 | png_image_free(&pngImage);  |
| 158 | delete[] rawPixels;  |
| 159 |   |
| 160 | if ((params.Flags & LoadFlag_ForceToBpc8) && Pixels16)  |
| 161 | {  |
| 162 | Pixels8 = new tPixel4b[Width*Height*sizeof(tPixel4b)];  |
| 163 |   |
| 164 | int dindex = 0; tColour4b c;  |
| 165 | for (int p = 0; p < Width*Height; p++)  |
| 166 | {  |
| 167 | c.Set(Pixels16[p]);  |
| 168 | Pixels8[p].Set(c);  |
| 169 | }  |
| 170 | delete[] Pixels16;  |
| 171 | Pixels16 = nullptr;  |
| 172 | }  |
| 173 |   |
| 174 | PixelFormat = Pixels8 ? tPixelFormat::R8G8B8A8 : tPixelFormat::R16G16B16A16;  |
| 175 |   |
| 176 | // Apply gamma or sRGB compression if necessary.  |
| 177 | tAssert(Pixels8 || Pixels16);  |
| 178 | bool flagSRGB = (params.Flags & LoadFlag_SRGBCompression) ? true : false;  |
| 179 | bool flagGama = (params.Flags & LoadFlag_GammaCompression)? true : false;  |
| 180 | if (Pixels8 && (flagSRGB || flagGama))  |
| 181 | {  |
| 182 | for (int p = 0; p < Width*Height; p++)  |
| 183 | {  |
| 184 | tColour4f colour(Pixels8[p]);  |
| 185 | if (flagSRGB)  |
| 186 | colour.LinearToSRGB(tCompBit_RGB);  |
| 187 | if (flagGama)  |
| 188 | colour.LinearToGamma(params.Gamma, tCompBit_RGB);  |
| 189 | Pixels8[p].SetR(colour.R);  |
| 190 | Pixels8[p].SetG(colour.G);  |
| 191 | Pixels8[p].SetB(colour.B);  |
| 192 | }  |
| 193 | }  |
| 194 | else if (Pixels16 && (flagSRGB || flagGama))  |
| 195 | {  |
| 196 | for (int p = 0; p < Width*Height; p++)  |
| 197 | {  |
| 198 | tColour4f colour(Pixels16[p]);  |
| 199 | if (flagSRGB)  |
| 200 | colour.LinearToSRGB(tCompBit_RGB);  |
| 201 | if (flagGama)  |
| 202 | colour.LinearToGamma(params.Gamma, tCompBit_RGB);  |
| 203 | Pixels16[p].SetR(colour.R);  |
| 204 | Pixels16[p].SetG(colour.G);  |
| 205 | Pixels16[p].SetB(colour.B);  |
| 206 | }  |
| 207 | }  |
| 208 |   |
| 209 | if (params.Flags & LoadFlag_SRGBCompression) ColourProfile = tColourProfile::sRGB;  |
| 210 | if (params.Flags & LoadFlag_GammaCompression) ColourProfile = tColourProfile::gRGB;  |
| 211 |   |
| 212 | return true;  |
| 213 | }  |
| 214 | #endif  |
| 215 |   |
| 216 |   |
| 217 | #ifdef USE_SPNG_LIBRARY  |
| 218 | bool tImagePNG::Load(const uint8* pngFileInMemory, int numBytes, const LoadParams& paramsIn)  |
| 219 | {  |
| 220 | Clear();  |
| 221 | if ((numBytes <= 0) || !pngFileInMemory)  |
| 222 | return false;  |
| 223 |   |
| 224 | LoadParams params(paramsIn);  |
| 225 |   |
| 226 | spng_ctx* ctx = spng_ctx_new(flags: 0);  |
| 227 | if (!ctx)  |
| 228 | return false;  |
| 229 |   |
| 230 | // Ignore and don't calculate chunk CRCs.  |
| 231 | spng_set_crc_action(ctx, critical: SPNG_CRC_USE, ancillary: SPNG_CRC_USE);  |
| 232 |   |
| 233 | // Set memory usage limits for storing standard and unknown chunks. This is important when reading untrusted files.  |
| 234 | size_t limit = 1024 * 1024 * 64;  |
| 235 | spng_set_chunk_limits(ctx, chunk_size: limit, cache_size: limit);  |
| 236 |   |
| 237 | // Tell the context about the source png image.  |
| 238 | spng_set_png_buffer(ctx, buf: pngFileInMemory, size: numBytes);  |
| 239 |   |
| 240 | struct spng_ihdr ihdr;  |
| 241 | int errCode = spng_get_ihdr(ctx, ihdr: &ihdr);  |
| 242 | if (errCode)  |
| 243 | {  |
| 244 | spng_ctx_free(ctx);  |
| 245 | if ((params.Flags & LoadFlag_AllowJPG))  |
| 246 | {  |
| 247 | tImageJPG jpg;  |
| 248 | bool success = jpg.Load(jpgFileInMemory: pngFileInMemory, numBytes);  |
| 249 | if (!success)  |
| 250 | return false;  |
| 251 |   |
| 252 | PixelFormatSrc = tPixelFormat::R8G8B8;  |
| 253 | PixelFormat = PixelFormatSrc;  |
| 254 | ColourProfileSrc = tColourProfile::sRGB;  |
| 255 | ColourProfile = ColourProfileSrc;  |
| 256 | Width = jpg.GetWidth();  |
| 257 | Height = jpg.GetHeight();  |
| 258 | Pixels8 = jpg.StealPixels();  |
| 259 | return true;  |
| 260 | }  |
| 261 |   |
| 262 | return false;  |
| 263 | }  |
| 264 |   |
| 265 | Width = ihdr.width;  |
| 266 | Height = ihdr.height;  |
| 267 | int numPixels = Width * Height;  |
| 268 | int bitDepth = ihdr.bit_depth;  |
| 269 |   |
| 270 | if (ihdr.color_type == SPNG_COLOR_TYPE_INDEXED)  |
| 271 | {  |
| 272 | switch (bitDepth)  |
| 273 | {  |
| 274 | case 1: PixelFormatSrc = tPixelFormat::PAL1BIT; break;  |
| 275 | case 2: PixelFormatSrc = tPixelFormat::PAL2BIT; break;  |
| 276 | case 3: PixelFormatSrc = tPixelFormat::PAL3BIT; break;  |
| 277 | case 4: PixelFormatSrc = tPixelFormat::PAL4BIT; break;  |
| 278 | case 5: PixelFormatSrc = tPixelFormat::PAL5BIT; break;  |
| 279 | case 6: PixelFormatSrc = tPixelFormat::PAL6BIT; break;  |
| 280 | case 7: PixelFormatSrc = tPixelFormat::PAL7BIT; break;  |
| 281 | default:  |
| 282 | case 8: PixelFormatSrc = tPixelFormat::PAL8BIT; break;  |
| 283 | }   |
| 284 | }  |
| 285 | else  |
| 286 | {  |
| 287 | bool hasAlpha = (ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA) || (ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA);  |
| 288 |   |
| 289 | // If the src bit depth is 16, RGBA are all linear. Otherwise RGB are sRGB and A is linear.  |
| 290 | if (bitDepth == 16)  |
| 291 | PixelFormatSrc = hasAlpha ? tPixelFormat::R16G16B16A16 : tPixelFormat::R16G16B16;  |
| 292 | else  |
| 293 | PixelFormatSrc = hasAlpha ? tPixelFormat::R8G8B8A8 : tPixelFormat::R8G8B8;  |
| 294 | ColourProfileSrc = (bitDepth == 16) ? tColourProfile::lRGB : tColourProfile::sRGB;  |
| 295 | }  |
| 296 |   |
| 297 | PixelFormat = PixelFormatSrc;  |
| 298 | ColourProfile = ColourProfileSrc;  |
| 299 |   |
| 300 | // Are we being asked to do auto-gamma-compression?  |
| 301 | if (params.Flags & LoadFlag_AutoGamma)  |
| 302 | {  |
| 303 | // Clear all related flags.  |
| 304 | params.Flags &= ~(LoadFlag_AutoGamma | LoadFlag_SRGBCompression | LoadFlag_GammaCompression);  |
| 305 | if (ColourProfileSrc == tColourProfile::lRGB)  |
| 306 | params.Flags |= LoadFlag_SRGBCompression;  |
| 307 | }  |
| 308 |   |
| 309 | struct spng_plte plte = { .n_entries: 0 };  |
| 310 | errCode = spng_get_plte(ctx, plte: &plte);  |
| 311 | if (errCode && (errCode != SPNG_ECHUNKAVAIL))  |
| 312 | {  |
| 313 | spng_ctx_free(ctx);  |
| 314 | Clear();  |
| 315 | return false;  |
| 316 | }  |
| 317 |   |
| 318 | // Output format, does not depend on source PNG format except for SPNG_FMT_PNG, which is the PNGs format in  |
| 319 | // host-endian (or big-endian for SPNG_FMT_RAW). Note that for these two formats < 8-bit images are left byte-packed.  |
| 320 | // Here we decode to a 16 bit buffer if the src is 16 bit to keep full precision. For non-16-bit per component  |
| 321 | // buffers, including palettized, we decode to RGBA8.  |
| 322 | int fmt = (bitDepth == 16) ? SPNG_FMT_RGBA16 : SPNG_FMT_RGBA8;  |
| 323 |   |
| 324 | size_t rawPixelsSize = 0;  |
| 325 | errCode = spng_decoded_image_size(ctx, fmt, len: &rawPixelsSize);  |
| 326 | if (errCode)  |
| 327 | {  |
| 328 | spng_ctx_free(ctx);  |
| 329 | Clear();  |
| 330 | return false;  |
| 331 | }  |
| 332 |   |
| 333 | uint8* rawPixels = new uint8[rawPixelsSize];  |
| 334 |   |
| 335 | // Decode the image in one go. I'm pretty sure we always want to decode transparency.  |
| 336 | // Certainly for palettized images it is required.  |
| 337 | errCode = spng_decode_image(ctx, out: rawPixels, len: rawPixelsSize, fmt, flags: SPNG_DECODE_TRNS);  |
| 338 | if (errCode)  |
| 339 | {  |
| 340 | delete[] rawPixels;  |
| 341 | spng_ctx_free(ctx);  |
| 342 | Clear();  |
| 343 | return false;  |
| 344 | }  |
| 345 |   |
| 346 | // Reverse rows as we copy into our final buffer.  |
| 347 | if (fmt == SPNG_FMT_RGBA8)  |
| 348 | {  |
| 349 | Pixels8 = new tPixel4b[numPixels];  |
| 350 | int bytesPerRow = Width*sizeof(tPixel4b);  |
| 351 | for (int y = Height-1; y >= 0; y--)  |
| 352 | tStd::tMemcpy(dest: (uint8*)Pixels8 + ((Height-1)-y)*bytesPerRow, src: rawPixels + y*bytesPerRow, numBytes: bytesPerRow);  |
| 353 | }  |
| 354 | else  |
| 355 | {  |
| 356 | Pixels16 = new tPixel4s[numPixels];  |
| 357 | int bytesPerRow = Width*sizeof(tPixel4s);  |
| 358 | for (int y = Height-1; y >= 0; y--)  |
| 359 | tStd::tMemcpy(dest: (uint8*)Pixels16 + ((Height-1)-y)*bytesPerRow, src: rawPixels + y*bytesPerRow, numBytes: bytesPerRow);  |
| 360 | }  |
| 361 | delete[] rawPixels;  |
| 362 | spng_ctx_free(ctx);  |
| 363 |   |
| 364 | if ((params.Flags & LoadFlag_ForceToBpc8) && Pixels16)  |
| 365 | {  |
| 366 | Pixels8 = new tPixel4b[Width*Height*sizeof(tPixel4b)];  |
| 367 |   |
| 368 | int dindex = 0; tColour4b c;  |
| 369 | for (int p = 0; p < Width*Height; p++)  |
| 370 | {  |
| 371 | c.Set(Pixels16[p]);  |
| 372 | Pixels8[p].Set(c);  |
| 373 | }  |
| 374 | delete[] Pixels16;  |
| 375 | Pixels16 = nullptr;  |
| 376 | }  |
| 377 |   |
| 378 | PixelFormat = Pixels8 ? tPixelFormat::R8G8B8A8 : tPixelFormat::R16G16B16A16;  |
| 379 |   |
| 380 | // Apply gamma or sRGB compression if necessary.  |
| 381 | tAssert(Pixels8 || Pixels16);  |
| 382 | bool flagSRGB = (params.Flags & LoadFlag_SRGBCompression) ? true : false;  |
| 383 | bool flagGama = (params.Flags & LoadFlag_GammaCompression)? true : false;  |
| 384 | if (Pixels8 && (flagSRGB || flagGama))  |
| 385 | {  |
| 386 | for (int p = 0; p < Width*Height; p++)  |
| 387 | {  |
| 388 | tColour4f colour(Pixels8[p]);  |
| 389 | if (flagSRGB)  |
| 390 | colour.LinearToSRGB(chans: tCompBit_RGB);  |
| 391 | if (flagGama)  |
| 392 | colour.LinearToGamma(gamma: params.Gamma, chans: tCompBit_RGB);  |
| 393 | Pixels8[p].SetR(colour.R);  |
| 394 | Pixels8[p].SetG(colour.G);  |
| 395 | Pixels8[p].SetB(colour.B);  |
| 396 | }  |
| 397 | }  |
| 398 | else if (Pixels16 && (flagSRGB || flagGama))  |
| 399 | {  |
| 400 | for (int p = 0; p < Width*Height; p++)  |
| 401 | {  |
| 402 | tColour4f colour(Pixels16[p]);  |
| 403 | if (flagSRGB)  |
| 404 | colour.LinearToSRGB(chans: tCompBit_RGB);  |
| 405 | if (flagGama)  |
| 406 | colour.LinearToGamma(gamma: params.Gamma, chans: tCompBit_RGB);  |
| 407 | Pixels16[p].SetR(colour.R);  |
| 408 | Pixels16[p].SetG(colour.G);  |
| 409 | Pixels16[p].SetB(colour.B);  |
| 410 | }  |
| 411 | }  |
| 412 |   |
| 413 | if (params.Flags & LoadFlag_SRGBCompression) ColourProfile = tColourProfile::sRGB;  |
| 414 | if (params.Flags & LoadFlag_GammaCompression) ColourProfile = tColourProfile::gRGB;  |
| 415 | return true;  |
| 416 | }  |
| 417 | #endif  |
| 418 |   |
| 419 |   |
| 420 | bool tImagePNG::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 421 | {  |
| 422 | Clear();  |
| 423 | if (!pixels || (width <= 0) || (height <= 0))  |
| 424 | return false;  |
| 425 |   |
| 426 | Width = width;  |
| 427 | Height = height;  |
| 428 | if (steal)  |
| 429 | {  |
| 430 | Pixels8 = pixels;  |
| 431 | }  |
| 432 | else  |
| 433 | {  |
| 434 | Pixels8 = new tPixel4b[Width*Height];  |
| 435 | tStd::tMemcpy(dest: Pixels8, src: pixels, numBytes: Width*Height*sizeof(tPixel4b));  |
| 436 | }  |
| 437 |   |
| 438 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 439 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 440 | ColourProfileSrc = tColourProfile::sRGB; // We assume 4-byte pixels must be sRGB.  |
| 441 | ColourProfile = tColourProfile::sRGB;  |
| 442 |   |
| 443 | return true;  |
| 444 | }  |
| 445 |   |
| 446 |   |
| 447 | bool tImagePNG::Set(tPixel4s* pixels, int width, int height, bool steal)  |
| 448 | {  |
| 449 | Clear();  |
| 450 | if (!pixels || (width <= 0) || (height <= 0))  |
| 451 | return false;  |
| 452 |   |
| 453 | Width = width;  |
| 454 | Height = height;  |
| 455 | if (steal)  |
| 456 | {  |
| 457 | Pixels16 = pixels;  |
| 458 | }  |
| 459 | else  |
| 460 | {  |
| 461 | Pixels16 = new tPixel4s[Width*Height];  |
| 462 | tStd::tMemcpy(dest: Pixels16, src: pixels, numBytes: Width*Height*sizeof(tPixel4s));  |
| 463 | }  |
| 464 |   |
| 465 | PixelFormatSrc = tPixelFormat::R16G16B16A16;  |
| 466 | PixelFormat = tPixelFormat::R16G16B16A16;  |
| 467 | ColourProfileSrc = tColourProfile::HDRa; // We assume 4-short pixels must be HDRa.  |
| 468 | ColourProfile = tColourProfile::HDRa;  |
| 469 |   |
| 470 | return true;  |
| 471 | }  |
| 472 |   |
| 473 |   |
| 474 | bool tImagePNG::Set(tFrame* frame, bool steal)  |
| 475 | {  |
| 476 | Clear();  |
| 477 | if (!frame || !frame->IsValid())  |
| 478 | return false;  |
| 479 |   |
| 480 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 481 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 482 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 483 | ColourProfile = tColourProfile::sRGB;  |
| 484 |   |
| 485 | Set(pixels: frame->GetPixels(steal), width: frame->Width, height: frame->Height, steal);  |
| 486 | if (steal)  |
| 487 | delete frame;  |
| 488 |   |
| 489 | return true;  |
| 490 | }  |
| 491 |   |
| 492 |   |
| 493 | bool tImagePNG::Set(tPicture& picture, bool steal)  |
| 494 | {  |
| 495 | Clear();  |
| 496 | if (!picture.IsValid())  |
| 497 | return false;  |
| 498 |   |
| 499 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 500 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 501 | // We don't know colour profile of tPicture.  |
| 502 |   |
| 503 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 504 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 505 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 506 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 507 | // succeed if picture was valid.  |
| 508 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 509 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 510 | tAssert(success);  |
| 511 | return true;  |
| 512 | }  |
| 513 |   |
| 514 |   |
| 515 | tFrame* tImagePNG::GetFrame(bool steal)  |
| 516 | {  |
| 517 | if (!IsValid())  |
| 518 | return nullptr;  |
| 519 |   |
| 520 | tFrame* frame = new tFrame();  |
| 521 | frame->PixelFormatSrc = PixelFormatSrc;  |
| 522 |   |
| 523 | if (steal)  |
| 524 | {  |
| 525 | frame->StealFrom(src: Pixels8, width: Width, height: Height);  |
| 526 | Pixels8 = nullptr;  |
| 527 | }  |
| 528 | else  |
| 529 | {  |
| 530 | frame->Set(srcPixels: Pixels8, width: Width, height: Height);  |
| 531 | }  |
| 532 |   |
| 533 | return frame;  |
| 534 | }  |
| 535 |   |
| 536 |   |
| 537 | tImagePNG::tFormat tImagePNG::Save(const tString& pngFile, tFormat format) const  |
| 538 | {  |
| 539 | SaveParams params;  |
| 540 | params.Format = format;  |
| 541 | return Save(pngFile, params);  |
| 542 | }  |
| 543 |   |
| 544 |   |
| 545 | #ifndef USE_SPNG_LIBRARY  |
| 546 | tImagePNG::tFormat tImagePNG::Save(const tString& pngFile, const SaveParams& params) const  |
| 547 | {  |
| 548 | if (!IsValid())  |
| 549 | return tFormat::Invalid;  |
| 550 |   |
| 551 | if (tSystem::tGetFileType(pngFile) != tSystem::tFileType::PNG)  |
| 552 | return tFormat::Invalid;  |
| 553 |   |
| 554 | int dstBytesPerPixel = 0;  |
| 555 |   |
| 556 | switch (params.Format)  |
| 557 | {  |
| 558 | case tFormat::BPP24_RGB_BPC8: dstBytesPerPixel = 3; break;  |
| 559 | case tFormat::BPP32_RGBA_BPC8: dstBytesPerPixel = 4; break;  |
| 560 | case tFormat::BPP48_RGB_BPC16: dstBytesPerPixel = 6; break;  |
| 561 | case tFormat::BPP64_RGBA_BPC16: dstBytesPerPixel = 8; break;  |
| 562 | case tFormat::Auto:  |
| 563 | dstBytesPerPixel = IsOpaque() ? 3 : 4;  |
| 564 | if (Pixels16) dstBytesPerPixel <<= 1;  |
| 565 | break;  |
| 566 | }  |
| 567 | if (!dstBytesPerPixel)  |
| 568 | return tFormat::Invalid;  |
| 569 |   |
| 570 | // Guard against integer overflow when saving.  |
| 571 | if (Height > PNG_SIZE_MAX / (Width * dstBytesPerPixel))  |
| 572 | return tFormat::Invalid;  |
| 573 |   |
| 574 | // If it's 3 or 6 bytes per pixel we make a no-alpha-channel buffer. This should not be  |
| 575 | // necessary but I can't figure out how to get libpng reading 32bit/64bit and writing 24/48.  |
| 576 | uint8* pixelData = new uint8[Width*Height*dstBytesPerPixel];  |
| 577 |   |
| 578 | switch (dstBytesPerPixel)  |
| 579 | {  |
| 580 | case 3:  |
| 581 | {  |
| 582 | int dindex = 0; tColour4b c;  |
| 583 | for (int p = 0; p < Width*Height; p++)  |
| 584 | {  |
| 585 | if (Pixels8) c.Set(Pixels8[p]); else c.Set(Pixels16[p]);  |
| 586 | pixelData[dindex++] = c.R;  |
| 587 | pixelData[dindex++] = c.G;  |
| 588 | pixelData[dindex++] = c.B;  |
| 589 | }  |
| 590 | break;  |
| 591 | }  |
| 592 |   |
| 593 | case 4:  |
| 594 | if (Pixels8)  |
| 595 | {  |
| 596 | tStd::tMemcpy(pixelData, Pixels8, Width*Height*4);  |
| 597 | }  |
| 598 | else  |
| 599 | {  |
| 600 | int dindex = 0; tColour4b c;  |
| 601 | for (int p = 0; p < Width*Height; p++)  |
| 602 | {  |
| 603 | c.Set(Pixels16[p]);  |
| 604 | pixelData[dindex++] = c.R;  |
| 605 | pixelData[dindex++] = c.G;  |
| 606 | pixelData[dindex++] = c.B;  |
| 607 | pixelData[dindex++] = c.A;  |
| 608 | }  |
| 609 | }  |
| 610 | break;  |
| 611 |   |
| 612 | case 6:  |
| 613 | {  |
| 614 | int dindex = 0; tColour4s c; uint16* pdata = (uint16*)pixelData;  |
| 615 | for (int p = 0; p < Width*Height; p++)  |
| 616 | {   |
| 617 | if (Pixels16) c.Set(Pixels16[p]); else c.Set(Pixels8[p]);  |
| 618 | pdata[dindex++] = tSwapEndian16(c.R);  |
| 619 | pdata[dindex++] = tSwapEndian16(c.G);  |
| 620 | pdata[dindex++] = tSwapEndian16(c.B);  |
| 621 | }  |
| 622 | break;  |
| 623 | }  |
| 624 |   |
| 625 | case 8:  |
| 626 | {  |
| 627 | int dindex = 0; tColour4s c; uint16* pdata = (uint16*)pixelData;  |
| 628 | for (int p = 0; p < Width*Height; p++)  |
| 629 | {  |
| 630 | if (Pixels16) c.Set(Pixels16[p]); else c.Set(Pixels8[p]);  |
| 631 | pdata[dindex++] = tSwapEndian16(c.R);  |
| 632 | pdata[dindex++] = tSwapEndian16(c.G);  |
| 633 | pdata[dindex++] = tSwapEndian16(c.B);  |
| 634 | pdata[dindex++] = tSwapEndian16(c.A);  |
| 635 | }  |
| 636 | break;  |
| 637 | }  |
| 638 | }  |
| 639 |   |
| 640 | FILE* fp = fopen(pngFile.Chr(), "wb" );  |
| 641 | if (!fp)  |
| 642 | {  |
| 643 | delete[] pixelData;  |
| 644 | return tFormat::Invalid;  |
| 645 | }  |
| 646 |   |
| 647 | // Create and initialize the png_struct with the desired error handler functions.  |
| 648 | png_structp pngPtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);  |
| 649 | if (!pngPtr)  |
| 650 | {  |
| 651 | fclose(fp);  |
| 652 | delete[] pixelData;  |
| 653 | return tFormat::Invalid;  |
| 654 | }  |
| 655 |   |
| 656 | png_infop infoPtr = png_create_info_struct(pngPtr);  |
| 657 | if (!infoPtr)  |
| 658 | {  |
| 659 | fclose(fp);  |
| 660 | png_destroy_write_struct(&pngPtr, 0);  |
| 661 | delete[] pixelData;  |
| 662 | return tFormat::Invalid;  |
| 663 | }  |
| 664 |   |
| 665 | // Set up default error handling.  |
| 666 | if (setjmp(png_jmpbuf(pngPtr)))  |
| 667 | {  |
| 668 | fclose(fp);  |
| 669 | png_destroy_write_struct(&pngPtr, &infoPtr);  |
| 670 | delete[] pixelData;  |
| 671 | return tFormat::Invalid;  |
| 672 | }  |
| 673 |   |
| 674 | png_init_io(pngPtr, fp);  |
| 675 |   |
| 676 | // Supported depths are 1, 2, 4, 8, 16. We support 8 and 16.  |
| 677 | int bitDepth = (dstBytesPerPixel <= 4) ? 8 : 16;  |
| 678 |   |
| 679 | // We write either 24/32 or 48/64 bit images depending on whether we have an alpha channel.  |
| 680 | uint32 pngColourType = ((dstBytesPerPixel == 3) || (dstBytesPerPixel == 6)) ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;  |
| 681 | png_set_IHDR  |
| 682 | (  |
| 683 | pngPtr, infoPtr, Width, Height, bitDepth, pngColourType,  |
| 684 | PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE  |
| 685 | );  |
| 686 |   |
| 687 | // The sBIT chunk tells the decoder the number of significant bits in the pixel data. It is optional  |
| 688 | // as the data is still stored as either 8 or 16 bits per component,  |
| 689 | png_color_8 sigBit;  |
| 690 | sigBit.red = bitDepth;  |
| 691 | sigBit.green = bitDepth;  |
| 692 | sigBit.blue = bitDepth;  |
| 693 | sigBit.alpha = ((dstBytesPerPixel == 3) || (dstBytesPerPixel == 6)) ? 0 : bitDepth;  |
| 694 | png_set_sBIT(pngPtr, infoPtr, &sigBit);  |
| 695 |   |
| 696 | png_write_info(pngPtr, infoPtr);  |
| 697 |   |
| 698 | // Shift the pixels up to a legal bit depth and fill in as appropriate to correctly scale the image.  |
| 699 | // png_set_shift(pngPtr, &sigBit);  |
| 700 | //  |
| 701 | // Pack pixels into bytes.  |
| 702 | // png_set_packing(pngPtr);  |
| 703 | //  |
| 704 | // Swap location of alpha bytes from ARGB to RGBA.  |
| 705 | // png_set_swap_alpha(pngPtr);  |
| 706 | //  |
| 707 | // Get rid of filler (OR ALPHA) bytes, pack XRGB/RGBX/ARGB/RGBA into RGB (4 channels -> 3 channels). The second parameter is not used.  |
| 708 | // png_set_filler(pngPtr, 0, PNG_FILLER_BEFORE);  |
| 709 | //  |
| 710 | // png_set_strip_alpha(pngPtr);  |
| 711 | //  |
| 712 | // Flip BGR pixels to RGB.  |
| 713 | // png_set_bgr(pngPtr);  |
| 714 | //  |
| 715 | // Swap bytes of 16-bit files to most significant byte first.  |
| 716 | // png_set_swap(pngPtr);  |
| 717 | tArray<png_bytep> rowPointers(Height);  |
| 718 |   |
| 719 | // Set up pointers into the src data.  |
| 720 | for (int r = 0; r < Height; r++)  |
| 721 | rowPointers[Height-1-r] = pixelData + r * Width * dstBytesPerPixel;  |
| 722 |   |
| 723 | // tArray has an implicit cast operator. rowPointers is equivalient to rowPointers.GetElements().  |
| 724 | png_write_image(pngPtr, rowPointers);  |
| 725 |   |
| 726 | // Finish writing the rest of the file.  |
| 727 | png_write_end(pngPtr, infoPtr);  |
| 728 |   |
| 729 | // Clear the srcPixels if we created the buffer.  |
| 730 | delete[] pixelData;  |
| 731 | pixelData = nullptr;  |
| 732 |   |
| 733 | // Clean up.  |
| 734 | png_destroy_write_struct(&pngPtr, &infoPtr);  |
| 735 | fclose(fp);  |
| 736 |   |
| 737 | switch (dstBytesPerPixel)  |
| 738 | {  |
| 739 | case 3: return tFormat::BPP24_RGB_BPC8;  |
| 740 | case 4: return tFormat::BPP32_RGBA_BPC8;  |
| 741 | case 6: return tFormat::BPP48_RGB_BPC16;  |
| 742 | case 8: return tFormat::BPP64_RGBA_BPC16;  |
| 743 | }  |
| 744 |   |
| 745 | return tFormat::Invalid;  |
| 746 | }  |
| 747 | #endif  |
| 748 |   |
| 749 |   |
| 750 | #ifdef USE_SPNG_LIBRARY  |
| 751 | tImagePNG::tFormat tImagePNG::Save(const tString& pngFile, const SaveParams& params) const  |
| 752 | {  |
| 753 | if (!IsValid())  |
| 754 | return tFormat::Invalid;  |
| 755 |   |
| 756 | if (tSystem::tGetFileType(file: pngFile) != tSystem::tFileType::PNG)  |
| 757 | return tFormat::Invalid;  |
| 758 |   |
| 759 | int bytesPerPixel = 0;  |
| 760 | switch (params.Format)  |
| 761 | {  |
| 762 | case tFormat::BPP24_RGB_BPC8: bytesPerPixel = 3; break;  |
| 763 | case tFormat::BPP32_RGBA_BPC8: bytesPerPixel = 4; break;  |
| 764 | case tFormat::BPP48_RGB_BPC16: bytesPerPixel = 6; break;  |
| 765 | case tFormat::BPP64_RGBA_BPC16: bytesPerPixel = 8; break;  |
| 766 | case tFormat::Auto:  |
| 767 | bytesPerPixel = IsOpaque() ? 3 : 4;  |
| 768 | if (Pixels16) bytesPerPixel <<= 1;  |
| 769 | break;  |
| 770 | }  |
| 771 | if (!bytesPerPixel)  |
| 772 | return tFormat::Invalid;  |
| 773 |   |
| 774 | // If it's 3 or 6 bytes per pixel we make a no-alpha-channel buffer. Basically we need the data  |
| 775 | // in the correct layout before we save it. SPNG does not do it for us.  |
| 776 | // The pixels need to be reordered so that the first row is at the top.  |
| 777 | uint8* pixelData = new uint8[Width*Height*bytesPerPixel];  |
| 778 | switch (bytesPerPixel)  |
| 779 | {  |
| 780 | case 3:  |
| 781 | {  |
| 782 | int dindex = 0; tColour4b c;  |
| 783 | for (int y = Height-1; y >= 0; y--)  |
| 784 | for (int x = 0; x < Width; x++)  |
| 785 | {  |
| 786 | int p = y*Width + x;  |
| 787 | if (Pixels8) c.Set(Pixels8[p]); else c.Set(Pixels16[p]);  |
| 788 | pixelData[dindex++] = c.R;  |
| 789 | pixelData[dindex++] = c.G;  |
| 790 | pixelData[dindex++] = c.B;  |
| 791 | }  |
| 792 | break;  |
| 793 | }  |
| 794 |   |
| 795 | case 4:  |
| 796 | {  |
| 797 | int dindex = 0; tColour4b c;  |
| 798 | for (int y = Height-1; y >= 0; y--)  |
| 799 | for (int x = 0; x < Width; x++)  |
| 800 | {  |
| 801 | int p = y*Width + x;  |
| 802 | if (Pixels8) c.Set(Pixels8[p]); else c.Set(Pixels16[p]);  |
| 803 | pixelData[dindex++] = c.R;  |
| 804 | pixelData[dindex++] = c.G;  |
| 805 | pixelData[dindex++] = c.B;  |
| 806 | pixelData[dindex++] = c.A;  |
| 807 | }  |
| 808 | break;  |
| 809 | }  |
| 810 |   |
| 811 | case 6:  |
| 812 | {  |
| 813 | int dindex = 0; tColour4s c; uint16* pdata = (uint16*)pixelData;  |
| 814 | for (int y = Height-1; y >= 0; y--)  |
| 815 | for (int x = 0; x < Width; x++)  |
| 816 | {  |
| 817 | int p = y*Width + x;  |
| 818 | if (Pixels16) c.Set(Pixels16[p]); else c.Set(Pixels8[p]);  |
| 819 | pdata[dindex++] = c.R;  |
| 820 | pdata[dindex++] = c.G;  |
| 821 | pdata[dindex++] = c.B;  |
| 822 | }  |
| 823 | break;  |
| 824 | }  |
| 825 |   |
| 826 | case 8:  |
| 827 | {  |
| 828 | int dindex = 0; tColour4s c; uint16* pdata = (uint16*)pixelData;  |
| 829 | for (int y = Height-1; y >= 0; y--)  |
| 830 | for (int x = 0; x < Width; x++)  |
| 831 | {  |
| 832 | int p = y*Width + x;  |
| 833 | if (Pixels16) c.Set(Pixels16[p]); else c.Set(Pixels8[p]);  |
| 834 | pdata[dindex++] = c.R;  |
| 835 | pdata[dindex++] = c.G;  |
| 836 | pdata[dindex++] = c.B;  |
| 837 | pdata[dindex++] = c.A;  |
| 838 | }  |
| 839 | break;  |
| 840 | }  |
| 841 | }  |
| 842 |   |
| 843 | FILE* fp = fopen(filename: pngFile.Chr(), modes: "wb" );  |
| 844 | if (!fp)  |
| 845 | {  |
| 846 | delete[] pixelData;  |
| 847 | return tFormat::Invalid;  |
| 848 | }  |
| 849 |   |
| 850 | // Creating an encoder context requires a flag.  |
| 851 | spng_ctx* ctx = spng_ctx_new(flags: SPNG_CTX_ENCODER);  |
| 852 |   |
| 853 | // Don't encode to internal buffer managed by the library. We'll be writing to a file.  |
| 854 | spng_set_option(ctx, option: SPNG_ENCODE_TO_BUFFER, value: 0);  |
| 855 | spng_set_png_file(ctx, file: fp);  |
| 856 | spng_set_option(ctx, option: SPNG_FILTER_CHOICE, value: SPNG_DISABLE_FILTERING);  |
| 857 |   |
| 858 | // Set image properties, this determines the destination image format. Start by zero-initing ihdr.  |
| 859 | struct spng_ihdr ihdr = { .width: 0 };  |
| 860 | ihdr.width = Width;  |
| 861 | ihdr.height = Height;  |
| 862 |   |
| 863 | // See https://www.w3.org/TR/2003/REC-PNG-20031110/#table111 for valid color-type/bit-depth combinations.  |
| 864 | switch (bytesPerPixel)  |
| 865 | {  |
| 866 | case 3: ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR; ihdr.bit_depth = 8; break;  |
| 867 | case 4: ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; ihdr.bit_depth = 8; break;  |
| 868 | case 6: ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR; ihdr.bit_depth = 16; break;  |
| 869 | case 8: ihdr.color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA; ihdr.bit_depth = 16; break;  |
| 870 | }  |
| 871 | spng_set_ihdr(ctx, ihdr: &ihdr);  |
| 872 |   |
| 873 | // This is the source data format. SPNG_FMT_PNG is a special value that matches the format in ihdr  |
| 874 | // The encode call only works if the format is SPNG_FMT_PNG (machine-endian) or SPNG_FMT_RAW (big-endian).  |
| 875 | // SPNG_ENCODE_FINALIZE will finalize the PNG with the end-of-file marker.  |
| 876 | int errCode = spng_encode_image(ctx, img: pixelData, len: Width*Height*bytesPerPixel, fmt: SPNG_FMT_PNG, flags: SPNG_ENCODE_FINALIZE);  |
| 877 | if (errCode)  |
| 878 | {  |
| 879 | fclose(stream: fp);  |
| 880 | spng_ctx_free(ctx);  |
| 881 | delete[] pixelData;  |
| 882 | return tFormat::Invalid;  |
| 883 | }  |
| 884 |   |
| 885 | fclose(stream: fp);  |
| 886 | spng_ctx_free(ctx);  |
| 887 | delete[] pixelData;  |
| 888 |   |
| 889 | switch (bytesPerPixel)  |
| 890 | {  |
| 891 | case 3: return tFormat::BPP24_RGB_BPC8;  |
| 892 | case 4: return tFormat::BPP32_RGBA_BPC8;  |
| 893 | case 6: return tFormat::BPP48_RGB_BPC16;  |
| 894 | case 8: return tFormat::BPP64_RGBA_BPC16;  |
| 895 | }  |
| 896 |   |
| 897 | return tFormat::Invalid;  |
| 898 | }  |
| 899 | #endif  |
| 900 |   |
| 901 |   |
| 902 | bool tImagePNG::IsOpaque() const  |
| 903 | {  |
| 904 | if (Pixels8)  |
| 905 | {  |
| 906 | for (int p = 0; p < (Width*Height); p++)  |
| 907 | {  |
| 908 | if (Pixels8[p].A < 255)  |
| 909 | return false;  |
| 910 | }  |
| 911 | }  |
| 912 | else if (Pixels16)  |
| 913 | {  |
| 914 | for (int p = 0; p < (Width*Height); p++)  |
| 915 | {  |
| 916 | if (Pixels16[p].A < 65535)  |
| 917 | return false;  |
| 918 | }  |
| 919 | }  |
| 920 |   |
| 921 | return true;  |
| 922 | }  |
| 923 |   |
| 924 |   |
| 925 | tPixel4b* tImagePNG::StealPixels8()  |
| 926 | {  |
| 927 | if (!Pixels8)  |
| 928 | return nullptr;  |
| 929 |   |
| 930 | tPixel4b* pixels = Pixels8;  |
| 931 | Pixels8 = nullptr;  |
| 932 | Width = 0;  |
| 933 | Height = 0;  |
| 934 | return pixels;  |
| 935 | }  |
| 936 |   |
| 937 |   |
| 938 | tPixel4s* tImagePNG::StealPixels16()  |
| 939 | {  |
| 940 | if (!Pixels16)  |
| 941 | return nullptr;  |
| 942 |   |
| 943 | tPixel4s* pixels = Pixels16;  |
| 944 | Pixels16 = nullptr;  |
| 945 | Width = 0;  |
| 946 | Height = 0;  |
| 947 | return pixels;  |
| 948 | }  |
| 949 |   |
| 950 |   |
| 951 | }  |
| 952 | |