mirror of https://github.com/buggins/dlangui.git
852 lines
25 KiB
D
852 lines
25 KiB
D
/*
|
|
Copyright (c) 2011-2015 Timur Gafarov, Martin Cejp
|
|
|
|
Boost Software License - Version 1.0 - August 17th, 2003
|
|
|
|
Permission is hereby granted, free of charge, to any person or organization
|
|
obtaining a copy of the software and accompanying documentation covered by
|
|
this license (the "Software") to use, reproduce, display, distribute,
|
|
execute, and transmit the Software, and to prepare derivative works of the
|
|
Software, and to permit third-parties to whom the Software is furnished to
|
|
do so, all subject to the following:
|
|
|
|
The copyright notices in the Software and this entire statement, including
|
|
the above license grant, this restriction and the following disclaimer,
|
|
must be included in all copies of the Software, in whole or in part, and
|
|
all derivative works of the Software, unless such copies or derivative
|
|
works are solely in the form of machine-executable object code generated by
|
|
a source language processor.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
// dimage is actually stripped out part of dlib - just to support reading PNG and JPEG
|
|
module dimage.png; //dlib.image.io.png
|
|
|
|
private
|
|
{
|
|
import std.stdio;
|
|
import std.math;
|
|
import std.string;
|
|
import std.range;
|
|
|
|
import dimage.memory;
|
|
import dimage.stream;
|
|
import dimage.compound;
|
|
//import dlib.filesystem.local;
|
|
//import dlib.math.utils;
|
|
import dimage.zlib;
|
|
import dimage.image;
|
|
//import dlib.image.io.io;
|
|
|
|
//import dlib.core.memory;
|
|
//import dlib.core.stream;
|
|
//import dlib.core.compound;
|
|
//import dlib.filesystem.local;
|
|
//import dlib.math.utils;
|
|
//import dlib.coding.zlib;
|
|
//import dlib.image.image;
|
|
//import dlib.image.io.io;
|
|
}
|
|
|
|
// uncomment this to see debug messages:
|
|
//version = PNGDebug;
|
|
|
|
static const ubyte[8] PNGSignature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
static const ubyte[4] IHDR = ['I', 'H', 'D', 'R'];
|
|
static const ubyte[4] IEND = ['I', 'E', 'N', 'D'];
|
|
static const ubyte[4] IDAT = ['I', 'D', 'A', 'T'];
|
|
static const ubyte[4] PLTE = ['P', 'L', 'T', 'E'];
|
|
static const ubyte[4] tRNS = ['t', 'R', 'N', 'S'];
|
|
static const ubyte[4] bKGD = ['b', 'K', 'G', 'D'];
|
|
static const ubyte[4] tEXt = ['t', 'E', 'X', 't'];
|
|
static const ubyte[4] iTXt = ['i', 'T', 'X', 't'];
|
|
static const ubyte[4] zTXt = ['z', 'T', 'X', 't'];
|
|
|
|
enum ColorType: ubyte
|
|
{
|
|
Greyscale = 0, // allowed bit depths: 1, 2, 4, 8 and 16
|
|
RGB = 2, // allowed bit depths: 8 and 16
|
|
Palette = 3, // allowed bit depths: 1, 2, 4 and 8
|
|
GreyscaleAlpha = 4, // allowed bit depths: 8 and 16
|
|
RGBA = 6, // allowed bit depths: 8 and 16
|
|
Any = 7 // one of the above
|
|
}
|
|
|
|
enum FilterMethod: ubyte
|
|
{
|
|
None = 0,
|
|
Sub = 1,
|
|
Up = 2,
|
|
Average = 3,
|
|
Paeth = 4
|
|
}
|
|
|
|
struct PNGChunk
|
|
{
|
|
uint length;
|
|
ubyte[4] type;
|
|
ubyte[] data;
|
|
uint crc;
|
|
|
|
void free()
|
|
{
|
|
if (data.ptr)
|
|
Delete(data);
|
|
}
|
|
}
|
|
|
|
struct PNGHeader
|
|
{
|
|
union
|
|
{
|
|
struct
|
|
{
|
|
uint width;
|
|
uint height;
|
|
ubyte bitDepth;
|
|
ubyte colorType;
|
|
ubyte compressionMethod;
|
|
ubyte filterMethod;
|
|
ubyte interlaceMethod;
|
|
};
|
|
ubyte[13] bytes;
|
|
}
|
|
}
|
|
class PNGLoadException: ImageLoadException
|
|
{
|
|
this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
|
|
{
|
|
super(msg, file, line, next);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Load PNG from file using local FileSystem.
|
|
* Causes GC allocation
|
|
*/
|
|
//SuperImage loadPNG(string filename)
|
|
//{
|
|
// InputStream input = openForInput(filename);
|
|
// auto img = loadPNG(input);
|
|
// input.close();
|
|
// return img;
|
|
//}
|
|
|
|
/*
|
|
* Save PNG to file using local FileSystem.
|
|
* Causes GC allocation
|
|
*/
|
|
//void savePNG(SuperImage img, string filename)
|
|
//{
|
|
// OutputStream output = openForOutput(filename);
|
|
// Compound!(bool, string) res =
|
|
// savePNG(img, output);
|
|
// output.close();
|
|
//
|
|
// if (!res[0])
|
|
// throw new PNGLoadException(res[1]);
|
|
//}
|
|
|
|
/*
|
|
* Load PNG from stream using default image factory.
|
|
* Causes GC allocation
|
|
*/
|
|
SuperImage loadPNG(InputStream istrm)
|
|
{
|
|
Compound!(SuperImage, string) res =
|
|
loadPNG(istrm, defaultImageFactory);
|
|
if (res[0] is null)
|
|
throw new PNGLoadException(res[1]);
|
|
else
|
|
return res[0];
|
|
}
|
|
|
|
/*
|
|
* Load PNG from stream using specified image factory.
|
|
* GC-free
|
|
*/
|
|
Compound!(SuperImage, string) loadPNG(
|
|
InputStream istrm,
|
|
SuperImageFactory imgFac)
|
|
{
|
|
SuperImage img = null;
|
|
|
|
Compound!(SuperImage, string) error(string errorMsg)
|
|
{
|
|
if (img)
|
|
{
|
|
img.free();
|
|
img = null;
|
|
}
|
|
return compound(img, errorMsg);
|
|
}
|
|
|
|
bool readChunk(PNGChunk* chunk)
|
|
{
|
|
if (!istrm.readBE!uint(&chunk.length)
|
|
|| !istrm.fillArray(chunk.type))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
version(PNGDebug) writefln("Chunk length = %s", chunk.length);
|
|
version(PNGDebug) writefln("Chunk type = %s", cast(char[])chunk.type);
|
|
|
|
if (chunk.length > 0)
|
|
{
|
|
chunk.data = New!(ubyte[])(chunk.length);
|
|
|
|
if (!istrm.fillArray(chunk.data))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
version(PNGDebug) writefln("Chunk data.length = %s", chunk.data.length);
|
|
|
|
if (!istrm.readBE!uint(&chunk.crc))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// TODO: reimplement CRC check with ranges instead of concatenation
|
|
uint calculatedCRC = crc32(chain(chunk.type[0..$], chunk.data));
|
|
|
|
version(PNGDebug)
|
|
{
|
|
writefln("Chunk CRC = %X", chunk.crc);
|
|
writefln("Calculated CRC = %X", calculatedCRC);
|
|
writeln("-------------------");
|
|
}
|
|
|
|
if (chunk.crc != calculatedCRC)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool readHeader(PNGHeader* hdr, PNGChunk* chunk)
|
|
{
|
|
hdr.bytes[] = chunk.data[];
|
|
hdr.width = bigEndian(hdr.width);
|
|
hdr.height = bigEndian(hdr.height);
|
|
|
|
version(PNGDebug)
|
|
{
|
|
writefln("width = %s", hdr.width);
|
|
writefln("height = %s", hdr.height);
|
|
writefln("bitDepth = %s", hdr.bitDepth);
|
|
writefln("colorType = %s", hdr.colorType);
|
|
writefln("compressionMethod = %s", hdr.compressionMethod);
|
|
writefln("filterMethod = %s", hdr.filterMethod);
|
|
writefln("interlaceMethod = %s", hdr.interlaceMethod);
|
|
writeln("----------------");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ubyte[8] signatureBuffer;
|
|
|
|
if (!istrm.fillArray(signatureBuffer))
|
|
{
|
|
return error("loadPNG error: signature check failed");
|
|
}
|
|
|
|
version(PNGDebug)
|
|
{
|
|
writeln("----------------");
|
|
writeln("PNG Signature: ", signatureBuffer);
|
|
writeln("----------------");
|
|
}
|
|
|
|
PNGHeader hdr;
|
|
|
|
ZlibDecoder zlibDecoder;
|
|
|
|
ubyte[] palette;
|
|
ubyte[] transparency;
|
|
uint paletteSize = 0;
|
|
|
|
bool endChunk = false;
|
|
while (!endChunk && istrm.readable)
|
|
{
|
|
PNGChunk chunk;
|
|
bool res = readChunk(&chunk);
|
|
if (!res)
|
|
{
|
|
chunk.free();
|
|
return error("loadPNG error: failed to read chunk");
|
|
}
|
|
else
|
|
{
|
|
if (chunk.type == IEND)
|
|
{
|
|
endChunk = true;
|
|
chunk.free();
|
|
}
|
|
else if (chunk.type == IHDR)
|
|
{
|
|
if (chunk.data.length < hdr.bytes.length)
|
|
return error("loadPNG error: illegal header chunk");
|
|
|
|
readHeader(&hdr, &chunk);
|
|
chunk.free();
|
|
|
|
bool supportedIndexed =
|
|
(hdr.colorType == ColorType.Palette) &&
|
|
(hdr.bitDepth == 1 ||
|
|
hdr.bitDepth == 2 ||
|
|
hdr.bitDepth == 4 ||
|
|
hdr.bitDepth == 8);
|
|
|
|
if (hdr.bitDepth != 8 && hdr.bitDepth != 16 && !supportedIndexed)
|
|
return error("loadPNG error: unsupported bit depth");
|
|
|
|
if (hdr.compressionMethod != 0)
|
|
return error("loadPNG error: unsupported compression method");
|
|
|
|
if (hdr.filterMethod != 0)
|
|
return error("loadPNG error: unsupported filter method");
|
|
|
|
if (hdr.interlaceMethod != 0)
|
|
return error("loadPNG error: interlacing is not supported");
|
|
|
|
uint bufferLength = ((hdr.width * hdr.bitDepth + 7) / 8) * hdr.height + hdr.height;
|
|
ubyte[] buffer = New!(ubyte[])(bufferLength);
|
|
|
|
zlibDecoder = ZlibDecoder(buffer);
|
|
|
|
version(PNGDebug)
|
|
{
|
|
writefln("buffer.length = %s", bufferLength);
|
|
writeln("----------------");
|
|
}
|
|
}
|
|
else if (chunk.type == IDAT)
|
|
{
|
|
zlibDecoder.decode(chunk.data);
|
|
chunk.free();
|
|
}
|
|
else if (chunk.type == PLTE)
|
|
{
|
|
palette = chunk.data;
|
|
}
|
|
else if (chunk.type == tRNS)
|
|
{
|
|
transparency = chunk.data;
|
|
version(PNGDebug)
|
|
{
|
|
writeln("----------------");
|
|
writefln("transparency.length = %s", transparency.length);
|
|
writeln("----------------");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
chunk.free();
|
|
}
|
|
}
|
|
}
|
|
|
|
// finalize decoder
|
|
version(PNGDebug) writefln("zlibDecoder.hasEnded = %s", zlibDecoder.hasEnded);
|
|
if (!zlibDecoder.hasEnded)
|
|
return error("loadPNG error: unexpected end of zlib stream");
|
|
|
|
ubyte[] buffer = zlibDecoder.buffer;
|
|
version(PNGDebug) writefln("buffer.length = %s", buffer.length);
|
|
|
|
bool transparencyPalette;
|
|
|
|
// create image
|
|
if (hdr.colorType == ColorType.Greyscale)
|
|
{
|
|
if (hdr.bitDepth == 8)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 1, 8);
|
|
else if (hdr.bitDepth == 16)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 1, 16);
|
|
}
|
|
else if (hdr.colorType == ColorType.GreyscaleAlpha)
|
|
{
|
|
if (hdr.bitDepth == 8)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 2, 8);
|
|
else if (hdr.bitDepth == 16)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 2, 16);
|
|
}
|
|
else if (hdr.colorType == ColorType.RGB)
|
|
{
|
|
if (hdr.bitDepth == 8)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
|
|
else if (hdr.bitDepth == 16)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 3, 16);
|
|
}
|
|
else if (hdr.colorType == ColorType.RGBA)
|
|
{
|
|
if (hdr.bitDepth == 8)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
|
|
else if (hdr.bitDepth == 16)
|
|
img = imgFac.createImage(hdr.width, hdr.height, 4, 16);
|
|
}
|
|
else if (hdr.colorType == ColorType.Palette)
|
|
{
|
|
if (transparency.length > 0) {
|
|
img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
|
|
transparencyPalette = true;
|
|
} else
|
|
img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
|
|
}
|
|
else
|
|
return error("loadPNG error: unsupported color type");
|
|
|
|
version(PNGDebug)
|
|
{
|
|
writefln("img.width = %s", img.width);
|
|
writefln("img.height = %s", img.height);
|
|
writefln("img.bitDepth = %s", img.bitDepth);
|
|
writefln("img.channels = %s", img.channels);
|
|
writeln("----------------");
|
|
}
|
|
|
|
bool indexed = (hdr.colorType == ColorType.Palette);
|
|
|
|
// don't close the stream, just release our reference
|
|
istrm = null;
|
|
|
|
// apply filtering to the image data
|
|
ubyte[] buffer2;
|
|
string errorMsg;
|
|
if (!filter(&hdr, img.channels, indexed, buffer, buffer2, errorMsg))
|
|
{
|
|
return error(errorMsg);
|
|
}
|
|
Delete(buffer);
|
|
buffer = buffer2;
|
|
|
|
// if a palette is used, substitute target colors
|
|
if (indexed)
|
|
{
|
|
if (palette.length == 0)
|
|
return error("loadPNG error: palette chunk not found");
|
|
|
|
ubyte[] pdata = New!(ubyte[])(img.width * img.height * img.channels);
|
|
if (hdr.bitDepth == 8)
|
|
{
|
|
for (int i = 0; i < buffer.length; ++i)
|
|
{
|
|
ubyte b = buffer[i];
|
|
pdata[i * img.channels + 0] = palette[b * 3 + 0];
|
|
pdata[i * img.channels + 1] = palette[b * 3 + 1];
|
|
pdata[i * img.channels + 2] = palette[b * 3 + 2];
|
|
if (transparency.length > 0)
|
|
pdata[i * img.channels + 3] =
|
|
b < transparency.length ? transparency[b] : 0;
|
|
}
|
|
}
|
|
else // bit depths 1, 2, 4
|
|
{
|
|
int srcindex = 0;
|
|
int srcshift = 8 - hdr.bitDepth;
|
|
ubyte mask = cast(ubyte)((1 << hdr.bitDepth) - 1);
|
|
int sz = img.width * img.height;
|
|
for (int dstindex = 0; dstindex < sz; dstindex++)
|
|
{
|
|
auto b = ((buffer[srcindex] >> srcshift) & mask);
|
|
//assert(b * 3 + 2 < palette.length);
|
|
pdata[dstindex * img.channels + 0] = palette[b * 3 + 0];
|
|
pdata[dstindex * img.channels + 1] = palette[b * 3 + 1];
|
|
pdata[dstindex * img.channels + 2] = palette[b * 3 + 2];
|
|
|
|
if (transparency.length > 0)
|
|
pdata[dstindex * img.channels + 3] =
|
|
b < transparency.length ? transparency[b] : 0;
|
|
|
|
if (srcshift <= 0)
|
|
{
|
|
srcshift = 8 - hdr.bitDepth;
|
|
srcindex++;
|
|
}
|
|
else
|
|
{
|
|
srcshift -= hdr.bitDepth;
|
|
}
|
|
}
|
|
}
|
|
|
|
Delete(buffer);
|
|
buffer = pdata;
|
|
|
|
Delete(palette);
|
|
|
|
if (transparency.length > 0)
|
|
Delete(transparency);
|
|
}
|
|
|
|
//if (img.data.length != buffer.length)
|
|
// return error("loadPNG error: uncompressed data length mismatch");
|
|
//
|
|
//foreach(i, v; buffer)
|
|
// img.data[i] = v;
|
|
|
|
int bufindex = 0;
|
|
if (hdr.colorType == ColorType.Greyscale)
|
|
{
|
|
if (hdr.bitDepth == 8) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 1, 8);
|
|
for (int i = 0; i < img.length; i++) {
|
|
img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex])<<8) | ((cast(uint)buffer[bufindex])<<0) | 0xFF000000;
|
|
bufindex += 1;
|
|
}
|
|
} else if (hdr.bitDepth == 16) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 1, 16);
|
|
assert(false);
|
|
}
|
|
}
|
|
else if (hdr.colorType == ColorType.GreyscaleAlpha)
|
|
{
|
|
if (hdr.bitDepth == 8) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 2, 8);
|
|
for (int i = 0; i < img.length; i++) {
|
|
img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex])<<8) | ((cast(uint)buffer[bufindex])<<0) | ((cast(uint)buffer[bufindex + 1])<<24);
|
|
bufindex += 2;
|
|
}
|
|
} else if (hdr.bitDepth == 16) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 2, 16);
|
|
assert(false);
|
|
}
|
|
}
|
|
else if (hdr.colorType == ColorType.RGB)
|
|
{
|
|
if (hdr.bitDepth == 8) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
|
|
for (int i = 0; i < img.length; i++) {
|
|
img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | 0xFF000000;
|
|
bufindex += 3;
|
|
}
|
|
} else if (hdr.bitDepth == 16) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 3, 16);
|
|
assert(false);
|
|
}
|
|
}
|
|
else if (hdr.colorType == ColorType.RGBA)
|
|
{
|
|
if (hdr.bitDepth == 8) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
|
|
for (int i = 0; i < img.length; i++) {
|
|
img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | ((cast(uint)buffer[bufindex + 3])<<24);
|
|
bufindex += 4;
|
|
}
|
|
} else if (hdr.bitDepth == 16) {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 4, 16);
|
|
assert(false);
|
|
}
|
|
}
|
|
else if (hdr.colorType == ColorType.Palette)
|
|
{
|
|
if (transparencyPalette) {
|
|
for (int i = 0; i < img.length; i++) {
|
|
img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | ((cast(uint)buffer[bufindex + 3])<<24);
|
|
bufindex += 4;
|
|
}
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
|
|
} else {
|
|
//img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
|
|
for (int i = 0; i < img.length; i++) {
|
|
img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | 0xFF000000;
|
|
bufindex += 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
Delete(buffer);
|
|
|
|
return compound(img, "");
|
|
}
|
|
|
|
version (ENABLE_SAVE_PNG) {
|
|
/*
|
|
* Save PNG to stream.
|
|
* GC-free
|
|
*/
|
|
Compound!(bool, string) savePNG(SuperImage img, OutputStream output)
|
|
in
|
|
{
|
|
assert (img.data.length);
|
|
}
|
|
do
|
|
{
|
|
Compound!(bool, string) error(string errorMsg)
|
|
{
|
|
return compound(false, errorMsg);
|
|
}
|
|
|
|
if (img.bitDepth != 8)
|
|
return error("savePNG error: only 8-bit images are supported by encoder");
|
|
|
|
bool writeChunk(ubyte[4] chunkType, ubyte[] chunkData)
|
|
{
|
|
PNGChunk hdrChunk;
|
|
hdrChunk.length = cast(uint)chunkData.length;
|
|
hdrChunk.type = chunkType;
|
|
hdrChunk.data = chunkData;
|
|
hdrChunk.crc = crc32(chain(chunkType[0..$], hdrChunk.data));
|
|
|
|
if (!output.writeBE!uint(hdrChunk.length)
|
|
|| !output.writeArray(hdrChunk.type))
|
|
return false;
|
|
|
|
if (chunkData.length)
|
|
if (!output.writeArray(hdrChunk.data))
|
|
return false;
|
|
|
|
if (!output.writeBE!uint(hdrChunk.crc))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool writeHeader()
|
|
{
|
|
PNGHeader hdr;
|
|
hdr.width = networkByteOrder(img.width);
|
|
hdr.height = networkByteOrder(img.height);
|
|
hdr.bitDepth = 8;
|
|
if (img.channels == 4)
|
|
hdr.colorType = ColorType.RGBA;
|
|
else if (img.channels == 3)
|
|
hdr.colorType = ColorType.RGB;
|
|
else if (img.channels == 2)
|
|
hdr.colorType = ColorType.GreyscaleAlpha;
|
|
else if (img.channels == 1)
|
|
hdr.colorType = ColorType.Greyscale;
|
|
hdr.compressionMethod = 0;
|
|
hdr.filterMethod = 0;
|
|
hdr.interlaceMethod = 0;
|
|
|
|
return writeChunk(IHDR, hdr.bytes);
|
|
}
|
|
|
|
output.writeArray(PNGSignature);
|
|
if (!writeHeader())
|
|
return error("savePNG error: write failed (disk full?)");
|
|
|
|
//TODO: filtering
|
|
ubyte[] raw = New!(ubyte[])(img.width * img.height * img.channels + img.height);
|
|
foreach(y; 0..img.height)
|
|
{
|
|
auto rowStart = y * (img.width * img.channels + 1);
|
|
raw[rowStart] = 0; // No filter
|
|
|
|
foreach(x; 0..img.width)
|
|
{
|
|
auto dataIndex = (y * img.width + x) * img.channels;
|
|
auto rawIndex = rowStart + 1 + x * img.channels;
|
|
|
|
foreach(ch; 0..img.channels)
|
|
raw[rawIndex + ch] = img.data[dataIndex + ch];
|
|
}
|
|
}
|
|
|
|
ubyte[] buffer = New!(ubyte[])(64 * 1024);
|
|
ZlibBufferedEncoder zlibEncoder = ZlibBufferedEncoder(buffer, raw);
|
|
while (!zlibEncoder.ended)
|
|
{
|
|
auto len = zlibEncoder.encode();
|
|
if (len > 0)
|
|
writeChunk(IDAT, zlibEncoder.buffer[0..len]);
|
|
}
|
|
|
|
writeChunk(IEND, []);
|
|
|
|
Delete(buffer);
|
|
Delete(raw);
|
|
|
|
return compound(true, "");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* performs the paeth PNG filter from pixels values:
|
|
* a = back
|
|
* b = up
|
|
* c = up and back
|
|
*/
|
|
pure ubyte paeth(ubyte a, ubyte b, ubyte c)
|
|
{
|
|
int p = a + b - c;
|
|
int pa = abs(p - a);
|
|
int pb = abs(p - b);
|
|
int pc = abs(p - c);
|
|
if (pa <= pb && pa <= pc) return a;
|
|
else if (pb <= pc) return b;
|
|
else return c;
|
|
}
|
|
|
|
bool filter(PNGHeader* hdr,
|
|
uint channels,
|
|
bool indexed,
|
|
ubyte[] ibuffer,
|
|
out ubyte[] obuffer,
|
|
out string errorMsg)
|
|
{
|
|
uint dataSize = cast(uint)ibuffer.length;
|
|
uint scanlineSize;
|
|
|
|
uint calculatedSize;
|
|
if (indexed)
|
|
{
|
|
calculatedSize = hdr.width * hdr.height * hdr.bitDepth / 8 + hdr.height;
|
|
scanlineSize = hdr.width * hdr.bitDepth / 8 + 1;
|
|
}
|
|
else
|
|
{
|
|
calculatedSize = hdr.width * hdr.height * channels + hdr.height;
|
|
scanlineSize = hdr.width * channels + 1;
|
|
}
|
|
|
|
version(PNGDebug)
|
|
{
|
|
writefln("[filter] dataSize = %s", dataSize);
|
|
writefln("[filter] calculatedSize = %s", calculatedSize);
|
|
}
|
|
|
|
if (dataSize != calculatedSize)
|
|
{
|
|
errorMsg = "loadPNG error: image size and data mismatch";
|
|
return false;
|
|
}
|
|
|
|
obuffer = New!(ubyte[])(calculatedSize - hdr.height);
|
|
|
|
ubyte pback, pup, pupback, cbyte;
|
|
|
|
for (int i = 0; i < hdr.height; ++i)
|
|
{
|
|
pback = 0;
|
|
|
|
// get the first byte of a scanline
|
|
ubyte scanFilter = ibuffer[i * scanlineSize];
|
|
|
|
if (indexed)
|
|
{
|
|
// TODO: support filtering for indexed images
|
|
if (scanFilter != FilterMethod.None)
|
|
{
|
|
errorMsg = "loadPNG error: filtering is not supported for indexed images";
|
|
return false;
|
|
}
|
|
|
|
for (int j = 1; j < scanlineSize; ++j)
|
|
{
|
|
ubyte b = ibuffer[(i * scanlineSize) + j];
|
|
obuffer[(i * (scanlineSize-1) + j - 1)] = b;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
for (int j = 0; j < hdr.width; ++j)
|
|
{
|
|
for (int k = 0; k < channels; ++k)
|
|
{
|
|
if (i == 0) pup = 0;
|
|
else pup = obuffer[((i-1) * hdr.width + j) * channels + k]; // (hdr.height-(i-1)-1)
|
|
if (j == 0) pback = 0;
|
|
else pback = obuffer[(i * hdr.width + j-1) * channels + k];
|
|
if (i == 0 || j == 0) pupback = 0;
|
|
else pupback = obuffer[((i-1) * hdr.width + j - 1) * channels + k];
|
|
|
|
// get the current byte from ibuffer
|
|
cbyte = ibuffer[i * (hdr.width * channels + 1) + j * channels + k + 1];
|
|
|
|
// filter, then set the current byte in data
|
|
switch (scanFilter)
|
|
{
|
|
case FilterMethod.None:
|
|
obuffer[(i * hdr.width + j) * channels + k] = cbyte;
|
|
break;
|
|
case FilterMethod.Sub:
|
|
obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + pback);
|
|
break;
|
|
case FilterMethod.Up:
|
|
obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + pup);
|
|
break;
|
|
case FilterMethod.Average:
|
|
obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + (pback + pup) / 2);
|
|
break;
|
|
case FilterMethod.Paeth:
|
|
obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + paeth(pback, pup, pupback));
|
|
break;
|
|
default:
|
|
errorMsg = format("loadPNG error: unknown scanline filter (%s)", scanFilter);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint crc32(R)(R range, uint inCrc = 0) if (isInputRange!R)
|
|
{
|
|
uint[256] generateTable()
|
|
{
|
|
uint[256] table;
|
|
uint crc;
|
|
for (int i = 0; i < 256; i++)
|
|
{
|
|
crc = i;
|
|
for (int j = 0; j < 8; j++)
|
|
crc = crc & 1 ? (crc >> 1) ^ 0xEDB88320UL : crc >> 1;
|
|
table[i] = crc;
|
|
}
|
|
return table;
|
|
}
|
|
|
|
static const uint[256] table = generateTable();
|
|
|
|
uint crc;
|
|
|
|
crc = inCrc ^ 0xFFFFFFFF;
|
|
foreach(v; range)
|
|
crc = (crc >> 8) ^ table[(crc ^ v) & 0xFF];
|
|
|
|
return (crc ^ 0xFFFFFFFF);
|
|
}
|
|
|
|
static if (false) {
|
|
unittest
|
|
{
|
|
import std.base64;
|
|
|
|
InputStream png() {
|
|
string minimal =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVR42mL4z8AAEGAAAwEBAGb9nyQAAAAASUVORK5CYII=";
|
|
|
|
ubyte[] bytes = Base64.decode(minimal);
|
|
return new ArrayStream(bytes, bytes.length);
|
|
}
|
|
|
|
SuperImage img = loadPNG(png());
|
|
|
|
assert(img.width == 1);
|
|
assert(img.height == 1);
|
|
assert(img.channels == 3);
|
|
assert(img.pixelSize == 3);
|
|
assert(img.data == [0xff, 0x00, 0x00]);
|
|
|
|
createDir("tests", false);
|
|
savePNG(img, "tests/minimal.png");
|
|
loadPNG("tests/minimal.png");
|
|
}
|
|
}
|