1 /++
2 	Support for [animated png|https://wiki.mozilla.org/APNG_Specification] files.
3 +/
4 module arsd.apng;
5 
6 import arsd.png;
7 
8 // acTL
9 // must be in the file before the IDAT
10 struct AnimationControlChunk {
11 	uint num_frames;
12 	uint num_plays;
13 }
14 
15 // fcTL
16 struct FrameControlChunk {
17 	align(1):
18 	// this should go up each time, for frame control AND for frame data, each increases.
19 	uint sequence_number;
20 	uint width;
21 	uint height;
22 	uint x_offset;
23 	uint y_offset;
24 	ushort delay_num;
25 	ushort delay_den;
26 	APNG_DISPOSE_OP dispose_op;
27 	APNG_BLEND_OP blend_op;
28 
29 	static assert(dispose_op.offsetof == 24);
30 	static assert(blend_op.offsetof == 25);
31 }
32 
33 // fdAT
34 class ApngFrame {
35 
36 	ApngAnimation parent;
37 
38 	this(ApngAnimation parent) {
39 		this.parent = parent;
40 	}
41 
42 	FrameControlChunk frameControlChunk;
43 
44 	ubyte[] compressedDatastream;
45 
46 	ubyte[] data;
47 	void populateData() {
48 		if(data !is null)
49 			return;
50 
51 		import std.zlib;
52 
53 		auto raw = cast(ubyte[]) uncompress(compressedDatastream);
54 		auto bpp = bytesPerPixel(parent.header);
55 
56 		auto width = frameControlChunk.width;
57 		auto height = frameControlChunk.height;
58 
59 		auto bytesPerLine = bytesPerLineOfPng(parent.header.depth, parent.header.type, width);
60 		bytesPerLine--; // removing filter byte from this calculation since we handle separtely
61 
62 		size_t idataIdx;
63 		ubyte[] idata;
64 
65 		idata.length = width * height * (parent.header.type == 3 ? 1 : 4);
66 
67 		ubyte[] previousLine;
68 		foreach(y; 0 .. height) {
69 			auto filter = raw[0];
70 			raw = raw[1 .. $];
71 			auto line = raw[0 .. bytesPerLine];
72 			raw = raw[bytesPerLine .. $];
73 
74 			auto unfiltered = unfilter(filter, line, previousLine, bpp);
75 			previousLine = line;
76 
77 			convertPngData(parent.header.type, parent.header.depth, unfiltered, width, idata, idataIdx);
78 		}
79 
80 		this.data = idata;
81 	}
82 
83 	//MemoryImage frameData;
84 }
85 
86 class ApngAnimation {
87 	PngHeader header;
88 	AnimationControlChunk acc;
89 	Color[] palette;
90 	ApngFrame[] frames;
91 	// default image? tho i can just load it as a png for that too.
92 
93 	MemoryImage render() {
94 		return null;
95 	}
96 }
97 
98 enum APNG_DISPOSE_OP : byte {
99 	NONE = 0,
100 	BACKGROUND = 1,
101 	PREVIOUS = 2
102 }
103 
104 enum APNG_BLEND_OP : byte {
105 	SOURCE = 0,
106 	OVER = 1
107 }
108 
109 ApngAnimation readApng(in ubyte[] data) {
110 	auto png = readPng(data);
111 	auto header = PngHeader.fromChunk(png.chunks[0]);
112 
113 	auto obj = new ApngAnimation();
114 
115 	if(header.type == 3) {
116 		obj.palette = fetchPalette(png);
117 	}
118 
119 	bool seenIdat = false;
120 	bool seenFctl = false;
121 
122 	int frameNumber;
123 	int expectedSequenceNumber = 0;
124 
125 	foreach(chunk; png.chunks) {
126 		switch(chunk.stype) {
127 			case "IDAT":
128 				seenIdat = true;
129 				// all I care about here are animation frames,
130 				// so if this isn't after a control chunk, I'm
131 				// just going to ignore it. Read the file with
132 				// readPng if you want that.
133 				if(!seenFctl)
134 					continue;
135 
136 				assert(obj.frames[0]);
137 
138 				obj.frames[0].compressedDatastream ~= chunk.payload;
139 			break;
140 			case "acTL":
141 				AnimationControlChunk c;
142 				int offset = 0;
143 				c.num_frames |= chunk.payload[offset++] << 24;
144 				c.num_frames |= chunk.payload[offset++] << 16;
145 				c.num_frames |= chunk.payload[offset++] <<  8;
146 				c.num_frames |= chunk.payload[offset++] <<  0;
147 
148 				c.num_plays |= chunk.payload[offset++] << 24;
149 				c.num_plays |= chunk.payload[offset++] << 16;
150 				c.num_plays |= chunk.payload[offset++] <<  8;
151 				c.num_plays |= chunk.payload[offset++] <<  0;
152 
153 				assert(offset == chunk.payload.length);
154 
155 				obj.acc = c;
156 				obj.frames = new ApngFrame[](c.num_frames);
157 			break;
158 			case "fcTL":
159 				FrameControlChunk c;
160 				int offset = 0;
161 
162 				seenFctl = true;
163 
164 				c.sequence_number |= chunk.payload[offset++] << 24;
165 				c.sequence_number |= chunk.payload[offset++] << 16;
166 				c.sequence_number |= chunk.payload[offset++] <<  8;
167 				c.sequence_number |= chunk.payload[offset++] <<  0;
168 
169 				c.width |= chunk.payload[offset++] << 24;
170 				c.width |= chunk.payload[offset++] << 16;
171 				c.width |= chunk.payload[offset++] <<  8;
172 				c.width |= chunk.payload[offset++] <<  0;
173 
174 				c.height |= chunk.payload[offset++] << 24;
175 				c.height |= chunk.payload[offset++] << 16;
176 				c.height |= chunk.payload[offset++] <<  8;
177 				c.height |= chunk.payload[offset++] <<  0;
178 
179 				c.x_offset |= chunk.payload[offset++] << 24;
180 				c.x_offset |= chunk.payload[offset++] << 16;
181 				c.x_offset |= chunk.payload[offset++] <<  8;
182 				c.x_offset |= chunk.payload[offset++] <<  0;
183 
184 				c.y_offset |= chunk.payload[offset++] << 24;
185 				c.y_offset |= chunk.payload[offset++] << 16;
186 				c.y_offset |= chunk.payload[offset++] <<  8;
187 				c.y_offset |= chunk.payload[offset++] <<  0;
188 
189 				c.delay_num |= chunk.payload[offset++] <<  8;
190 				c.delay_num |= chunk.payload[offset++] <<  0;
191 
192 				c.delay_den |= chunk.payload[offset++] <<  8;
193 				c.delay_den |= chunk.payload[offset++] <<  0;
194 
195 				c.dispose_op = cast(APNG_DISPOSE_OP) chunk.payload[offset++];
196 				c.blend_op = cast(APNG_BLEND_OP) chunk.payload[offset++];
197 
198 				assert(offset == chunk.payload.length);
199 
200 				if(expectedSequenceNumber != c.sequence_number)
201 					throw new Exception("malformed apng file");
202 
203 				expectedSequenceNumber++;
204 
205 
206 				if(obj.frames[frameNumber] is null)
207 					obj.frames[frameNumber] = new ApngFrame(obj);
208 				obj.frames[frameNumber].frameControlChunk = c;
209 
210 				frameNumber++;
211 			break;
212 			case "fdAT":
213 				uint sequence_number;
214 				int offset;
215 
216 				sequence_number |= chunk.payload[offset++] << 24;
217 				sequence_number |= chunk.payload[offset++] << 16;
218 				sequence_number |= chunk.payload[offset++] <<  8;
219 				sequence_number |= chunk.payload[offset++] <<  0;
220 
221 				if(expectedSequenceNumber != sequence_number)
222 					throw new Exception("malformed apng file");
223 
224 				expectedSequenceNumber++;
225 
226 				// and the rest of it is a datastream...
227 				obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $];
228 			break;
229 			default:
230 				// ignore
231 		}
232 
233 	}
234 
235 	return obj;
236 }