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" 
38using namespace tSystem
39namespace tImage 
40
41 
42 
43bool 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 
63bool 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 
218bool 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 
420bool 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 
447bool 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 
474bool 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 
493bool 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 
515tFrame* 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 
537tImagePNG::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 
546tImagePNG::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 
751tImagePNG::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 
902bool 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 
925tPixel4b* 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 
938tPixel4s* 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