From 85b6a93e47e144ffff095ef22c4527869ebf1091 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Tue, 26 Mar 2019 22:02:02 -0400 Subject: [PATCH] apng starting code --- apng.d | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++ png.d | 201 ++++++++++++++++++++++-------------------- simpleaudio.d | 2 +- 3 files changed, 343 insertions(+), 97 deletions(-) create mode 100644 apng.d diff --git a/apng.d b/apng.d new file mode 100644 index 0000000..433f4c7 --- /dev/null +++ b/apng.d @@ -0,0 +1,237 @@ +/++ + Support for [animated png|https://wiki.mozilla.org/APNG_Specification] files. ++/ +module arsd.apng; + +import arsd.png; + +// acTL +// must be in the file before the IDAT +struct AnimationControlChunk { + uint num_frames; + uint num_plays; +} + +// fcTL +struct FrameControlChunk { + align(1): + // this should go up each time, for frame control AND for frame data, each increases. + uint sequence_number; + uint width; + uint height; + uint x_offset; + uint y_offset; + ushort delay_num; + ushort delay_den; + APNG_DISPOSE_OP dispose_op; + APNG_BLEND_OP blend_op; + + static assert(dispose_op.offsetof == 24); + static assert(blend_op.offsetof == 25); +} + +// fdAT +class ApngFrame { + + ApngAnimation parent; + + this(ApngAnimation parent) { + this.parent = parent; + } + + FrameControlChunk frameControlChunk; + + ubyte[] compressedDatastream; + + ubyte[] data; + void populateData() { + if(data !is null) + return; + + import std.zlib; + + auto raw = cast(ubyte[]) uncompress(compressedDatastream); + auto bpp = bytesPerPixel(parent.header); + + auto width = frameControlChunk.width; + auto height = frameControlChunk.height; + + auto bytesPerLine = bytesPerLineOfPng(parent.header.depth, parent.header.type, width); + + int idataIdx; + ubyte[] idata; + + idata.length = width * height * (parent.header.type == 3 ? 1 : 4); + + ubyte[] previousLine; + foreach(y; 0 .. height) { + auto filter = raw[0]; + raw = raw[1 .. $]; + auto line = raw[0 .. bytesPerLine]; + raw = raw[bytesPerLine .. $]; + + auto unfiltered = unfilter(filter, line, previousLine, bpp); + previousLine = line; + + convertPngData(parent.header.type, parent.header.depth, unfiltered, width, idata, idataIdx); + } + + this.data = idata; + } + + // then need to uncompress it + // and unfilter it... + // and then convert it to the right format. + + MemoryImage frameData; +} + +class ApngAnimation { + PngHeader header; + AnimationControlChunk acc; + Color[] palette; + ApngFrame[] frames; + // default image? tho i can just load it as a png for that too. + + MemoryImage render() { + return null; + } +} + +enum APNG_DISPOSE_OP : byte { + NONE = 0, + BACKGROUND = 1, + PREVIOUS = 2 +} + +enum APNG_BLEND_OP : byte { + SOURCE = 0, + OVER = 1 +} + +void readApng(in ubyte[] data) { + auto png = readPng(data); + auto header = PngHeader.fromChunk(png.chunks[0]); + Color[] palette; + if(header.type == 3) { + palette = fetchPalette(png); + } + + auto obj = new ApngAnimation(); + + bool seenIdat = false; + bool seenFctl = false; + + int frameNumber; + int expectedSequenceNumber = 0; + + foreach(chunk; png.chunks) { + switch(chunk.stype) { + case "IDAT": + seenIdat = true; + // all I care about here are animation frames, + // so if this isn't after a control chunk, I'm + // just going to ignore it. Read the file with + // readPng if you want that. + if(!seenFctl) + continue; + + assert(obj.frames[0]); + + obj.frames[0].compressedDatastream ~= chunk.payload; + break; + case "acTL": + AnimationControlChunk c; + int offset = 0; + c.num_frames |= chunk.payload[offset++] << 24; + c.num_frames |= chunk.payload[offset++] << 16; + c.num_frames |= chunk.payload[offset++] << 8; + c.num_frames |= chunk.payload[offset++] << 0; + + c.num_plays |= chunk.payload[offset++] << 24; + c.num_plays |= chunk.payload[offset++] << 16; + c.num_plays |= chunk.payload[offset++] << 8; + c.num_plays |= chunk.payload[offset++] << 0; + + assert(offset == chunk.payload.length); + + obj.acc = c; + obj.frames = new ApngFrame[](c.num_frames); + break; + case "fcTL": + FrameControlChunk c; + int offset = 0; + + seenFctl = true; + + c.sequence_number |= chunk.payload[offset++] << 24; + c.sequence_number |= chunk.payload[offset++] << 16; + c.sequence_number |= chunk.payload[offset++] << 8; + c.sequence_number |= chunk.payload[offset++] << 0; + + c.width |= chunk.payload[offset++] << 24; + c.width |= chunk.payload[offset++] << 16; + c.width |= chunk.payload[offset++] << 8; + c.width |= chunk.payload[offset++] << 0; + + c.height |= chunk.payload[offset++] << 24; + c.height |= chunk.payload[offset++] << 16; + c.height |= chunk.payload[offset++] << 8; + c.height |= chunk.payload[offset++] << 0; + + c.x_offset |= chunk.payload[offset++] << 24; + c.x_offset |= chunk.payload[offset++] << 16; + c.x_offset |= chunk.payload[offset++] << 8; + c.x_offset |= chunk.payload[offset++] << 0; + + c.y_offset |= chunk.payload[offset++] << 24; + c.y_offset |= chunk.payload[offset++] << 16; + c.y_offset |= chunk.payload[offset++] << 8; + c.y_offset |= chunk.payload[offset++] << 0; + + c.delay_num |= chunk.payload[offset++] << 8; + c.delay_num |= chunk.payload[offset++] << 0; + + c.delay_den |= chunk.payload[offset++] << 8; + c.delay_den |= chunk.payload[offset++] << 0; + + c.dispose_op = cast(APNG_DISPOSE_OP) chunk.payload[offset++]; + c.blend_op = cast(APNG_BLEND_OP) chunk.payload[offset++]; + + assert(offset == chunk.payload.length); + + if(expectedSequenceNumber != c.sequence_number) + throw new Exception("malformed apng file"); + + expectedSequenceNumber++; + + + if(obj.frames[frameNumber] is null) + obj.frames[frameNumber] = new ApngFrame(obj); + obj.frames[frameNumber].frameControlChunk = c; + + frameNumber++; + break; + case "fdAT": + uint sequence_number; + int offset; + + sequence_number |= chunk.payload[offset++] << 24; + sequence_number |= chunk.payload[offset++] << 16; + sequence_number |= chunk.payload[offset++] << 8; + sequence_number |= chunk.payload[offset++] << 0; + + if(expectedSequenceNumber != sequence_number) + throw new Exception("malformed apng file"); + + expectedSequenceNumber++; + + // and the rest of it is a datastream... + obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload; + break; + default: + // ignore + } + + } +} diff --git a/png.d b/png.d index 4c417b6..8b618d1 100644 --- a/png.d +++ b/png.d @@ -138,88 +138,9 @@ MemoryImage imageFromPng(PNG* png) { foreach(line; file.rawDatastreamByChunk()) { auto filter = line[0]; auto data = unfilter(filter, line[1 .. $], previousLine, bpp); - ubyte consumeOne() { - ubyte ret = data[0]; - data = data[1 .. $]; - return ret; - } previousLine = data; - import std.conv; - loop: for(int pixel = 0; pixel < h.width; pixel++) - switch(h.type) { - case 0: // greyscale - case 4: // greyscale with alpha - auto value = consumeOne(); - idata[idataIdx++] = value; - idata[idataIdx++] = value; - idata[idataIdx++] = value; - idata[idataIdx++] = (h.type == 4) ? consumeOne() : 255; - break; - case 3: // indexed - auto b = consumeOne(); - switch(h.depth) { - case 1: - idata[idataIdx++] = (b >> 7) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 6) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 5) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 4) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 3) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 2) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 1) & 0x01; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = b & 0x01; - break; - case 2: - idata[idataIdx++] = (b >> 6) & 0x03; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 4) & 0x03; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = (b >> 2) & 0x03; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = b & 0x03; - break; - case 4: - idata[idataIdx++] = (b >> 4) & 0x0f; - pixel++; if(pixel == h.width) break loop; - idata[idataIdx++] = b & 0x0f; - break; - case 8: - idata[idataIdx++] = b; - break; - default: - assert(0, "bit depth not implemented"); - } - break; - case 2: // truecolor - case 6: // true with alpha - if(h.depth == 8) { - idata[idataIdx++] = consumeOne(); - idata[idataIdx++] = consumeOne(); - idata[idataIdx++] = consumeOne(); - idata[idataIdx++] = (h.type == 6) ? consumeOne() : 255; - } else if(h.depth == 16) { - idata[idataIdx++] = consumeOne(); - consumeOne(); - idata[idataIdx++] = consumeOne(); - consumeOne(); - idata[idataIdx++] = consumeOne(); - consumeOne(); - idata[idataIdx++] = (h.type == 6) ? consumeOne() : 255; - if(h.type == 6) - consumeOne(); - - } else assert(0, "unsupported truecolor bit depth " ~ to!string(h.depth)); - break; - default: assert(0); - } - assert(data.length == 0, "not all consumed, wtf " ~ to!string(h)); + convertPngData(h.type, h.depth, data, h.width, idata, idataIdx); } assert(idataIdx == idata.length, "not all filled, wtf"); @@ -228,6 +149,91 @@ MemoryImage imageFromPng(PNG* png) { return i; } +// idata needs to be already sized for the image! width * height if indexed, width*height*4 if not. +void convertPngData(ubyte type, ubyte depth, const(ubyte)[] data, int width, ubyte[] idata, ref int idataIdx) { + ubyte consumeOne() { + ubyte ret = data[0]; + data = data[1 .. $]; + return ret; + } + import std.conv; + + loop: for(int pixel = 0; pixel < width; pixel++) + switch(type) { + case 0: // greyscale + case 4: // greyscale with alpha + auto value = consumeOne(); + idata[idataIdx++] = value; + idata[idataIdx++] = value; + idata[idataIdx++] = value; + idata[idataIdx++] = (type == 4) ? consumeOne() : 255; + break; + case 3: // indexed + auto b = consumeOne(); + switch(depth) { + case 1: + idata[idataIdx++] = (b >> 7) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 6) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 5) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 4) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 3) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 2) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 1) & 0x01; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = b & 0x01; + break; + case 2: + idata[idataIdx++] = (b >> 6) & 0x03; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 4) & 0x03; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = (b >> 2) & 0x03; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = b & 0x03; + break; + case 4: + idata[idataIdx++] = (b >> 4) & 0x0f; + pixel++; if(pixel == width) break loop; + idata[idataIdx++] = b & 0x0f; + break; + case 8: + idata[idataIdx++] = b; + break; + default: + assert(0, "bit depth not implemented"); + } + break; + case 2: // truecolor + case 6: // true with alpha + if(depth == 8) { + idata[idataIdx++] = consumeOne(); + idata[idataIdx++] = consumeOne(); + idata[idataIdx++] = consumeOne(); + idata[idataIdx++] = (type == 6) ? consumeOne() : 255; + } else if(depth == 16) { + idata[idataIdx++] = consumeOne(); + consumeOne(); + idata[idataIdx++] = consumeOne(); + consumeOne(); + idata[idataIdx++] = consumeOne(); + consumeOne(); + idata[idataIdx++] = (type == 6) ? consumeOne() : 255; + if(type == 6) + consumeOne(); + + } else assert(0, "unsupported truecolor bit depth " ~ to!string(depth)); + break; + default: assert(0); + } + assert(data.length == 0, "not all consumed, wtf ");// ~ to!string(h)); +} + /* struct PngHeader { uint width; @@ -1467,29 +1473,32 @@ struct LazyPngFile(LazyPngChunksProvider) return .bytesPerPixel(header); } - // FIXME: doesn't handle interlacing... I think int bytesPerLine() { - immutable bitsPerChannel = header.depth; - - int bitsPerPixel = bitsPerChannel; - if(header.type & 2 && !(header.type & 1)) // in color, but no palette - bitsPerPixel *= 3; - if(header.type & 4) // has alpha channel - bitsPerPixel += bitsPerChannel; - - - immutable int sizeInBits = header.width * bitsPerPixel; - - // need to round up to the nearest byte - int sizeInBytes = (sizeInBits + 7) / 8; - - return sizeInBytes + 1; // the +1 is for the filter byte that precedes all lines + return .bytesPerLineOfPng(header.depth, header.type, header.width); } PngHeader header; Color[] palette; } +// FIXME: doesn't handle interlacing... I think +@nogc @safe pure nothrow +int bytesPerLineOfPng(ubyte depth, ubyte type, uint width) { + immutable bitsPerChannel = depth; + + int bitsPerPixel = bitsPerChannel; + if(type & 2 && !(type & 1)) // in color, but no palette + bitsPerPixel *= 3; + if(type & 4) // has alpha channel + bitsPerPixel += bitsPerChannel; + + immutable int sizeInBits = width * bitsPerPixel; + + // need to round up to the nearest byte + int sizeInBytes = (sizeInBits + 7) / 8; + + return sizeInBytes + 1; // the +1 is for the filter byte that precedes all lines +} /************************************************** * Buffered input range - generic, non-image code diff --git a/simpleaudio.d b/simpleaudio.d index 46ab0d0..91b7ac2 100644 --- a/simpleaudio.d +++ b/simpleaudio.d @@ -45,7 +45,7 @@ module arsd.simpleaudio; enum BUFFER_SIZE_FRAMES = 1024;//512;//2048; enum BUFFER_SIZE_SHORT = BUFFER_SIZE_FRAMES * 2; -/// +/// A reasonable default volume for an individual sample. It doesn't need to be large; in fact it needs to not be large so mixing doesn't clip too much. enum DEFAULT_VOLUME = 20; version(Demo)