1 /++
2 	Support for [https://wiki.mozilla.org/APNG_Specification|animated png] files.
3 
4 	$(WARNING Please note this interface is not exactly stable and may break with minimum notice.)
5 
6 	History:
7 		Originally written March 2019 with read support.
8 
9 		Render support added December 28, 2020.
10 
11 		Write support added February 27, 2021.
12 +/
13 module arsd.apng;
14 
15 /// Demo creating one from scratch
16 unittest {
17 	import arsd.apng;
18 
19 	void main() {
20 		auto apng = new ApngAnimation(50, 50);
21 
22 		auto frame = apng.addFrame(25, 25);
23 		frame.data[] = 255;
24 
25 		frame = apng.addFrame(25, 25);
26 		frame.data[] = 255;
27 		frame.frameControlChunk.delay_num = 10;
28 
29 		frame = apng.addFrame(25, 25);
30 		frame.data[] = 255;
31 		frame.frameControlChunk.x_offset = 25;
32 		frame.frameControlChunk.delay_num = 10;
33 
34 		frame = apng.addFrame(25, 25);
35 		frame.data[] = 255;
36 		frame.frameControlChunk.y_offset = 25;
37 		frame.frameControlChunk.delay_num = 10;
38 
39 		frame = apng.addFrame(25, 25);
40 		frame.data[] = 255;
41 		frame.frameControlChunk.x_offset = 25;
42 		frame.frameControlChunk.y_offset = 25;
43 		frame.frameControlChunk.delay_num = 10;
44 
45 
46 		writeApngToFile(apng, "/home/me/test.apng");
47 	}
48 
49 	version(Demo) main(); // exclude from docs
50 }
51 
52 /// Demo reading and rendering
53 unittest {
54 	import arsd.simpledisplay;
55 	import arsd.game;
56 	import arsd.apng;
57 
58 	void main(string[] args) {
59 		import std.file;
60 		auto a = readApng(cast(ubyte[]) std.file.read(args[1]));
61 
62 		auto window = create2dWindow("Animated PNG viewer", a.header.width, a.header.height);
63 
64 		auto render = a.renderer();
65 		OpenGlTexture[] frames;
66 		int[] waits;
67 		foreach(frame; a.frames) {
68 			waits ~= render.nextFrame();
69 			// this would be the raw data for the frame
70 			//frames ~= new OpenGlTexture(frame.frameData.getAsTrueColorImage);
71 			// or the current rendered ersion
72 			frames ~= new OpenGlTexture(render.buffer);
73 		}
74 
75 		int pos;
76 		int currentWait;
77 
78 		void update() {
79 			currentWait += waits[pos];
80 			pos++;
81 			if(pos == frames.length)
82 				pos = 0;
83 		}
84 
85 		window.redrawOpenGlScene = () {
86 			glClear(GL_COLOR_BUFFER_BIT);
87 			frames[pos].draw(0, 0);
88 		};
89 
90 		auto tick = 50;
91 		window.eventLoop(tick, delegate() {
92 			currentWait -= tick;
93 			auto updateNeeded = currentWait <= 0;
94 			while(currentWait <= 0)
95 				update();
96 			if(updateNeeded)
97 				window.redrawOpenGlSceneNow();
98 		//},
99 		//(KeyEvent ev) {
100 		//if(ev.pressed)
101 		});
102 
103 		// writeApngToFile(a, "/home/me/test.apng");
104 	}
105 
106 	version(Demo) main(["", "/home/me/test.apng"]); // exclude from docs
107 	//version(Demo) main(["", "/home/me/small-clouds.png"]); // exclude from docs
108 }
109 
110 import arsd.png;
111 
112 // must be in the file before the IDAT
113 /// acTL chunk direct representation
114 struct AnimationControlChunk {
115 	uint num_frames;
116 	uint num_plays;
117 
118 	/// Adds it to a chunk payload buffer, returning the slice of `buffer` actually used
119 	/// Used internally by the [writeApngToFile] family of functions.
120 	ubyte[] toChunkPayload(ubyte[] buffer)
121 		in { assert(buffer.length >= 8); }
122 	do {
123 		int offset = 0;
124 		buffer[offset++] = (num_frames >> 24) & 0xff;
125 		buffer[offset++] = (num_frames >> 16) & 0xff;
126 		buffer[offset++] = (num_frames >>  8) & 0xff;
127 		buffer[offset++] = (num_frames >>  0) & 0xff;
128 
129 		buffer[offset++] = (num_plays >> 24) & 0xff;
130 		buffer[offset++] = (num_plays >> 16) & 0xff;
131 		buffer[offset++] = (num_plays >>  8) & 0xff;
132 		buffer[offset++] = (num_plays >>  0) & 0xff;
133 
134 		return buffer[0 .. offset];
135 	}
136 }
137 
138 /// fcTL chunk direct representation
139 struct FrameControlChunk {
140 	align(1):
141 	// this should go up each time, for frame control AND for frame data, each increases.
142 	uint sequence_number;
143 	uint width;
144 	uint height;
145 	uint x_offset;
146 	uint y_offset;
147 	ushort delay_num;
148 	ushort delay_den;
149 	APNG_DISPOSE_OP dispose_op;
150 	APNG_BLEND_OP blend_op;
151 
152 	static assert(dispose_op.offsetof == 24);
153 	static assert(blend_op.offsetof == 25);
154 
155 	ubyte[] toChunkPayload(int sequenceNumber, ubyte[] buffer)
156 		in { assert(buffer.length >= typeof(this).sizeof); }
157 	do {
158 		int offset = 0;
159 
160 		sequence_number = sequenceNumber;
161 
162 		buffer[offset++] = (sequence_number >> 24) & 0xff;
163 		buffer[offset++] = (sequence_number >> 16) & 0xff;
164 		buffer[offset++] = (sequence_number >>  8) & 0xff;
165 		buffer[offset++] = (sequence_number >>  0) & 0xff;
166 
167 		buffer[offset++] = (width >> 24) & 0xff;
168 		buffer[offset++] = (width >> 16) & 0xff;
169 		buffer[offset++] = (width >>  8) & 0xff;
170 		buffer[offset++] = (width >>  0) & 0xff;
171 
172 		buffer[offset++] = (height >> 24) & 0xff;
173 		buffer[offset++] = (height >> 16) & 0xff;
174 		buffer[offset++] = (height >>  8) & 0xff;
175 		buffer[offset++] = (height >>  0) & 0xff;
176 
177 		buffer[offset++] = (x_offset >> 24) & 0xff;
178 		buffer[offset++] = (x_offset >> 16) & 0xff;
179 		buffer[offset++] = (x_offset >>  8) & 0xff;
180 		buffer[offset++] = (x_offset >>  0) & 0xff;
181 
182 		buffer[offset++] = (y_offset >> 24) & 0xff;
183 		buffer[offset++] = (y_offset >> 16) & 0xff;
184 		buffer[offset++] = (y_offset >>  8) & 0xff;
185 		buffer[offset++] = (y_offset >>  0) & 0xff;
186 
187 		buffer[offset++] = (delay_num >>  8) & 0xff;
188 		buffer[offset++] = (delay_num >>  0) & 0xff;
189 
190 		buffer[offset++] = (delay_den >>  8) & 0xff;
191 		buffer[offset++] = (delay_den >>  0) & 0xff;
192 
193 		buffer[offset++] = cast(ubyte) dispose_op;
194 		buffer[offset++] = cast(ubyte) blend_op;
195 
196 		return buffer[0 .. offset];
197 	}
198 }
199 
200 /++
201 	Represents a single frame from the file, directly corresponding to the fcTL and fdAT data from the file.
202 +/
203 class ApngFrame {
204 
205 	ApngAnimation parent;
206 
207 	this(ApngAnimation parent) {
208 		this.parent = parent;
209 	}
210 
211 	this(ApngAnimation parent, int width, int height) {
212 		this.parent = parent;
213 		frameControlChunk.width = width;
214 		frameControlChunk.height = height;
215 
216 		if(parent.header.type == 3) { // FIXME: other types?!
217 			auto ii = new IndexedImage(width, height);
218 			ii.palette = parent.palette;
219 			frameData = ii;
220 			data = ii.data;
221 		} else {
222 			auto tci = new TrueColorImage(width, height);
223 			frameData = tci;
224 			data = tci.imageData.bytes;
225 		}
226 	}
227 
228 	void resyncData() {
229 		if(frameData is null)
230 			populateData();
231 
232 		assert(frameData !is null);
233 		assert(frameData.width == frameControlChunk.width);
234 		assert(frameData.height == frameControlChunk.height);
235 
236 		if(auto tci = cast(TrueColorImage) frameData) {
237 			data = tci.imageData.bytes;
238 			assert(parent.header.type == 6);
239 		} else if(auto ii = cast(IndexedImage) frameData) {
240 			data = ii.data;
241 			assert(parent.header.type == 3);
242 			assert(ii.palette == parent.palette);
243 		}
244 	}
245 
246 	/++
247 		You're allowed to edit these values but remember it is your responsibility to keep
248 		it consistent with the rest of the file (at least for now, I might change this in the future).
249 	+/
250 	FrameControlChunk frameControlChunk;
251 
252 	private ubyte[] compressedDatastream; /// Raw datastream from the file.
253 
254 	/++
255 		A reference to frameData's bytes. May be 8 bit if indexed or 32 bit rgba if not.
256 
257 		Do not replace this reference but you may edit the content.
258 	+/
259 	ubyte[] data;
260 
261 	/++
262 		Processed frame data as an image. only set after you call populateData.
263 
264 		You are allowed to edit the bytes on this but don't change the width/height or palette. Also don't replace the object.
265 
266 		This also means `getAsTrueColorImage` is not that useful, instead cast to [IndexedImage] or [TrueColorImage] depending
267 		on your type.
268 	+/
269 	MemoryImage frameData;
270 	/++
271 		Loads the raw [compressedDatastream] into raw uncompressed [data] and processed [frameData]
272 	+/
273 	void populateData() {
274 		if(data !is null)
275 			return;
276 
277 		import std.zlib;
278 
279 		auto raw = cast(ubyte[]) uncompress(compressedDatastream);
280 		auto bpp = bytesPerPixel(parent.header);
281 
282 		auto width = frameControlChunk.width;
283 		auto height = frameControlChunk.height;
284 
285 		auto bytesPerLine = bytesPerLineOfPng(parent.header.depth, parent.header.type, width);
286 		bytesPerLine--; // removing filter byte from this calculation since we handle separately
287 
288 		size_t idataIdx;
289 		ubyte[] idata;
290 
291 		MemoryImage img;
292 		if(parent.header.type == 3) {
293 			auto i = new IndexedImage(width, height);
294 			img = i;
295 			i.palette = parent.palette;
296 			idata = i.data;
297 		} else { // FIXME: other types?!
298 			auto i = new TrueColorImage(width, height);
299 			img = i;
300 			idata = i.imageData.bytes;
301 		}
302 
303 		immutable(ubyte)[] previousLine;
304 		foreach(y; 0 .. height) {
305 			auto filter = raw[0];
306 			raw = raw[1 .. $];
307 			auto line = raw[0 .. bytesPerLine];
308 			raw = raw[bytesPerLine .. $];
309 
310 			auto unfiltered = unfilter(filter, line, previousLine, bpp);
311 			previousLine = unfiltered;
312 
313 			convertPngData(parent.header.type, parent.header.depth, unfiltered, width, idata, idataIdx);
314 		}
315 
316 		this.data = idata;
317 		this.frameData = img;
318 	}
319 }
320 
321 /++
322 
323 +/
324 struct ApngRenderBuffer {
325 	/// Load this yourself
326 	ApngAnimation animation;
327 
328 	/// Then these are populated when you call [nextFrame]
329 	public TrueColorImage buffer;
330 	/// ditto
331 	public int frameNumber;
332 
333 	private FrameControlChunk prevFcc;
334 	private TrueColorImage[] convertedFrames;
335 	private TrueColorImage previousFrame;
336 
337 	/++
338 		Returns number of millisecond to wait until the next frame and populates [buffer] and [frameNumber].
339 	+/
340 	int nextFrame() {
341 		if(frameNumber == animation.frames.length) {
342 			frameNumber = 0;
343 			prevFcc = FrameControlChunk.init;
344 		}
345 
346 		auto frame = animation.frames[frameNumber];
347 		auto fcc = frame.frameControlChunk;
348 		if(convertedFrames is null) {
349 			convertedFrames = new TrueColorImage[](animation.frames.length);
350 		}
351 		if(convertedFrames[frameNumber] is null) {
352 			frame.populateData();
353 			convertedFrames[frameNumber] = frame.frameData.getAsTrueColorImage();
354 		}
355 
356 		final switch(prevFcc.dispose_op) {
357 			case APNG_DISPOSE_OP.NONE:
358 				break;
359 			case APNG_DISPOSE_OP.BACKGROUND:
360 				// clear area to 0
361 				foreach(y; prevFcc.y_offset .. prevFcc.y_offset + prevFcc.height)
362 					buffer.imageData.bytes[
363 						4 * (prevFcc.x_offset + y * buffer.width)
364 						..
365 						4 * (prevFcc.x_offset + prevFcc.width + y * buffer.width)
366 					] = 0;
367 				break;
368 			case APNG_DISPOSE_OP.PREVIOUS:
369 				// put the buffer back in
370 
371 				// this could prolly be more efficient, it only really cares about the prevFcc bounding box
372 				buffer.imageData.bytes[] = previousFrame.imageData.bytes[];
373 				break;
374 		}
375 
376 		prevFcc = fcc;
377 		// should copy the buffer at this point for a PREVIOUS case happening
378 		if(fcc.dispose_op == APNG_DISPOSE_OP.PREVIOUS) {
379 			// this could prolly be more efficient, it only really cares about the prevFcc bounding box
380 			if(previousFrame is null){
381 				previousFrame = buffer.clone();
382 			} else {
383 				previousFrame.imageData.bytes[] = buffer.imageData.bytes[];
384 			}
385 		}
386 
387 		size_t foff;
388 		foreach(y; fcc.y_offset .. fcc.y_offset + fcc.height) {
389 			final switch(fcc.blend_op) {
390 				case APNG_BLEND_OP.SOURCE:
391 					buffer.imageData.bytes[
392 						4 * (fcc.x_offset + y * buffer.width)
393 						..
394 						4 * (fcc.x_offset + y * buffer.width + fcc.width)
395 					] = convertedFrames[frameNumber].imageData.bytes[foff .. foff + fcc.width * 4];
396 					foff += fcc.width * 4;
397 				break;
398 				case APNG_BLEND_OP.OVER:
399 					foreach(x; fcc.x_offset .. fcc.x_offset + fcc.width) {
400 						buffer.imageData.colors[y * buffer.width + x] =
401 							alphaBlend(
402 								convertedFrames[frameNumber].imageData.colors[foff],
403 								buffer.imageData.colors[y * buffer.width + x]
404 							);
405 						foff++;
406 					}
407 				break;
408 			}
409 		}
410 
411 		frameNumber++;
412 
413 		if(fcc.delay_den == 0)
414 			return fcc.delay_num * 1000 / 100;
415 		else
416 			return fcc.delay_num * 1000 / fcc.delay_den;
417 	}
418 }
419 
420 /+
421 
422 +/
423 class ApngAnimation {
424 	PngHeader header;
425 	AnimationControlChunk acc;
426 	Color[] palette;
427 	ApngFrame[] frames;
428 	// default image? tho i can just load it as a png for that too.
429 
430 	/// This is an uninitialized thing, you're responsible for filling in all data yourself. You probably don't want this.
431 	this() {
432 
433 	}
434 
435 	/++
436 		If palette is null, it is a true color image. If it has data, it is indexed.
437 	+/
438 	this(int width, int height, Color[] palette = null) {
439 		header.type = (palette !is null) ? 3 : 6;
440 		header.width = width;
441 		header.height = height;
442 
443 		this.palette = palette;
444 	}
445 
446 	/++
447 		Adds a frame with the given size and returns the object. You can change other values in the frameControlChunk on it
448 		and get the data bytes out of there.
449 	+/
450 	ApngFrame addFrame(int width, int height) {
451 		assert(width <= header.width);
452 		assert(height <= header.height);
453 		auto f = new ApngFrame(this, width, height);
454 		frames ~= f;
455 		acc.num_frames++;
456 		return f;
457 	}
458 
459 	// call before writing or trying to render again
460 	void resyncData() {
461 		acc.num_frames = cast(int) frames.length;
462 		foreach(frame; frames)
463 			frame.resyncData();
464 	}
465 
466 	///
467 	ApngRenderBuffer renderer() {
468 		return ApngRenderBuffer(this, new TrueColorImage(header.width, header.height), 0);
469 	}
470 }
471 
472 ///
473 enum APNG_DISPOSE_OP : byte {
474 	NONE = 0, ///
475 	BACKGROUND = 1, ///
476 	PREVIOUS = 2 ///
477 }
478 
479 ///
480 enum APNG_BLEND_OP : byte {
481 	SOURCE = 0, ///
482 	OVER = 1 ///
483 }
484 
485 /++
486 	Loads an apng file.
487 
488 	Params:
489 		data = the raw data bytes of the file
490 		strictApng = if true, it will strictly interpret
491 		the file as apng and ignore the default image. If there
492 		are no animation chunks, it will return an empty ApngAnimation
493 		object.
494 
495 		If false, it will use the default image as the first
496 		(and only) frame of animation if there are no apng chunks.
497 
498 	History:
499 		Parameter `strictApng` added February 27, 2021
500 +/
501 ApngAnimation readApng(in ubyte[] data, bool strictApng = false) {
502 	auto png = readPng(data);
503 	auto header = PngHeader.fromChunk(png.chunks[0]);
504 
505 	auto obj = new ApngAnimation();
506 	obj.header = header;
507 
508 	if(header.type == 3) {
509 		obj.palette = fetchPalette(png);
510 	}
511 
512 	bool seenIdat = false;
513 	bool seenFctl = false;
514 
515 	int frameNumber;
516 	int expectedSequenceNumber = 0;
517 
518 	bool seenacTL = false;
519 
520 	foreach(chunk; png.chunks) {
521 		switch(chunk.stype) {
522 			case "IDAT":
523 
524 				if(!seenacTL && !strictApng) {
525 					// acTL chunks must appear before IDAT per spec,
526 					// so if there isn't one by now, it isn't an apng file.
527 					// but unless we care about strictApng, we can salvage
528 					// by making some dummy data.
529 
530 					{
531 						AnimationControlChunk c;
532 						c.num_frames = 1;
533 						c.num_plays = 1;
534 
535 						obj.acc = c;
536 						obj.frames = new ApngFrame[](c.num_frames);
537 
538 						seenacTL = true;
539 					}
540 
541 					{
542 						FrameControlChunk c;
543 						c.sequence_number = 1;
544 						c.width = header.width;
545 						c.height = header.height;
546 						c.x_offset = 0;
547 						c.y_offset = 0;
548 						c.delay_num = short.max;
549 						c.delay_den = 1;
550 						c.dispose_op = APNG_DISPOSE_OP.NONE;
551 						c.blend_op = APNG_BLEND_OP.SOURCE;
552 
553 						seenFctl = true;
554 
555 						// not increasing expectedSequenceNumber since if something is present, this is malformed!
556 
557 						if(obj.frames[frameNumber] is null)
558 							obj.frames[frameNumber] = new ApngFrame(obj);
559 						obj.frames[frameNumber].frameControlChunk = c;
560 
561 						frameNumber++;
562 					}
563 				}
564 
565 
566 				seenIdat = true;
567 				// all I care about here are animation frames,
568 				// so if this isn't after a control chunk, I'm
569 				// just going to ignore it. Read the file with
570 				// readPng if you want that.
571 				if(!seenFctl)
572 					continue;
573 
574 				assert(frameNumber == 1); // we work on frame 0 but fcTL advances it
575 				assert(obj.frames[0]);
576 
577 				obj.frames[0].compressedDatastream ~= chunk.payload;
578 			break;
579 			case "acTL":
580 				AnimationControlChunk c;
581 				int offset = 0;
582 				c.num_frames |= chunk.payload[offset++] << 24;
583 				c.num_frames |= chunk.payload[offset++] << 16;
584 				c.num_frames |= chunk.payload[offset++] <<  8;
585 				c.num_frames |= chunk.payload[offset++] <<  0;
586 
587 				c.num_plays |= chunk.payload[offset++] << 24;
588 				c.num_plays |= chunk.payload[offset++] << 16;
589 				c.num_plays |= chunk.payload[offset++] <<  8;
590 				c.num_plays |= chunk.payload[offset++] <<  0;
591 
592 				assert(offset == chunk.payload.length);
593 
594 				obj.acc = c;
595 				obj.frames = new ApngFrame[](c.num_frames);
596 
597 				seenacTL = true;
598 			break;
599 			case "fcTL":
600 				FrameControlChunk c;
601 				int offset = 0;
602 
603 				seenFctl = true;
604 
605 				c.sequence_number |= chunk.payload[offset++] << 24;
606 				c.sequence_number |= chunk.payload[offset++] << 16;
607 				c.sequence_number |= chunk.payload[offset++] <<  8;
608 				c.sequence_number |= chunk.payload[offset++] <<  0;
609 
610 				c.width |= chunk.payload[offset++] << 24;
611 				c.width |= chunk.payload[offset++] << 16;
612 				c.width |= chunk.payload[offset++] <<  8;
613 				c.width |= chunk.payload[offset++] <<  0;
614 
615 				c.height |= chunk.payload[offset++] << 24;
616 				c.height |= chunk.payload[offset++] << 16;
617 				c.height |= chunk.payload[offset++] <<  8;
618 				c.height |= chunk.payload[offset++] <<  0;
619 
620 				c.x_offset |= chunk.payload[offset++] << 24;
621 				c.x_offset |= chunk.payload[offset++] << 16;
622 				c.x_offset |= chunk.payload[offset++] <<  8;
623 				c.x_offset |= chunk.payload[offset++] <<  0;
624 
625 				c.y_offset |= chunk.payload[offset++] << 24;
626 				c.y_offset |= chunk.payload[offset++] << 16;
627 				c.y_offset |= chunk.payload[offset++] <<  8;
628 				c.y_offset |= chunk.payload[offset++] <<  0;
629 
630 				c.delay_num |= chunk.payload[offset++] <<  8;
631 				c.delay_num |= chunk.payload[offset++] <<  0;
632 
633 				c.delay_den |= chunk.payload[offset++] <<  8;
634 				c.delay_den |= chunk.payload[offset++] <<  0;
635 
636 				c.dispose_op = cast(APNG_DISPOSE_OP) chunk.payload[offset++];
637 				c.blend_op = cast(APNG_BLEND_OP) chunk.payload[offset++];
638 
639 				assert(offset == chunk.payload.length);
640 
641 				import std.conv;
642 				if(expectedSequenceNumber != c.sequence_number)
643 					throw new Exception("malformed apng file expected fcTL seq " ~ to!string(expectedSequenceNumber) ~ " got " ~ to!string(c.sequence_number));
644 
645 				expectedSequenceNumber++;
646 
647 
648 				if(obj.frames[frameNumber] is null)
649 					obj.frames[frameNumber] = new ApngFrame(obj);
650 				obj.frames[frameNumber].frameControlChunk = c;
651 
652 				frameNumber++;
653 			break;
654 			case "fdAT":
655 				uint sequence_number;
656 				int offset;
657 
658 				sequence_number |= chunk.payload[offset++] << 24;
659 				sequence_number |= chunk.payload[offset++] << 16;
660 				sequence_number |= chunk.payload[offset++] <<  8;
661 				sequence_number |= chunk.payload[offset++] <<  0;
662 
663 				import std.conv;
664 				if(expectedSequenceNumber != sequence_number)
665 					throw new Exception("malformed apng file expected fdAT seq " ~ to!string(expectedSequenceNumber) ~ " got " ~ to!string(sequence_number));
666 
667 				expectedSequenceNumber++;
668 
669 				// and the rest of it is a datastream...
670 				obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $];
671 			break;
672 			default:
673 				// ignore
674 		}
675 
676 	}
677 
678 	return obj;
679 }
680 
681 
682 /++
683 
684 +/
685 void writeApngToData(ApngAnimation apng, scope void delegate(in ubyte[] data) sink) {
686 
687 	apng.resyncData();
688 
689 	PNG* p = blankPNG(apng.header);
690 	if(apng.palette.length)
691 		p.replacePalette(apng.palette);
692 
693 	// I want acTL first, then frames, then idat last.
694 
695 	ubyte[128] buffer;
696 
697 	p.chunks ~= *(Chunk.create("acTL", apng.acc.toChunkPayload(buffer[]).dup));
698 
699 	// then IDAT is required
700 	// FIXME: it might be better to just legit use the first frame but meh gotta check size and stuff too
701 	auto render = apng.renderer();
702 	render.nextFrame();
703 	auto data = render.buffer.imageData.bytes;
704 	addImageDatastreamToPng(data, p, false);
705 
706 	// then the frames
707 	int sequenceNumber = 0;
708 	foreach(frame; apng.frames) {
709 		p.chunks ~= *(Chunk.create("fcTL", frame.frameControlChunk.toChunkPayload(sequenceNumber++, buffer[]).dup));
710 		// fdAT
711 
712 		import std.zlib;
713 
714 		size_t bytesPerLine;
715 		switch(apng.header.type) {
716 			case 0:
717 				// FIXME: < 8 depth not supported here but should be
718 				bytesPerLine = cast(size_t) frame.frameControlChunk.width * 1 * apng.header.depth / 8;
719 			break;
720 			case 2:
721 				bytesPerLine = cast(size_t) frame.frameControlChunk.width * 3 * apng.header.depth / 8;
722 			break;
723 			case 3:
724 				bytesPerLine = cast(size_t) frame.frameControlChunk.width * 1 * apng.header.depth / 8;
725 			break;
726 			case 4:
727 				// FIXME: < 8 depth not supported here but should be
728 				bytesPerLine = cast(size_t) frame.frameControlChunk.width * 2 * apng.header.depth / 8;
729 			break;
730 			case 6:
731 				bytesPerLine = cast(size_t) frame.frameControlChunk.width * 4 * apng.header.depth / 8;
732 			break;
733 			default: assert(0);
734 		
735 		}
736 
737 		Chunk dat;
738 		dat.type = ['f', 'd', 'A', 'T'];
739 		size_t pos = 0;
740 
741 		const(ubyte)[] output;
742 
743 		frame.populateData();
744 
745 		while(pos+bytesPerLine <= frame.data.length) {
746 			output ~= 0;
747 			output ~= frame.data[pos..pos+bytesPerLine];
748 			pos += bytesPerLine;
749 		}
750 
751 		auto com = cast(ubyte[]) compress(output);
752 		dat.size = cast(int) com.length + 4;
753 
754 		buffer[0] = (sequenceNumber >> 24) & 0xff;
755 		buffer[1] = (sequenceNumber >> 16) & 0xff;
756 		buffer[2] = (sequenceNumber >>  8) & 0xff;
757 		buffer[3] = (sequenceNumber >>  0) & 0xff;
758 
759 		sequenceNumber++;
760 
761 
762 		dat.payload = buffer[0 .. 4] ~ com;
763 		dat.checksum = crc("fdAT", dat.payload);
764 
765 		p.chunks ~= dat;
766 	}
767 
768 	{
769 		Chunk c;
770 
771 		c.size = 0;
772 		c.type = ['I', 'E', 'N', 'D'];
773 		c.checksum = crc("IEND", c.payload);
774 		p.chunks ~= c;
775 	}
776 
777 	sink(writePng(p));
778 }
779 
780 /// ditto
781 void writeApngToFile(ApngAnimation apng, string filename) {
782 	import std.stdio;
783 	auto file = File(filename, "wb");
784 	writeApngToData(apng, delegate(in ubyte[] data) {
785 		file.rawWrite(data);
786 	});
787 }
788 
789 /// ditto
790 ubyte[] getApngBytes(ApngAnimation apng) {
791 	ubyte[] ret;
792 	writeApngToData(apng, (in ubyte[] data) { ret ~= data; });
793 	return ret;
794 }
Suggestion Box / Bug Report