1// tImageAPNG.cpp 
2// 
3// This knows how to load/save animated PNGs (APNGs). It knows the details of the apng file format and loads the data 
4// into multiple tPixel arrays, one for each frame. 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/tImageAPNG.h" 
20#include "Image/tPicture.h" 
21#include "apngdis.h" 
22#include "apngasm.h" 
23using namespace tSystem
24namespace tImage 
25
26 
27 
28bool tImageAPNG::IsAnimatedPNG(const tString& pngFile
29
30 int numBytes = 2048
31 
32 // Remember, tLoadFileHead modifies numBytes if the file is smaller than the head-size requested. 
33 uint8* headData = tSystem::tLoadFileHead(file: pngFile, bytesToRead&: numBytes); 
34 if (!headData
35 return false
36 
37 uint8 acTL[] = { 'a', 'c', 'T', 'L' }; 
38 uint8 IDAT[] = { 'I', 'D', 'A', 'T' }; 
39 uint8* actlLoc = (uint8*)tStd::tMemsrch(haystack: headData, haystackNumBytes: numBytes, needle: acTL, needleNumBytes: sizeof(acTL)); 
40 if (!actlLoc
41 return false
42 
43 // Now for safety we also make sure there is an IDAT after the acTL. 
44 uint8* idatLoc = (uint8*)tStd::tMemsrch(haystack: actlLoc+sizeof(acTL), haystackNumBytes: numBytes - (actlLoc-headData) - sizeof(acTL), needle: IDAT, needleNumBytes: sizeof(IDAT)); 
45 
46 bool found = idatLoc ? true : false
47 delete[] headData
48 return found
49
50 
51 
52bool tImageAPNG::Load(const tString& apngFile
53
54 Clear(); 
55 
56 // Note that many apng files still have a .png extension/filetype, so we support both here. 
57 tSystem::tFileType filetype = tSystem::tGetFileType(file: apngFile); 
58 if ((filetype != tSystem::tFileType::APNG) && (filetype != tSystem::tFileType::PNG)) 
59 return false
60 
61 if (!tFileExists(file: apngFile)) 
62 return false
63 
64 std::vector<APngDis::Image> frames
65 
66 // We assume here that load_apng can hande UTF-8 filenames. 
67 int result = APngDis::load_apng(szIn: apngFile.Chr(), img&: frames); 
68 if (result < 0
69 return false
70 
71 // Now we load and populate the frames. 
72 for (int f = 0; f < frames.size(); f++) 
73
74 APngDis::Image& srcFrame = frames[f]; 
75 tFrame* newFrame = new tFrame
76 int width = srcFrame.w
77 int height = srcFrame.h
78 newFrame->Width = width
79 newFrame->Height = height
80 newFrame->Pixels = new tPixel4b[width * height]; 
81 
82 // From the official apng spec: 
83 // The delay_num and delay_den parameters together specify a fraction indicating the time to display 
84 // the current frame, in seconds. If the denominator is 0, it is to be treated as if it were 100 (that 
85 // is, delay_num then specifies 1/100ths of a second). If the the value of the numerator is 0 the decoder 
86 // should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound. 
87 uint dnum = srcFrame.delay_num
88 uint dden = srcFrame.delay_den
89 if (dden == 0
90 dden = 100
91 newFrame->Duration = (dnum == 0) ? 1.0f/60.0f : float(dnum) / float(dden); 
92 
93 tAssert(srcFrame.bpp == 4); 
94 for (int r = 0; r < height; r++) 
95
96 uint8* srcRowData = srcFrame.rows[r]; 
97 uint8* dstRowData = (uint8*)newFrame->Pixels + ((height-1)-r) * (width*4); 
98 tStd::tMemcpy(dest: dstRowData, src: srcRowData, numBytes: width*4); 
99
100 
101 Frames.Append(item: newFrame); 
102
103 
104 for (int f = 0; f < frames.size(); f++) 
105 frames[f].free(); 
106 frames.clear(); 
107 
108 PixelFormatSrc = tPixelFormat::R8G8B8A8
109 PixelFormat = tPixelFormat::R8G8B8A8
110 
111 // Assume colour profile is sRGB for source and current. 
112 ColourProfileSrc = tColourProfile::sRGB
113 ColourProfile = tColourProfile::sRGB
114 
115 if (IsOpaque()) 
116 PixelFormatSrc = tPixelFormat::R8G8B8
117 
118 // Set every frame's source pixel format. 
119 for (tFrame* frame = Frames.Head(); frame; frame = frame->Next()) 
120 frame->PixelFormatSrc = PixelFormatSrc
121 
122 return true
123
124 
125 
126bool tImageAPNG::Set(tList<tFrame>& srcFrames, bool stealFrames
127
128 Clear(); 
129 if (srcFrames.GetNumItems() <= 0
130 return false
131 
132 PixelFormatSrc = srcFrames.Head()->PixelFormatSrc
133 PixelFormat = tPixelFormat::R8G8B8A8
134 ColourProfileSrc = tColourProfile::sRGB; // We assume srcFrames must be sRGB. 
135 ColourProfile = tColourProfile::sRGB
136 
137 if (stealFrames
138
139 while (tFrame* frame = srcFrames.Remove()) 
140 Frames.Append(item: frame); 
141
142 else 
143
144 for (tFrame* frame = srcFrames.Head(); frame; frame = frame->Next()) 
145 Frames.Append(item: new tFrame(*frame)); 
146
147 
148 return true
149
150 
151 
152bool tImageAPNG::Set(tPixel4b* pixels, int width, int height, bool steal
153
154 Clear(); 
155 if (!pixels || (width <= 0) || (height <= 0)) 
156 return false
157 
158 tFrame* frame = new tFrame(); 
159 if (steal
160 frame->StealFrom(src: pixels, width, height); 
161 else 
162 frame->Set(srcPixels: pixels, width, height); 
163 Frames.Append(item: frame); 
164 
165 PixelFormatSrc = tPixelFormat::R8G8B8A8
166 PixelFormat = tPixelFormat::R8G8B8A8
167 ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB. 
168 ColourProfile = tColourProfile::sRGB
169 
170 return true
171
172 
173 
174bool tImageAPNG::Set(tFrame* frame, bool steal
175
176 Clear(); 
177 if (!frame || !frame->IsValid()) 
178 return false
179 
180 PixelFormatSrc = frame->PixelFormatSrc
181 PixelFormat = tPixelFormat::R8G8B8A8
182 ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB. 
183 ColourProfile = tColourProfile::sRGB
184 
185 if (steal
186 Frames.Append(item: frame); 
187 else 
188 Frames.Append(item: new tFrame(*frame)); 
189 return true
190
191 
192 
193bool tImageAPNG::Set(tPicture& picture, bool steal
194
195 Clear(); 
196 if (!picture.IsValid()) 
197 return false
198 
199 PixelFormatSrc = picture.PixelFormatSrc
200 PixelFormat = tPixelFormat::R8G8B8A8
201 // We don't know colour profile of tPicture. 
202 
203 // This is worth some explanation. If steal is true the picture becomes invalid and the 
204 // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the 
205 // 'set' call will memcpy them out... which makes sure the picture is still valid after and 
206 // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must 
207 // succeed if picture was valid. 
208 tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels(); 
209 bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal); 
210 tAssert(success); 
211 return true
212
213 
214 
215tFrame* tImageAPNG::GetFrame(bool steal
216
217 if (!IsValid()) 
218 return nullptr
219 
220 return steal ? Frames.Remove() : new tFrame( *Frames.First() ); 
221
222 
223 
224tImageAPNG::tFormat tImageAPNG::Save(const tString& apngFile, tFormat format, int overrideFrameDuration) const 
225
226 SaveParams params
227 params.Format = format
228 params.OverrideFrameDuration = overrideFrameDuration
229 return Save(apngFile, params); 
230
231 
232 
233tImageAPNG::tFormat tImageAPNG::Save(const tString& apngFile, const SaveParams& params) const 
234
235 if (!IsValid()) 
236 return tFormat::Invalid
237 
238 if ((tSystem::tGetFileType(file: apngFile) != tSystem::tFileType::PNG) && (tSystem::tGetFileType(file: apngFile) != tSystem::tFileType::APNG)) 
239 return tFormat::Invalid
240 
241 // Currently apng_save will crash if all the images don't have the same size. We check that here. 
242 // Additionally it seems the frame sizes in an APNG must match in size. From the spec: "Each frame 
243 // is identical for each play, therefore it is safe for applications to cache the frames." This means 
244 // we'll need to keep this check in. 
245 int frameWidth = Frames.Head()->Width
246 int frameHeight = Frames.Head()->Height
247 if ((frameWidth <= 0) || (frameHeight <= 0)) 
248 return tFormat::Invalid
249 for (tFrame* frame = Frames.Head(); frame; frame = frame->Next()) 
250
251 if ((frame->Width != frameWidth) || (frame->Height != frameHeight)) 
252 return tFormat::Invalid
253
254 
255 int overrideFrameDuration = params.OverrideFrameDuration
256 tMath::tiClampMax(val&: overrideFrameDuration, max: 65535); 
257 int bytesPerPixel = 0
258 switch (params.Format
259
260 case tFormat::Auto: bytesPerPixel = IsOpaque() ? 3 : 4; break
261 case tFormat::BPP24: bytesPerPixel = 3; break
262 case tFormat::BPP32: bytesPerPixel = 4; break
263
264 if (!bytesPerPixel
265 return tFormat::Invalid
266 
267 std::vector<APngAsm::Image> images
268 images.resize(new_size: Frames.GetNumItems()); 
269 
270 int frameIndex = 0
271 for (tFrame* frame = Frames.Head(); frame; frame = frame->Next(), frameIndex++) 
272
273 APngAsm::Image& img = images[frameIndex]; 
274 int w = frame->Width
275 int h = frame->Height
276 
277 // Use coltype = 6 for RGBA. Use coltype = 2 for RGB. 
278 int coltype = (bytesPerPixel == 4) ? 6 : 2
279 img.init(w1: frame->Width, h1: frame->Height, bpp1: bytesPerPixel, type1: coltype); 
280 
281 // Initing does not populate the pixel data. We do that here. 
282 for (int r = 0; r < h; r++) 
283
284 uint8* row = img.rows[h-r-1]; 
285 for (int x = 0; x < w; x++) 
286
287 int idx = r*w + x
288 row[x*bytesPerPixel + 0] = frame->Pixels[idx].R
289 row[x*bytesPerPixel + 1] = frame->Pixels[idx].G
290 row[x*bytesPerPixel + 2] = frame->Pixels[idx].B
291 if (bytesPerPixel == 4
292 row[x*bytesPerPixel + 3] = frame->Pixels[idx].A
293
294
295 
296 // From the official apng spec: 
297 // The delay_num and delay_den parameters together specify a fraction indicating the time to display 
298 // the current frame, in seconds. If the denominator is 0, it is to be treated as if it were 100 (that 
299 // is, delay_num then specifies 1/100ths of a second). If the the value of the numerator is 0 the decoder 
300 // should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound. 
301 // Default is numerator = 0. Fast as possible. Use when frameDur = 0. 
302 uint delayNumer = 0
303 uint delayDenom = 1000
304 if (overrideFrameDuration < 0
305
306 // We use milliseconds here since the apng format uses 16bit unsigned (65535 max) for numerator and denominator. 
307 // A max numerator of 65535 gives us ~65 seconds max per frame, which seems reasonable. 
308 float frameDur = frame->Duration
309 if (frameDur > 0.0f
310 delayNumer = uint(frameDur * 1000.0f); 
311
312 else 
313
314 if (overrideFrameDuration > 0
315 delayNumer = overrideFrameDuration
316
317 img.delay_num = delayNumer
318 img.delay_den = delayDenom
319
320 
321 int errCode = APngAsm::save_apng(szOut: (char*)apngFile.Chr(), img&: images, loops: 0, first: 0, deflate_method: 0, iter: 0); 
322 if (errCode
323 return tFormat::Invalid
324 
325 return (bytesPerPixel == 3) ? tFormat::BPP24 : tFormat::BPP32
326
327 
328 
329
330