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 }