mirror of https://github.com/adamdruppe/arsd.git
apng starting code
This commit is contained in:
parent
68e67279f2
commit
85b6a93e47
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
201
png.d
201
png.d
|
@ -138,88 +138,9 @@ MemoryImage imageFromPng(PNG* png) {
|
||||||
foreach(line; file.rawDatastreamByChunk()) {
|
foreach(line; file.rawDatastreamByChunk()) {
|
||||||
auto filter = line[0];
|
auto filter = line[0];
|
||||||
auto data = unfilter(filter, line[1 .. $], previousLine, bpp);
|
auto data = unfilter(filter, line[1 .. $], previousLine, bpp);
|
||||||
ubyte consumeOne() {
|
|
||||||
ubyte ret = data[0];
|
|
||||||
data = data[1 .. $];
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
previousLine = data;
|
previousLine = data;
|
||||||
import std.conv;
|
|
||||||
|
|
||||||
loop: for(int pixel = 0; pixel < h.width; pixel++)
|
convertPngData(h.type, h.depth, data, h.width, idata, idataIdx);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
assert(idataIdx == idata.length, "not all filled, wtf");
|
assert(idataIdx == idata.length, "not all filled, wtf");
|
||||||
|
|
||||||
|
@ -228,6 +149,91 @@ MemoryImage imageFromPng(PNG* png) {
|
||||||
return i;
|
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 {
|
struct PngHeader {
|
||||||
uint width;
|
uint width;
|
||||||
|
@ -1467,29 +1473,32 @@ struct LazyPngFile(LazyPngChunksProvider)
|
||||||
return .bytesPerPixel(header);
|
return .bytesPerPixel(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: doesn't handle interlacing... I think
|
|
||||||
int bytesPerLine() {
|
int bytesPerLine() {
|
||||||
immutable bitsPerChannel = header.depth;
|
return .bytesPerLineOfPng(header.depth, header.type, header.width);
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PngHeader header;
|
PngHeader header;
|
||||||
Color[] palette;
|
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
|
* Buffered input range - generic, non-image code
|
||||||
|
|
|
@ -45,7 +45,7 @@ module arsd.simpleaudio;
|
||||||
enum BUFFER_SIZE_FRAMES = 1024;//512;//2048;
|
enum BUFFER_SIZE_FRAMES = 1024;//512;//2048;
|
||||||
enum BUFFER_SIZE_SHORT = BUFFER_SIZE_FRAMES * 2;
|
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;
|
enum DEFAULT_VOLUME = 20;
|
||||||
|
|
||||||
version(Demo)
|
version(Demo)
|
||||||
|
|
Loading…
Reference in New Issue