diff options
Diffstat (limited to 'src/ext_depends/imageformats')
41 files changed, 3292 insertions, 0 deletions
diff --git a/src/ext_depends/imageformats/.travis.yml b/src/ext_depends/imageformats/.travis.yml new file mode 100644 index 0000000..62d0c9e --- /dev/null +++ b/src/ext_depends/imageformats/.travis.yml @@ -0,0 +1 @@ +language: d diff --git a/src/ext_depends/imageformats/LICENSE b/src/ext_depends/imageformats/LICENSE new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/src/ext_depends/imageformats/LICENSE @@ -0,0 +1,23 @@ +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. diff --git a/src/ext_depends/imageformats/README.md b/src/ext_depends/imageformats/README.md new file mode 100644 index 0000000..c9c0b36 --- /dev/null +++ b/src/ext_depends/imageformats/README.md @@ -0,0 +1,37 @@ +# imageformats [![Build Status](https://travis-ci.org/lgvz/imageformats.svg)](https://travis-ci.org/lgvz/imageformats) + +- Returned image data is 8-bit except PNG can also return 16-bit. +- Image data can be converted to Y, YA, RGB or RGBA. +- There's a `@nogc` remake: [imagefmt](https://github.com/tjhann/imagefmt) + +**Decoders:** +- PNG. 8-bit and 16-bit interlaced and paletted (+`tRNS` chunk) +- TGA. 8-bit non-paletted +- BMP. 8-bit uncompressed +- JPEG. baseline + +**Encoders:** +- PNG. 8-bit non-paletted non-interlaced +- TGA. 8-bit non-paletted rle-compressed +- BMP. 8-bit uncompressed + +```D +import imageformats; + +void main() { + IFImage i0 = read_image("peruna.png"); + IFImage i1 = read_image("peruna.png", ColFmt.YA); // convert + IFImage i2 = read_image("peruna.png", ColFmt.RGB); + + write_image("peruna.tga", i0.w, i0.h, i0.pixels); + write_image("peruna.tga", i0.w, i0.h, i0.pixels, ColFmt.RGBA); + + int w, h, chans; + read_image_info("peruna.png", w, h, chans); + + // format specific functions + PNG_Header hdr = read_png_header("peruna.png"); + IFImage i3 = read_jpeg("porkkana.jpg"); + write_tga("porkkana.tga", i3.w, i3.h, i3.pixels); +} +``` diff --git a/src/ext_depends/imageformats/dub.sdl b/src/ext_depends/imageformats/dub.sdl new file mode 100644 index 0000000..2814286 --- /dev/null +++ b/src/ext_depends/imageformats/dub.sdl @@ -0,0 +1,7 @@ +name "imageformats" +description "Decoders for PNG, TGA, BMP, JPEG and encoders for PNG, TGA, BMP." +authors "Tero Hänninen" +license "BSL-1.0" +targetName "imageformats" +sourcePaths "imageformats" +importPaths "." diff --git a/src/ext_depends/imageformats/imageformats/bmp.d b/src/ext_depends/imageformats/imageformats/bmp.d new file mode 100644 index 0000000..ace042a --- /dev/null +++ b/src/ext_depends/imageformats/imageformats/bmp.d @@ -0,0 +1,452 @@ +module imageformats.bmp; + +import std.bitmanip : littleEndianToNative, nativeToLittleEndian; +import std.stdio : File, SEEK_SET; +import std.math : abs; +import std.typecons : scoped; +import imageformats; + +private: + +immutable bmp_header = ['B', 'M']; + +/// Reads a BMP image. req_chans defines the format of returned image +/// (you can use ColFmt here). +public IFImage read_bmp(in char[] filename, long req_chans = 0) { + auto reader = scoped!FileReader(filename); + return read_bmp(reader, req_chans); +} + +/// Reads an image from a buffer containing a BMP image. req_chans defines the +/// format of returned image (you can use ColFmt here). +public IFImage read_bmp_from_mem(in ubyte[] source, long req_chans = 0) { + auto reader = scoped!MemReader(source); + return read_bmp(reader, req_chans); +} + +/// Returns the header of a BMP file. +public BMP_Header read_bmp_header(in char[] filename) { + auto reader = scoped!FileReader(filename); + return read_bmp_header(reader); +} + +/// Reads the image header from a buffer containing a BMP image. +public BMP_Header read_bmp_header_from_mem(in ubyte[] source) { + auto reader = scoped!MemReader(source); + return read_bmp_header(reader); +} + +/// Header of a BMP file. +public struct BMP_Header { + uint file_size; + uint pixel_data_offset; + + uint dib_size; + int width; + int height; + ushort planes; + int bits_pp; + uint dib_version; + DibV1 dib_v1; + DibV2 dib_v2; + uint dib_v3_alpha_mask; + DibV4 dib_v4; + DibV5 dib_v5; +} + +/// Part of BMP header, not always present. +public struct DibV1 { + uint compression; + uint idat_size; + uint pixels_per_meter_x; + uint pixels_per_meter_y; + uint palette_length; + uint important_color_count; +} + +/// Part of BMP header, not always present. +public struct DibV2 { + uint red_mask; + uint green_mask; + uint blue_mask; +} + +/// Part of BMP header, not always present. +public struct DibV4 { + uint color_space_type; + ubyte[36] color_space_endpoints; + uint gamma_red; + uint gamma_green; + uint gamma_blue; +} + +/// Part of BMP header, not always present. +public struct DibV5 { + uint icc_profile_data; + uint icc_profile_size; +} + +/// Returns width, height and color format information via w, h and chans. +public void read_bmp_info(in char[] filename, out int w, out int h, out int chans) { + auto reader = scoped!FileReader(filename); + return read_bmp_info(reader, w, h, chans); +} + +/// Returns width, height and color format information via w, h and chans. +public void read_bmp_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { + auto reader = scoped!MemReader(source); + return read_bmp_info(reader, w, h, chans); +} + +/// Writes a BMP image into a file. +public void write_bmp(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) +{ + auto writer = scoped!FileWriter(file); + write_bmp(writer, w, h, data, tgt_chans); +} + +/// Writes a BMP image into a buffer. +public ubyte[] write_bmp_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { + auto writer = scoped!MemWriter(); + write_bmp(writer, w, h, data, tgt_chans); + return writer.result; +} + +// Detects whether a BMP image is readable from stream. +package bool detect_bmp(Reader stream) { + try { + ubyte[18] tmp = void; // bmp header + size of dib header + stream.readExact(tmp, tmp.length); + size_t ds = littleEndianToNative!uint(tmp[14..18]); + return (tmp[0..2] == bmp_header + && (ds == 12 || ds == 40 || ds == 52 || ds == 56 || ds == 108 || ds == 124)); + } catch (Throwable) { + return false; + } finally { + stream.seek(0, SEEK_SET); + } +} + +BMP_Header read_bmp_header(Reader stream) { + ubyte[18] tmp = void; // bmp header + size of dib header + stream.readExact(tmp[], tmp.length); + + if (tmp[0..2] != bmp_header) + throw new ImageIOException("corrupt header"); + + uint dib_size = littleEndianToNative!uint(tmp[14..18]); + uint dib_version; + switch (dib_size) { + case 12: dib_version = 0; break; + case 40: dib_version = 1; break; + case 52: dib_version = 2; break; + case 56: dib_version = 3; break; + case 108: dib_version = 4; break; + case 124: dib_version = 5; break; + default: throw new ImageIOException("unsupported dib version"); + } + auto dib_header = new ubyte[dib_size-4]; + stream.readExact(dib_header[], dib_header.length); + + DibV1 dib_v1; + DibV2 dib_v2; + uint dib_v3_alpha_mask; + DibV4 dib_v4; + DibV5 dib_v5; + + if (1 <= dib_version) { + DibV1 v1 = { + compression : littleEndianToNative!uint(dib_header[12..16]), + idat_size : littleEndianToNative!uint(dib_header[16..20]), + pixels_per_meter_x : littleEndianToNative!uint(dib_header[20..24]), + pixels_per_meter_y : littleEndianToNative!uint(dib_header[24..28]), + palette_length : littleEndianToNative!uint(dib_header[28..32]), + important_color_count : littleEndianToNative!uint(dib_header[32..36]), + }; + dib_v1 = v1; + } + + if (2 <= dib_version) { + DibV2 v2 = { + red_mask : littleEndianToNative!uint(dib_header[36..40]), + green_mask : littleEndianToNative!uint(dib_header[40..44]), + blue_mask : littleEndianToNative!uint(dib_header[44..48]), + }; + dib_v2 = v2; + } + + if (3 <= dib_version) { + dib_v3_alpha_mask = littleEndianToNative!uint(dib_header[48..52]); + } + + if (4 <= dib_version) { + DibV4 v4 = { + color_space_type : littleEndianToNative!uint(dib_header[52..56]), + color_space_endpoints : dib_header[56..92], + gamma_red : littleEndianToNative!uint(dib_header[92..96]), + gamma_green : littleEndianToNative!uint(dib_header[96..100]), + gamma_blue : littleEndianToNative!uint(dib_header[100..104]), + }; + dib_v4 = v4; + } + + if (5 <= dib_version) { + DibV5 v5 = { + icc_profile_data : littleEndianToNative!uint(dib_header[108..112]), + icc_profile_size : littleEndianToNative!uint(dib_header[112..116]), + }; + dib_v5 = v5; + } + + int width, height; ushort planes; int bits_pp; + if (0 == dib_version) { + width = littleEndianToNative!ushort(dib_header[0..2]); + height = littleEndianToNative!ushort(dib_header[2..4]); + planes = littleEndianToNative!ushort(dib_header[4..6]); + bits_pp = littleEndianToNative!ushort(dib_header[6..8]); + } else { + width = littleEndianToNative!int(dib_header[0..4]); + height = littleEndianToNative!int(dib_header[4..8]); + planes = littleEndianToNative!ushort(dib_header[8..10]); + bits_pp = littleEndianToNative!ushort(dib_header[10..12]); + } + + BMP_Header header = { + file_size : littleEndianToNative!uint(tmp[2..6]), + pixel_data_offset : littleEndianToNative!uint(tmp[10..14]), + width : width, + height : height, + planes : planes, + bits_pp : bits_pp, + dib_version : dib_version, + dib_v1 : dib_v1, + dib_v2 : dib_v2, + dib_v3_alpha_mask : dib_v3_alpha_mask, + dib_v4 : dib_v4, + dib_v5 : dib_v5, + }; + return header; +} + +enum CMP_RGB = 0; +enum CMP_BITS = 3; + +package IFImage read_bmp(Reader stream, long req_chans = 0) { + if (req_chans < 0 || 4 < req_chans) + throw new ImageIOException("unknown color format"); + + BMP_Header hdr = read_bmp_header(stream); + + if (hdr.width < 1 || hdr.height == 0) + throw new ImageIOException("invalid dimensions"); + if (hdr.pixel_data_offset < (14 + hdr.dib_size) + || hdr.pixel_data_offset > 0xffffff /* arbitrary */) { + throw new ImageIOException("invalid pixel data offset"); + } + if (hdr.planes != 1) + throw new ImageIOException("not supported"); + + auto bytes_pp = 1; + bool paletted = true; + size_t palette_length = 256; + bool rgb_masked = false; + auto pe_bytes_pp = 3; + + if (1 <= hdr.dib_version) { + if (256 < hdr.dib_v1.palette_length) + throw new ImageIOException("ivnalid palette length"); + if (hdr.bits_pp <= 8 && + (hdr.dib_v1.palette_length == 0 || hdr.dib_v1.compression != CMP_RGB)) + throw new ImageIOException("unsupported format"); + if (hdr.dib_v1.compression != CMP_RGB && hdr.dib_v1.compression != CMP_BITS) + throw new ImageIOException("unsupported compression"); + + switch (hdr.bits_pp) { + case 8 : bytes_pp = 1; paletted = true; break; + case 24 : bytes_pp = 3; paletted = false; break; + case 32 : bytes_pp = 4; paletted = false; break; + default: throw new ImageIOException("not supported"); + } + + palette_length = hdr.dib_v1.palette_length; + rgb_masked = hdr.dib_v1.compression == CMP_BITS; + pe_bytes_pp = 4; + } + + static size_t mask_to_idx(in uint mask) { + switch (mask) { + case 0xff00_0000: return 3; + case 0x00ff_0000: return 2; + case 0x0000_ff00: return 1; + case 0x0000_00ff: return 0; + default: throw new ImageIOException("unsupported mask"); + } + } + + size_t redi = 2; + size_t greeni = 1; + size_t bluei = 0; + if (rgb_masked) { + if (hdr.dib_version < 2) + throw new ImageIOException("invalid format"); + redi = mask_to_idx(hdr.dib_v2.red_mask); + greeni = mask_to_idx(hdr.dib_v2.green_mask); + bluei = mask_to_idx(hdr.dib_v2.blue_mask); + } + + bool alpha_masked = false; + size_t alphai = 0; + if (bytes_pp == 4 && 3 <= hdr.dib_version && hdr.dib_v3_alpha_mask != 0) { + alpha_masked = true; + alphai = mask_to_idx(hdr.dib_v3_alpha_mask); + } + + ubyte[] depaletted_line = null; + ubyte[] palette = null; + if (paletted) { + depaletted_line = new ubyte[hdr.width * pe_bytes_pp]; + palette = new ubyte[palette_length * pe_bytes_pp]; + stream.readExact(palette[], palette.length); + } + + stream.seek(hdr.pixel_data_offset, SEEK_SET); + + const tgt_chans = (0 < req_chans) ? req_chans + : (alpha_masked) ? _ColFmt.RGBA + : _ColFmt.RGB; + + const src_fmt = (!paletted || pe_bytes_pp == 4) ? _ColFmt.BGRA : _ColFmt.BGR; + const LineConv!ubyte convert = get_converter!ubyte(src_fmt, tgt_chans); + + const size_t src_linesize = hdr.width * bytes_pp; // without padding + const size_t src_pad = 3 - ((src_linesize-1) % 4); + const ptrdiff_t tgt_linesize = (hdr.width * cast(int) tgt_chans); + + const ptrdiff_t tgt_stride = (hdr.height < 0) ? tgt_linesize : -tgt_linesize; + ptrdiff_t ti = (hdr.height < 0) ? 0 : (hdr.height-1) * tgt_linesize; + + auto src_line = new ubyte[src_linesize + src_pad]; + auto bgra_line_buf = (paletted) ? null : new ubyte[hdr.width * 4]; + auto result = new ubyte[hdr.width * abs(hdr.height) * cast(int) tgt_chans]; + + foreach (_; 0 .. abs(hdr.height)) { + stream.readExact(src_line[], src_line.length); + + if (paletted) { + const size_t ps = pe_bytes_pp; + size_t di = 0; + foreach (idx; src_line[0..src_linesize]) { + if (idx > palette_length) + throw new ImageIOException("invalid palette index"); + size_t i = idx * ps; + depaletted_line[di .. di+ps] = palette[i .. i+ps]; + if (ps == 4) { + depaletted_line[di+3] = 255; + } + di += ps; + } + convert(depaletted_line[], result[ti .. ti + tgt_linesize]); + } else { + for (size_t si, di; si < src_linesize; si+=bytes_pp, di+=4) { + bgra_line_buf[di + 0] = src_line[si + bluei]; + bgra_line_buf[di + 1] = src_line[si + greeni]; + bgra_line_buf[di + 2] = src_line[si + redi]; + bgra_line_buf[di + 3] = (alpha_masked) ? src_line[si + alphai] + : 255; + } + convert(bgra_line_buf[], result[ti .. ti + tgt_linesize]); + } + + ti += tgt_stride; + } + + IFImage ret = { + w : hdr.width, + h : abs(hdr.height), + c : cast(ColFmt) tgt_chans, + pixels : result, + }; + return ret; +} + +package void read_bmp_info(Reader stream, out int w, out int h, out int chans) { + BMP_Header hdr = read_bmp_header(stream); + w = abs(hdr.width); + h = abs(hdr.height); + chans = (hdr.dib_version >= 3 && hdr.dib_v3_alpha_mask != 0 && hdr.bits_pp == 32) + ? ColFmt.RGBA + : ColFmt.RGB; +} + +// ---------------------------------------------------------------------- +// BMP encoder + +// Writes RGB or RGBA data. +void write_bmp(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { + if (w < 1 || h < 1 || 0x7fff < w || 0x7fff < h) + throw new ImageIOException("invalid dimensions"); + size_t src_chans = data.length / cast(size_t) w / cast(size_t) h; + if (src_chans < 1 || 4 < src_chans) + throw new ImageIOException("invalid channel count"); + if (tgt_chans != 0 && tgt_chans != 3 && tgt_chans != 4) + throw new ImageIOException("unsupported format for writing"); + if (src_chans * w * h != data.length) + throw new ImageIOException("mismatching dimensions and length"); + + if (tgt_chans == 0) + tgt_chans = (src_chans == 1 || src_chans == 3) ? 3 : 4; + + const dib_size = 108; + const size_t tgt_linesize = cast(size_t) (w * tgt_chans); + const size_t pad = 3 - ((tgt_linesize-1) & 3); + const size_t idat_offset = 14 + dib_size; // bmp file header + dib header + const size_t filesize = idat_offset + cast(size_t) h * (tgt_linesize + pad); + if (filesize > 0xffff_ffff) { + throw new ImageIOException("image too large"); + } + + ubyte[14+dib_size] hdr; + hdr[0] = 0x42; + hdr[1] = 0x4d; + hdr[2..6] = nativeToLittleEndian(cast(uint) filesize); + hdr[6..10] = 0; // reserved + hdr[10..14] = nativeToLittleEndian(cast(uint) idat_offset); // offset of pixel data + hdr[14..18] = nativeToLittleEndian(cast(uint) dib_size); // dib header size + hdr[18..22] = nativeToLittleEndian(cast(int) w); + hdr[22..26] = nativeToLittleEndian(cast(int) h); // positive -> bottom-up + hdr[26..28] = nativeToLittleEndian(cast(ushort) 1); // planes + hdr[28..30] = nativeToLittleEndian(cast(ushort) (tgt_chans * 8)); // bits per pixel + hdr[30..34] = nativeToLittleEndian((tgt_chans == 3) ? CMP_RGB : CMP_BITS); + hdr[34..54] = 0; // rest of dib v1 + if (tgt_chans == 3) { + hdr[54..70] = 0; // dib v2 and v3 + } else { + static immutable ubyte[16] b = [ + 0, 0, 0xff, 0, + 0, 0xff, 0, 0, + 0xff, 0, 0, 0, + 0, 0, 0, 0xff + ]; + hdr[54..70] = b; + } + static immutable ubyte[4] BGRs = ['B', 'G', 'R', 's']; + hdr[70..74] = BGRs; + hdr[74..122] = 0; + stream.rawWrite(hdr); + + const LineConv!ubyte convert = + get_converter!ubyte(src_chans, (tgt_chans == 3) ? _ColFmt.BGR + : _ColFmt.BGRA); + + auto tgt_line = new ubyte[tgt_linesize + pad]; + const size_t src_linesize = cast(size_t) w * src_chans; + size_t si = cast(size_t) h * src_linesize; + + foreach (_; 0..h) { + si -= src_linesize; + convert(data[si .. si + src_linesize], tgt_line[0..tgt_linesize]); + stream.rawWrite(tgt_line); + } + + stream.flush(); +} diff --git a/src/ext_depends/imageformats/imageformats/jpeg.d b/src/ext_depends/imageformats/imageformats/jpeg.d new file mode 100644 index 0000000..6481d77 --- /dev/null +++ b/src/ext_depends/imageformats/imageformats/jpeg.d @@ -0,0 +1,1038 @@ +// Baseline JPEG decoder + +module imageformats.jpeg; + +import std.math : ceil; +import std.bitmanip : bigEndianToNative; +import std.stdio : File, SEEK_SET, SEEK_CUR; +import std.typecons : scoped; +import imageformats; + +private: + +/// Reads a JPEG image. req_chans defines the format of returned image +/// (you can use ColFmt here). +public IFImage read_jpeg(in char[] filename, long req_chans = 0) { + auto reader = scoped!FileReader(filename); + return read_jpeg(reader, req_chans); +} + +/// Reads an image from a buffer containing a JPEG image. req_chans defines the +/// format of returned image (you can use ColFmt here). +public IFImage read_jpeg_from_mem(in ubyte[] source, long req_chans = 0) { + auto reader = scoped!MemReader(source); + return read_jpeg(reader, req_chans); +} + +/// Returns width, height and color format information via w, h and chans. +public void read_jpeg_info(in char[] filename, out int w, out int h, out int chans) { + auto reader = scoped!FileReader(filename); + return read_jpeg_info(reader, w, h, chans); +} + +/// Returns width, height and color format information via w, h and chans. +public void read_jpeg_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { + auto reader = scoped!MemReader(source); + return read_jpeg_info(reader, w, h, chans); +} + +// Detects whether a JPEG image is readable from stream. +package bool detect_jpeg(Reader stream) { + try { + int w, h, c; + read_jpeg_info(stream, w, h, c); + return true; + } catch (Throwable) { + return false; + } finally { + stream.seek(0, SEEK_SET); + } +} + +package void read_jpeg_info(Reader stream, out int w, out int h, out int chans) { + ubyte[2] marker = void; + stream.readExact(marker, 2); + + // SOI + if (marker[0] != 0xff || marker[1] != Marker.SOI) + throw new ImageIOException("not JPEG"); + + while (true) { + stream.readExact(marker, 2); + + if (marker[0] != 0xff) + throw new ImageIOException("no frame header"); + while (marker[1] == 0xff) + stream.readExact(marker[1..$], 1); + + switch (marker[1]) with (Marker) { + case SOF0: .. case SOF3: + case SOF9: .. case SOF11: + ubyte[8] tmp; + stream.readExact(tmp[0..8], 8); + //int len = bigEndianToNative!ushort(tmp[0..2]); + w = bigEndianToNative!ushort(tmp[5..7]); + h = bigEndianToNative!ushort(tmp[3..5]); + chans = tmp[7]; + return; + case SOS, EOI: throw new ImageIOException("no frame header"); + case DRI, DHT, DQT, COM: + case APP0: .. case APPf: + ubyte[2] lenbuf = void; + stream.readExact(lenbuf, 2); + int skiplen = bigEndianToNative!ushort(lenbuf) - 2; + stream.seek(skiplen, SEEK_CUR); + break; + default: throw new ImageIOException("unsupported marker"); + } + } + assert(0); +} + +package IFImage read_jpeg(Reader stream, long req_chans = 0) { + if (req_chans < 0 || 4 < req_chans) + throw new ImageIOException("come on..."); + + // SOI + ubyte[2] tmp = void; + stream.readExact(tmp, tmp.length); + if (tmp[0] != 0xff || tmp[1] != Marker.SOI) + throw new ImageIOException("not JPEG"); + + JPEG_Decoder dc = { stream: stream }; + + read_markers(dc); // reads until first scan header or eoi + if (dc.eoi_reached) + throw new ImageIOException("no image data"); + + dc.tgt_chans = (req_chans == 0) ? dc.num_comps : cast(int) req_chans; + + foreach (ref comp; dc.comps[0..dc.num_comps]) + comp.data = new ubyte[dc.num_mcu_x*comp.sfx*8*dc.num_mcu_y*comp.sfy*8]; + + // E.7 -- Multiple scans are for progressive images which are not supported + //while (!dc.eoi_reached) { + decode_scan(dc); // E.2.3 + //read_markers(dc); // reads until next scan header or eoi + //} + + // throw away fill samples and convert to target format + ubyte[] pixels = dc.reconstruct(); + + IFImage result = { + w : dc.width, + h : dc.height, + c : cast(ColFmt) dc.tgt_chans, + pixels : pixels, + }; + return result; +} + +struct JPEG_Decoder { + Reader stream; + + bool has_frame_header = false; + bool eoi_reached = false; + + ubyte[64][4] qtables; + HuffTab[2] ac_tables; + HuffTab[2] dc_tables; + + ubyte cb; // current byte (next bit always at MSB) + int bits_left; // num of unused bits in cb + + bool correct_comp_ids; + Component[3] comps; + ubyte num_comps; + int tgt_chans; + + int width, height; + + int hmax, vmax; + + ushort restart_interval; // number of MCUs in restart interval + + // image component + struct Component { + ubyte sfx, sfy; // sampling factors, aka. h and v + size_t x, y; // total num of samples, without fill samples + ubyte qtable; + ubyte ac_table; + ubyte dc_table; + int pred; // dc prediction + ubyte[] data; // reconstructed samples + } + + int num_mcu_x; + int num_mcu_y; +} + +struct HuffTab { + ubyte[256] values; + ubyte[257] sizes; + short[16] mincode, maxcode; + short[16] valptr; +} + +enum Marker : ubyte { + SOI = 0xd8, // start of image + SOF0 = 0xc0, // start of frame / baseline DCT + //SOF1 = 0xc1, // start of frame / extended seq. + //SOF2 = 0xc2, // start of frame / progressive DCT + SOF3 = 0xc3, // start of frame / lossless + SOF9 = 0xc9, // start of frame / extended seq., arithmetic + SOF11 = 0xcb, // start of frame / lossless, arithmetic + DHT = 0xc4, // define huffman tables + DQT = 0xdb, // define quantization tables + DRI = 0xdd, // define restart interval + SOS = 0xda, // start of scan + DNL = 0xdc, // define number of lines + RST0 = 0xd0, // restart entropy coded data + // ... + RST7 = 0xd7, // restart entropy coded data + APP0 = 0xe0, // application 0 segment + // ... + APPf = 0xef, // application f segment + //DAC = 0xcc, // define arithmetic conditioning table + COM = 0xfe, // comment + EOI = 0xd9, // end of image +} + +void read_markers(ref JPEG_Decoder dc) { + bool has_next_scan_header = false; + while (!has_next_scan_header && !dc.eoi_reached) { + ubyte[2] marker; + dc.stream.readExact(marker, 2); + + if (marker[0] != 0xff) + throw new ImageIOException("no marker"); + while (marker[1] == 0xff) + dc.stream.readExact(marker[1..$], 1); + + switch (marker[1]) with (Marker) { + case DHT: dc.read_huffman_tables(); break; + case DQT: dc.read_quantization_tables(); break; + case SOF0: + if (dc.has_frame_header) + throw new ImageIOException("extra frame header"); + dc.read_frame_header(); + dc.has_frame_header = true; + break; + case SOS: + if (!dc.has_frame_header) + throw new ImageIOException("no frame header"); + dc.read_scan_header(); + has_next_scan_header = true; + break; + case DRI: dc.read_restart_interval(); break; + case EOI: dc.eoi_reached = true; break; + case APP0: .. case APPf: + case COM: + ubyte[2] lenbuf = void; + dc.stream.readExact(lenbuf, lenbuf.length); + int len = bigEndianToNative!ushort(lenbuf) - 2; + dc.stream.seek(len, SEEK_CUR); + break; + default: throw new ImageIOException("invalid / unsupported marker"); + } + } +} + +// DHT -- define huffman tables +void read_huffman_tables(ref JPEG_Decoder dc) { + ubyte[19] tmp = void; + dc.stream.readExact(tmp, 2); + int len = bigEndianToNative!ushort(tmp[0..2]); + len -= 2; + + while (0 < len) { + dc.stream.readExact(tmp, 17); // info byte & the BITS + ubyte table_slot = tmp[0] & 0xf; // must be 0 or 1 for baseline + ubyte table_class = tmp[0] >> 4; // 0 = dc table, 1 = ac table + if (1 < table_slot || 1 < table_class) + throw new ImageIOException("invalid / not supported"); + + // compute total number of huffman codes + int mt = 0; + foreach (i; 1..17) + mt += tmp[i]; + if (256 < mt) // TODO where in the spec? + throw new ImageIOException("invalid / not supported"); + + if (table_class == 0) { + dc.stream.readExact(dc.dc_tables[table_slot].values, mt); + derive_table(dc.dc_tables[table_slot], tmp[1..17]); + } else { + dc.stream.readExact(dc.ac_tables[table_slot].values, mt); + derive_table(dc.ac_tables[table_slot], tmp[1..17]); + } + + len -= 17 + mt; + } +} + +// num_values is the BITS +void derive_table(ref HuffTab table, in ref ubyte[16] num_values) { + short[256] codes; + + int k = 0; + foreach (i; 0..16) { + foreach (j; 0..num_values[i]) { + table.sizes[k] = cast(ubyte) (i + 1); + ++k; + } + } + table.sizes[k] = 0; + + k = 0; + short code = 0; + ubyte si = table.sizes[k]; + while (true) { + do { + codes[k] = code; + ++code; + ++k; + } while (si == table.sizes[k]); + + if (table.sizes[k] == 0) + break; + +// assert(si < table.sizes[k]); + do { + code <<= 1; + ++si; + } while (si != table.sizes[k]); + } + + derive_mincode_maxcode_valptr( + table.mincode, table.maxcode, table.valptr, + codes, num_values + ); +} + +// F.15 +void derive_mincode_maxcode_valptr(ref short[16] mincode, ref short[16] maxcode, + ref short[16] valptr, in ref short[256] codes, in ref ubyte[16] num_values) pure +{ + mincode[] = -1; + maxcode[] = -1; + valptr[] = -1; + + int j = 0; + foreach (i; 0..16) { + if (num_values[i] != 0) { + valptr[i] = cast(short) j; + mincode[i] = codes[j]; + j += num_values[i] - 1; + maxcode[i] = codes[j]; + j += 1; + } + } +} + +// DQT -- define quantization tables +void read_quantization_tables(ref JPEG_Decoder dc) { + ubyte[2] tmp = void; + dc.stream.readExact(tmp, 2); + int len = bigEndianToNative!ushort(tmp[0..2]); + if (len % 65 != 2) + throw new ImageIOException("invalid / not supported"); + len -= 2; + while (0 < len) { + dc.stream.readExact(tmp, 1); + ubyte table_info = tmp[0]; + ubyte table_slot = table_info & 0xf; + ubyte precision = table_info >> 4; // 0 = 8 bit, 1 = 16 bit + if (3 < table_slot || precision != 0) // only 8 bit for baseline + throw new ImageIOException("invalid / not supported"); + + dc.stream.readExact(dc.qtables[table_slot], 64); + len -= 1 + 64; + } +} + +// SOF0 -- start of frame +void read_frame_header(ref JPEG_Decoder dc) { + ubyte[9] tmp = void; + dc.stream.readExact(tmp, 8); + int len = bigEndianToNative!ushort(tmp[0..2]); // 8 + num_comps*3 + ubyte precision = tmp[2]; + dc.height = bigEndianToNative!ushort(tmp[3..5]); + dc.width = bigEndianToNative!ushort(tmp[5..7]); + dc.num_comps = tmp[7]; + + if ( precision != 8 || + (dc.num_comps != 1 && dc.num_comps != 3) || + len != 8 + dc.num_comps*3 ) + throw new ImageIOException("invalid / not supported"); + + dc.hmax = 0; + dc.vmax = 0; + int mcu_du = 0; // data units in one mcu + dc.stream.readExact(tmp, dc.num_comps*3); + foreach (i; 0..dc.num_comps) { + ubyte ci = tmp[i*3]; + // JFIF says ci should be i+1, but there are images where ci is i. Normalize ids + // so that ci == i, always. So much for standards... + if (i == 0) { dc.correct_comp_ids = ci == i+1; } + if ((dc.correct_comp_ids && ci != i+1) + || (!dc.correct_comp_ids && ci != i)) + throw new ImageIOException("invalid component id"); + + auto comp = &dc.comps[i]; + ubyte sampling_factors = tmp[i*3 + 1]; + comp.sfx = sampling_factors >> 4; + comp.sfy = sampling_factors & 0xf; + comp.qtable = tmp[i*3 + 2]; + if ( comp.sfy < 1 || 4 < comp.sfy || + comp.sfx < 1 || 4 < comp.sfx || + 3 < comp.qtable ) + throw new ImageIOException("invalid / not supported"); + + if (dc.hmax < comp.sfx) dc.hmax = comp.sfx; + if (dc.vmax < comp.sfy) dc.vmax = comp.sfy; + + mcu_du += comp.sfx * comp.sfy; + } + if (10 < mcu_du) + throw new ImageIOException("invalid / not supported"); + + foreach (i; 0..dc.num_comps) { + dc.comps[i].x = cast(size_t) ceil(dc.width * (cast(double) dc.comps[i].sfx / dc.hmax)); + dc.comps[i].y = cast(size_t) ceil(dc.height * (cast(double) dc.comps[i].sfy / dc.vmax)); + } + + size_t mcu_w = dc.hmax * 8; + size_t mcu_h = dc.vmax * 8; + dc.num_mcu_x = cast(int) ((dc.width + mcu_w-1) / mcu_w); + dc.num_mcu_y = cast(int) ((dc.height + mcu_h-1) / mcu_h); + + debug(DebugJPEG) { + writefln("\tlen: %s", len); + writefln("\tprecision: %s", precision); + writefln("\tdimensions: %s x %s", dc.width, dc.height); + writefln("\tnum_comps: %s", dc.num_comps); + writefln("\tnum_mcu_x: %s", dc.num_mcu_x); + writefln("\tnum_mcu_y: %s", dc.num_mcu_y); + } + +} + +// SOS -- start of scan +void read_scan_header(ref JPEG_Decoder dc) { + ubyte[3] tmp = void; + dc.stream.readExact(tmp, tmp.length); + ushort len = bigEndianToNative!ushort(tmp[0..2]); + ubyte num_scan_comps = tmp[2]; + + if ( num_scan_comps != dc.num_comps || + len != (6+num_scan_comps*2) ) + throw new ImageIOException("invalid / not supported"); + + ubyte[16] buf; + dc.stream.readExact(buf, len-3); + + foreach (i; 0..num_scan_comps) { + uint ci = buf[i*2] - ((dc.correct_comp_ids) ? 1 : 0); + if (ci >= dc.num_comps) + throw new ImageIOException("invalid component id"); + + ubyte tables = buf[i*2+1]; + dc.comps[ci].dc_table = tables >> 4; + dc.comps[ci].ac_table = tables & 0xf; + if ( 1 < dc.comps[ci].dc_table || + 1 < dc.comps[ci].ac_table ) + throw new ImageIOException("invalid / not supported"); + } + + // ignore these + //ubyte spectral_start = buf[$-3]; + //ubyte spectral_end = buf[$-2]; + //ubyte approx = buf[$-1]; +} + +void read_restart_interval(ref JPEG_Decoder dc) { + ubyte[4] tmp = void; + dc.stream.readExact(tmp, tmp.length); + ushort len = bigEndianToNative!ushort(tmp[0..2]); + if (len != 4) + throw new ImageIOException("invalid / not supported"); + dc.restart_interval = bigEndianToNative!ushort(tmp[2..4]); +} + +// E.2.3 and E.8 and E.9 +void decode_scan(ref JPEG_Decoder dc) { + int intervals, mcus; + if (0 < dc.restart_interval) { + int total_mcus = dc.num_mcu_x * dc.num_mcu_y; + intervals = (total_mcus + dc.restart_interval-1) / dc.restart_interval; + mcus = dc.restart_interval; + } else { + intervals = 1; + mcus = dc.num_mcu_x * dc.num_mcu_y; + } + + foreach (mcu_j; 0 .. dc.num_mcu_y) { + foreach (mcu_i; 0 .. dc.num_mcu_x) { + + // decode mcu + foreach (c; 0..dc.num_comps) { + auto comp = &dc.comps[c]; + foreach (du_j; 0 .. comp.sfy) { + foreach (du_i; 0 .. comp.sfx) { + // decode entropy, dequantize & dezigzag + short[64] data = decode_block(dc, *comp, dc.qtables[comp.qtable]); + // idct & level-shift + int outx = (mcu_i * comp.sfx + du_i) * 8; + int outy = (mcu_j * comp.sfy + du_j) * 8; + int dst_stride = dc.num_mcu_x * comp.sfx*8; + ubyte* dst = comp.data.ptr + outy*dst_stride + outx; + stbi__idct_block(dst, dst_stride, data); + } + } + } + + --mcus; + + if (!mcus) { + --intervals; + if (!intervals) + return; + + read_restart(dc.stream); // RSTx marker + + if (intervals == 1) { + // last interval, may have fewer MCUs than defined by DRI + mcus = (dc.num_mcu_y - mcu_j - 1) + * dc.num_mcu_x + dc.num_mcu_x - mcu_i - 1; + } else { + mcus = dc.restart_interval; + } + + // reset decoder + dc.cb = 0; + dc.bits_left = 0; + foreach (k; 0..dc.num_comps) + dc.comps[k].pred = 0; + } + + } + } +} + +// RST0-RST7 +void read_restart(Reader stream) { + ubyte[2] tmp = void; + stream.readExact(tmp, tmp.length); + if (tmp[0] != 0xff || tmp[1] < Marker.RST0 || Marker.RST7 < tmp[1]) + throw new ImageIOException("reset marker missing"); + // the markers should cycle 0 through 7, could check that here... +} + +immutable ubyte[64] dezigzag = [ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, +]; + +// decode entropy, dequantize & dezigzag (see section F.2) +short[64] decode_block(ref JPEG_Decoder dc, ref JPEG_Decoder.Component comp, + in ref ubyte[64] qtable) +{ + short[64] res = 0; + + ubyte t = decode_huff(dc, dc.dc_tables[comp.dc_table]); + int diff = t ? dc.receive_and_extend(t) : 0; + + comp.pred = comp.pred + diff; + res[0] = cast(short) (comp.pred * qtable[0]); + + int k = 1; + do { + ubyte rs = decode_huff(dc, dc.ac_tables[comp.ac_table]); + ubyte rrrr = rs >> 4; + ubyte ssss = rs & 0xf; + + if (ssss == 0) { + if (rrrr != 0xf) + break; // end of block + k += 16; // run length is 16 + continue; + } + + k += rrrr; + + if (63 < k) + throw new ImageIOException("corrupt block"); + res[dezigzag[k]] = cast(short) (dc.receive_and_extend(ssss) * qtable[k]); + k += 1; + } while (k < 64); + + return res; +} + +int receive_and_extend(ref JPEG_Decoder dc, ubyte s) { + // receive + int symbol = 0; + foreach (_; 0..s) + symbol = (symbol << 1) + nextbit(dc); + // extend + int vt = 1 << (s-1); + if (symbol < vt) + return symbol + (-1 << s) + 1; + return symbol; +} + +// F.16 -- the DECODE +ubyte decode_huff(ref JPEG_Decoder dc, in ref HuffTab tab) { + short code = nextbit(dc); + + int i = 0; + while (tab.maxcode[i] < code) { + code = cast(short) ((code << 1) + nextbit(dc)); + i += 1; + if (tab.maxcode.length <= i) + throw new ImageIOException("corrupt huffman coding"); + } + int j = tab.valptr[i] + code - tab.mincode[i]; + if (tab.values.length <= cast(uint) j) + throw new ImageIOException("corrupt huffman coding"); + return tab.values[j]; +} + +// F.2.2.5 and F.18 +ubyte nextbit(ref JPEG_Decoder dc) { + if (!dc.bits_left) { + ubyte[1] bytebuf; + dc.stream.readExact(bytebuf, 1); + dc.cb = bytebuf[0]; + dc.bits_left = 8; + + if (dc.cb == 0xff) { + dc.stream.readExact(bytebuf, 1); + if (bytebuf[0] != 0x0) + throw new ImageIOException("unexpected marker"); + } + } + + ubyte r = dc.cb >> 7; + dc.cb <<= 1; + dc.bits_left -= 1; + return r; +} + +ubyte[] reconstruct(in ref JPEG_Decoder dc) { + auto result = new ubyte[dc.width * dc.height * dc.tgt_chans]; + + switch (dc.num_comps * 10 + dc.tgt_chans) { + case 34, 33: + // Use specialized bilinear filtering functions for the frequent cases where + // Cb & Cr channels have half resolution. + if ((dc.comps[0].sfx <= 2 && dc.comps[0].sfy <= 2) + && (dc.comps[0].sfx + dc.comps[0].sfy >= 3) + && dc.comps[1].sfx == 1 && dc.comps[1].sfy == 1 + && dc.comps[2].sfx == 1 && dc.comps[2].sfy == 1) { + void function(in ubyte[], in ubyte[], ubyte[]) resample; + switch (dc.comps[0].sfx * 10 + dc.comps[0].sfy) { + case 22: resample = &upsample_h2_v2; break; + case 21: resample = &upsample_h2_v1; break; + case 12: resample = &upsample_h1_v2; break; + default: throw new ImageIOException("bug"); + } + + auto comp1 = new ubyte[dc.width]; + auto comp2 = new ubyte[dc.width]; + + size_t s = 0; + size_t di = 0; + foreach (j; 0 .. dc.height) { + size_t mi = j / dc.comps[0].sfy; + size_t si = (mi == 0 || mi >= (dc.height-1)/dc.comps[0].sfy) + ? mi : mi - 1 + s * 2; + s = s ^ 1; + + size_t cs = dc.num_mcu_x * dc.comps[1].sfx * 8; + size_t cl0 = mi * cs; + size_t cl1 = si * cs; + resample(dc.comps[1].data[cl0 .. cl0 + dc.comps[1].x], + dc.comps[1].data[cl1 .. cl1 + dc.comps[1].x], + comp1[]); + resample(dc.comps[2].data[cl0 .. cl0 + dc.comps[2].x], + dc.comps[2].data[cl1 .. cl1 + dc.comps[2].x], + comp2[]); + + foreach (i; 0 .. dc.width) { + result[di .. di+3] = ycbcr_to_rgb( + dc.comps[0].data[j * dc.num_mcu_x * dc.comps[0].sfx * 8 + i], + comp1[i], + comp2[i], + ); + if (dc.tgt_chans == 4) + result[di+3] = 255; + di += dc.tgt_chans; + } + } + + return result; + } + + foreach (const ref comp; dc.comps[0..dc.num_comps]) { + if (comp.sfx != dc.hmax || comp.sfy != dc.vmax) + return dc.upsample(result); + } + + size_t si, di; + foreach (j; 0 .. dc.height) { + foreach (i; 0 .. dc.width) { + result[di .. di+3] = ycbcr_to_rgb( + dc.comps[0].data[si+i], + dc.comps[1].data[si+i], + dc.comps[2].data[si+i], + ); + if (dc.tgt_chans == 4) + result[di+3] = 255; + di += dc.tgt_chans; + } + si += dc.num_mcu_x * dc.comps[0].sfx * 8; + } + return result; + case 32, 12, 31, 11: + const comp = &dc.comps[0]; + if (comp.sfx == dc.hmax && comp.sfy == dc.vmax) { + size_t si, di; + if (dc.tgt_chans == 2) { + foreach (j; 0 .. dc.height) { + foreach (i; 0 .. dc.width) { + result[di++] = comp.data[si+i]; + result[di++] = 255; + } + si += dc.num_mcu_x * comp.sfx * 8; + } + } else { + foreach (j; 0 .. dc.height) { + result[di .. di+dc.width] = comp.data[si .. si+dc.width]; + si += dc.num_mcu_x * comp.sfx * 8; + di += dc.width; + } + } + return result; + } else { + // need to resample (haven't tested this...) + return dc.upsample_luma(result); + } + case 14, 13: + const comp = &dc.comps[0]; + size_t si, di; + foreach (j; 0 .. dc.height) { + foreach (i; 0 .. dc.width) { + result[di .. di+3] = comp.data[si+i]; + if (dc.tgt_chans == 4) + result[di+3] = 255; + di += dc.tgt_chans; + } + si += dc.num_mcu_x * comp.sfx * 8; + } + return result; + default: assert(0); + } +} + +void upsample_h2_v2(in ubyte[] line0, in ubyte[] line1, ubyte[] result) { + ubyte mix(ubyte mm, ubyte ms, ubyte sm, ubyte ss) { + return cast(ubyte) (( cast(uint) mm * 3 * 3 + + cast(uint) ms * 3 * 1 + + cast(uint) sm * 1 * 3 + + cast(uint) ss * 1 * 1 + + 8) / 16); + } + + result[0] = cast(ubyte) (( cast(uint) line0[0] * 3 + + cast(uint) line1[0] * 1 + + 2) / 4); + if (line0.length == 1) return; + result[1] = mix(line0[0], line0[1], line1[0], line1[1]); + + size_t di = 2; + foreach (i; 1 .. line0.length) { + result[di] = mix(line0[i], line0[i-1], line1[i], line1[i-1]); + di += 1; + if (i == line0.length-1) { + if (di < result.length) { + result[di] = cast(ubyte) (( cast(uint) line0[i] * 3 + + cast(uint) line1[i] * 1 + + 2) / 4); + } + return; + } + result[di] = mix(line0[i], line0[i+1], line1[i], line1[i+1]); + di += 1; + } +} + +void upsample_h2_v1(in ubyte[] line0, in ubyte[] _line1, ubyte[] result) { + result[0] = line0[0]; + if (line0.length == 1) return; + result[1] = cast(ubyte) (( cast(uint) line0[0] * 3 + + cast(uint) line0[1] * 1 + + 2) / 4); + size_t di = 2; + foreach (i; 1 .. line0.length) { + result[di] = cast(ubyte) (( cast(uint) line0[i-1] * 1 + + cast(uint) line0[i+0] * 3 + + 2) / 4); + di += 1; + if (i == line0.length-1) { + if (di < result.length) result[di] = line0[i]; + return; + } + result[di] = cast(ubyte) (( cast(uint) line0[i+0] * 3 + + cast(uint) line0[i+1] * 1 + + 2) / 4); + di += 1; + } +} + +void upsample_h1_v2(in ubyte[] line0, in ubyte[] line1, ubyte[] result) { + foreach (i; 0 .. result.length) { + result[i] = cast(ubyte) (( cast(uint) line0[i] * 3 + + cast(uint) line1[i] * 1 + + 2) / 4); + } +} + +// Nearest neighbor +ubyte[] upsample_luma(in ref JPEG_Decoder dc, ubyte[] result) { + const size_t stride0 = dc.num_mcu_x * dc.comps[0].sfx * 8; + const y_step0 = cast(float) dc.comps[0].sfy / cast(float) dc.vmax; + const x_step0 = cast(float) dc.comps[0].sfx / cast(float) dc.hmax; + + float y0 = y_step0 * 0.5f; + size_t y0i = 0; + + size_t di; + + foreach (j; 0 .. dc.height) { + float x0 = x_step0 * 0.5f; + size_t x0i = 0; + foreach (i; 0 .. dc.width) { + result[di] = dc.comps[0].data[y0i + x0i]; + if (dc.tgt_chans == 2) + result[di+1] = 255; + di += dc.tgt_chans; + x0 += x_step0; + if (x0 >= 1.0f) { x0 -= 1.0f; x0i += 1; } + } + y0 += y_step0; + if (y0 >= 1.0f) { y0 -= 1.0f; y0i += stride0; } + } + return result; +} + +// Nearest neighbor +ubyte[] upsample(in ref JPEG_Decoder dc, ubyte[] result) { + const size_t stride0 = dc.num_mcu_x * dc.comps[0].sfx * 8; + const size_t stride1 = dc.num_mcu_x * dc.comps[1].sfx * 8; + const size_t stride2 = dc.num_mcu_x * dc.comps[2].sfx * 8; + + const y_step0 = cast(float) dc.comps[0].sfy / cast(float) dc.vmax; + const y_step1 = cast(float) dc.comps[1].sfy / cast(float) dc.vmax; + const y_step2 = cast(float) dc.comps[2].sfy / cast(float) dc.vmax; + const x_step0 = cast(float) dc.comps[0].sfx / cast(float) dc.hmax; + const x_step1 = cast(float) dc.comps[1].sfx / cast(float) dc.hmax; + const x_step2 = cast(float) dc.comps[2].sfx / cast(float) dc.hmax; + + float y0 = y_step0 * 0.5f; + float y1 = y_step1 * 0.5f; + float y2 = y_step2 * 0.5f; + size_t y0i = 0; + size_t y1i = 0; + size_t y2i = 0; + + size_t di; + + foreach (_j; 0 .. dc.height) { + float x0 = x_step0 * 0.5f; + float x1 = x_step1 * 0.5f; + float x2 = x_step2 * 0.5f; + size_t x0i = 0; + size_t x1i = 0; + size_t x2i = 0; + foreach (i; 0 .. dc.width) { + result[di .. di+3] = ycbcr_to_rgb( + dc.comps[0].data[y0i + x0i], + dc.comps[1].data[y1i + x1i], + dc.comps[2].data[y2i + x2i], + ); + if (dc.tgt_chans == 4) + result[di+3] = 255; + di += dc.tgt_chans; + x0 += x_step0; + x1 += x_step1; + x2 += x_step2; + if (x0 >= 1.0) { x0 -= 1.0f; x0i += 1; } + if (x1 >= 1.0) { x1 -= 1.0f; x1i += 1; } + if (x2 >= 1.0) { x2 -= 1.0f; x2i += 1; } + } + y0 += y_step0; + y1 += y_step1; + y2 += y_step2; + if (y0 >= 1.0) { y0 -= 1.0f; y0i += stride0; } + if (y1 >= 1.0) { y1 -= 1.0f; y1i += stride1; } + if (y2 >= 1.0) { y2 -= 1.0f; y2i += stride2; } + } + return result; +} + +ubyte[3] ycbcr_to_rgb(ubyte y, ubyte cb, ubyte cr) pure { + ubyte[3] rgb = void; + rgb[0] = clamp(y + 1.402*(cr-128)); + rgb[1] = clamp(y - 0.34414*(cb-128) - 0.71414*(cr-128)); + rgb[2] = clamp(y + 1.772*(cb-128)); + return rgb; +} + +ubyte clamp(float x) pure { + if (x < 0) return 0; + if (255 < x) return 255; + return cast(ubyte) x; +} + +// ------------------------------------------------------------ +// The IDCT stuff here (to the next dashed line) is copied and adapted from +// stb_image which is released under public domain. Many thanks to stb_image +// author, Sean Barrett. +// Link: https://github.com/nothings/stb/blob/master/stb_image.h + +pure int f2f(float x) { return cast(int) (x * 4096 + 0.5); } +pure int fsh(int x) { return x << 12; } + +// from stb_image, derived from jidctint -- DCT_ISLOW +pure void STBI__IDCT_1D(ref int t0, ref int t1, ref int t2, ref int t3, + ref int x0, ref int x1, ref int x2, ref int x3, + int s0, int s1, int s2, int s3, int s4, int s5, int s6, int s7) +{ + int p1,p2,p3,p4,p5; + //int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; + p2 = s2; + p3 = s6; + p1 = (p2+p3) * f2f(0.5411961f); + t2 = p1 + p3 * f2f(-1.847759065f); + t3 = p1 + p2 * f2f( 0.765366865f); + p2 = s0; + p3 = s4; + t0 = fsh(p2+p3); + t1 = fsh(p2-p3); + x0 = t0+t3; + x3 = t0-t3; + x1 = t1+t2; + x2 = t1-t2; + t0 = s7; + t1 = s5; + t2 = s3; + t3 = s1; + p3 = t0+t2; + p4 = t1+t3; + p1 = t0+t3; + p2 = t1+t2; + p5 = (p3+p4)*f2f( 1.175875602f); + t0 = t0*f2f( 0.298631336f); + t1 = t1*f2f( 2.053119869f); + t2 = t2*f2f( 3.072711026f); + t3 = t3*f2f( 1.501321110f); + p1 = p5 + p1*f2f(-0.899976223f); + p2 = p5 + p2*f2f(-2.562915447f); + p3 = p3*f2f(-1.961570560f); + p4 = p4*f2f(-0.390180644f); + t3 += p1+p4; + t2 += p2+p3; + t1 += p2+p4; + t0 += p1+p3; +} + +// idct and level-shift +pure void stbi__idct_block(ubyte* dst, int dst_stride, in ref short[64] data) { + int i; + int[64] val; + int* v = val.ptr; + const(short)* d = data.ptr; + + // columns + for (i=0; i < 8; ++i,++d, ++v) { + // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing + if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 + && d[40]==0 && d[48]==0 && d[56]==0) { + // no shortcut 0 seconds + // (1|2|3|4|5|6|7)==0 0 seconds + // all separate -0.047 seconds + // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds + int dcterm = d[0] << 2; + v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; + } else { + int t0,t1,t2,t3,x0,x1,x2,x3; + STBI__IDCT_1D( + t0, t1, t2, t3, + x0, x1, x2, x3, + d[ 0], d[ 8], d[16], d[24], + d[32], d[40], d[48], d[56] + ); + // constants scaled things up by 1<<12; let's bring them back + // down, but keep 2 extra bits of precision + x0 += 512; x1 += 512; x2 += 512; x3 += 512; + v[ 0] = (x0+t3) >> 10; + v[56] = (x0-t3) >> 10; + v[ 8] = (x1+t2) >> 10; + v[48] = (x1-t2) >> 10; + v[16] = (x2+t1) >> 10; + v[40] = (x2-t1) >> 10; + v[24] = (x3+t0) >> 10; + v[32] = (x3-t0) >> 10; + } + } + + ubyte* o = dst; + for (i=0, v=val.ptr; i < 8; ++i,v+=8,o+=dst_stride) { + // no fast case since the first 1D IDCT spread components out + int t0,t1,t2,t3,x0,x1,x2,x3; + STBI__IDCT_1D( + t0, t1, t2, t3, + x0, x1, x2, x3, + v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7] + ); + // constants scaled things up by 1<<12, plus we had 1<<2 from first + // loop, plus horizontal and vertical each scale by sqrt(8) so together + // we've got an extra 1<<3, so 1<<17 total we need to remove. + // so we want to round that, which means adding 0.5 * 1<<17, + // aka 65536. Also, we'll end up with -128 to 127 that we want + // to encode as 0-255 by adding 128, so we'll add that before the shift + x0 += 65536 + (128<<17); + x1 += 65536 + (128<<17); + x2 += 65536 + (128<<17); + x3 += 65536 + (128<<17); + // tried computing the shifts into temps, or'ing the temps to see + // if any were out of range, but that was slower + o[0] = stbi__clamp((x0+t3) >> 17); + o[7] = stbi__clamp((x0-t3) >> 17); + o[1] = stbi__clamp((x1+t2) >> 17); + o[6] = stbi__clamp((x1-t2) >> 17); + o[2] = stbi__clamp((x2+t1) >> 17); + o[5] = stbi__clamp((x2-t1) >> 17); + o[3] = stbi__clamp((x3+t0) >> 17); + o[4] = stbi__clamp((x3-t0) >> 17); + } +} + +// clamp to 0-255 +pure ubyte stbi__clamp(int x) { + if (cast(uint) x > 255) { + if (x < 0) return 0; + if (x > 255) return 255; + } + return cast(ubyte) x; +} + +// ------------------------------------------------------------ diff --git a/src/ext_depends/imageformats/imageformats/package.d b/src/ext_depends/imageformats/imageformats/package.d new file mode 100644 index 0000000..7f7414d --- /dev/null +++ b/src/ext_depends/imageformats/imageformats/package.d @@ -0,0 +1,455 @@ +// Copyright (c) 2014-2018 Tero Hänninen +// Boost Software License - Version 1.0 - August 17th, 2003 +module imageformats; + +import std.stdio : File, SEEK_SET, SEEK_CUR, SEEK_END; +import std.string : toLower, lastIndexOf; +import std.typecons : scoped; +public import imageformats.png; +public import imageformats.tga; +public import imageformats.bmp; +public import imageformats.jpeg; + +/// Image with 8-bit channels. Top-left corner at (0, 0). +struct IFImage { + /// width + int w; + /// height + int h; + /// channels + ColFmt c; + /// buffer + ubyte[] pixels; +} + +/// Image with 16-bit channels. Top-left corner at (0, 0). +struct IFImage16 { + /// width + int w; + /// height + int h; + /// channels + ColFmt c; + /// buffer + ushort[] pixels; +} + +/// Color format which you can pass to the read and write functions. +enum ColFmt { + Y = 1, /// Gray + YA = 2, /// Gray + Alpha + RGB = 3, /// Truecolor + RGBA = 4, /// Truecolor + Alpha +} + +/// Reads an image from file. req_chans defines the format of returned image +/// (you can use ColFmt here). +IFImage read_image(in char[] file, long req_chans = 0) { + auto reader = scoped!FileReader(file); + return read_image_from_reader(reader, req_chans); +} + +/// Reads an image from a buffer. req_chans defines the format of returned +/// image (you can use ColFmt here). +IFImage read_image_from_mem(in ubyte[] source, long req_chans = 0) { + auto reader = scoped!MemReader(source); + return read_image_from_reader(reader, req_chans); +} + +/// Writes an image to file. req_chans defines the format the image is saved in +/// (you can use ColFmt here). +void write_image(in char[] file, long w, long h, in ubyte[] data, long req_chans = 0) { + const char[] ext = extract_extension_lowercase(file); + + void function(Writer, long, long, in ubyte[], long) write_image; + switch (ext) { + case "png": write_image = &write_png; break; + case "tga": write_image = &write_tga; break; + case "bmp": write_image = &write_bmp; break; + default: throw new ImageIOException("unknown image extension/type"); + } + auto writer = scoped!FileWriter(file); + write_image(writer, w, h, data, req_chans); +} + +/// Returns width, height and color format information via w, h and chans. +/// If number of channels is unknown chans is set to zero, otherwise chans +/// values map to those of ColFmt. +void read_image_info(in char[] file, out int w, out int h, out int chans) { + auto reader = scoped!FileReader(file); + try { + return read_png_info(reader, w, h, chans); + } catch (Throwable) { + reader.seek(0, SEEK_SET); + } + try { + return read_jpeg_info(reader, w, h, chans); + } catch (Throwable) { + reader.seek(0, SEEK_SET); + } + try { + return read_bmp_info(reader, w, h, chans); + } catch (Throwable) { + reader.seek(0, SEEK_SET); + } + try { + return read_tga_info(reader, w, h, chans); + } catch (Throwable) { + reader.seek(0, SEEK_SET); + } + throw new ImageIOException("unknown image type"); +} + +/// Thrown from all the functions... +class ImageIOException : Exception { + @safe pure const + this(string msg, string file = __FILE__, size_t line = __LINE__) { + super(msg, file, line); + } +} + +private: + +IFImage read_image_from_reader(Reader reader, long req_chans) { + if (detect_png(reader)) return read_png(reader, req_chans); + if (detect_jpeg(reader)) return read_jpeg(reader, req_chans); + if (detect_bmp(reader)) return read_bmp(reader, req_chans); + if (detect_tga(reader)) return read_tga(reader, req_chans); + throw new ImageIOException("unknown image type"); +} + +// -------------------------------------------------------------------------------- +// Conversions + +package enum _ColFmt : int { + Unknown = 0, + Y = 1, + YA, + RGB, + RGBA, + BGR, + BGRA, +} + +package alias LineConv(T) = void function(in T[] src, T[] tgt); + +package LineConv!T get_converter(T)(long src_chans, long tgt_chans) pure { + long combo(long a, long b) pure nothrow { return a*16 + b; } + + if (src_chans == tgt_chans) + return ©_line!T; + + switch (combo(src_chans, tgt_chans)) with (_ColFmt) { + case combo(Y, YA) : return &Y_to_YA!T; + case combo(Y, RGB) : return &Y_to_RGB!T; + case combo(Y, RGBA) : return &Y_to_RGBA!T; + case combo(Y, BGR) : return &Y_to_BGR!T; + case combo(Y, BGRA) : return &Y_to_BGRA!T; + case combo(YA, Y) : return &YA_to_Y!T; + case combo(YA, RGB) : return &YA_to_RGB!T; + case combo(YA, RGBA) : return &YA_to_RGBA!T; + case combo(YA, BGR) : return &YA_to_BGR!T; + case combo(YA, BGRA) : return &YA_to_BGRA!T; + case combo(RGB, Y) : return &RGB_to_Y!T; + case combo(RGB, YA) : return &RGB_to_YA!T; + case combo(RGB, RGBA) : return &RGB_to_RGBA!T; + case combo(RGB, BGR) : return &RGB_to_BGR!T; + case combo(RGB, BGRA) : return &RGB_to_BGRA!T; + case combo(RGBA, Y) : return &RGBA_to_Y!T; + case combo(RGBA, YA) : return &RGBA_to_YA!T; + case combo(RGBA, RGB) : return &RGBA_to_RGB!T; + case combo(RGBA, BGR) : return &RGBA_to_BGR!T; + case combo(RGBA, BGRA) : return &RGBA_to_BGRA!T; + case combo(BGR, Y) : return &BGR_to_Y!T; + case combo(BGR, YA) : return &BGR_to_YA!T; + case combo(BGR, RGB) : return &BGR_to_RGB!T; + case combo(BGR, RGBA) : return &BGR_to_RGBA!T; + case combo(BGRA, Y) : return &BGRA_to_Y!T; + case combo(BGRA, YA) : return &BGRA_to_YA!T; + case combo(BGRA, RGB) : return &BGRA_to_RGB!T; + case combo(BGRA, RGBA) : return &BGRA_to_RGBA!T; + default : throw new ImageIOException("internal error"); + } +} + +void copy_line(T)(in T[] src, T[] tgt) pure nothrow { + tgt[0..$] = src[0..$]; +} + +T luminance(T)(T r, T g, T b) pure nothrow { + return cast(T) (0.21*r + 0.64*g + 0.15*b); // somewhat arbitrary weights +} + +void Y_to_YA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=1, t+=2) { + tgt[t] = src[k]; + tgt[t+1] = T.max; + } +} + +alias Y_to_BGR = Y_to_RGB; +void Y_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=1, t+=3) + tgt[t .. t+3] = src[k]; +} + +alias Y_to_BGRA = Y_to_RGBA; +void Y_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=1, t+=4) { + tgt[t .. t+3] = src[k]; + tgt[t+3] = T.max; + } +} + +void YA_to_Y(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=2, t+=1) + tgt[t] = src[k]; +} + +alias YA_to_BGR = YA_to_RGB; +void YA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=2, t+=3) + tgt[t .. t+3] = src[k]; +} + +alias YA_to_BGRA = YA_to_RGBA; +void YA_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=2, t+=4) { + tgt[t .. t+3] = src[k]; + tgt[t+3] = src[k+1]; + } +} + +void RGB_to_Y(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=3, t+=1) + tgt[t] = luminance(src[k], src[k+1], src[k+2]); +} + +void RGB_to_YA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=3, t+=2) { + tgt[t] = luminance(src[k], src[k+1], src[k+2]); + tgt[t+1] = T.max; + } +} + +void RGB_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=3, t+=4) { + tgt[t .. t+3] = src[k .. k+3]; + tgt[t+3] = T.max; + } +} + +void RGBA_to_Y(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=1) + tgt[t] = luminance(src[k], src[k+1], src[k+2]); +} + +void RGBA_to_YA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=2) { + tgt[t] = luminance(src[k], src[k+1], src[k+2]); + tgt[t+1] = src[k+3]; + } +} + +void RGBA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=3) + tgt[t .. t+3] = src[k .. k+3]; +} + +void BGR_to_Y(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=3, t+=1) + tgt[t] = luminance(src[k+2], src[k+1], src[k+1]); +} + +void BGR_to_YA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=3, t+=2) { + tgt[t] = luminance(src[k+2], src[k+1], src[k+1]); + tgt[t+1] = T.max; + } +} + +alias RGB_to_BGR = BGR_to_RGB; +void BGR_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k; k < src.length; k+=3) { + tgt[k ] = src[k+2]; + tgt[k+1] = src[k+1]; + tgt[k+2] = src[k ]; + } +} + +alias RGB_to_BGRA = BGR_to_RGBA; +void BGR_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=3, t+=4) { + tgt[t ] = src[k+2]; + tgt[t+1] = src[k+1]; + tgt[t+2] = src[k ]; + tgt[t+3] = T.max; + } +} + +void BGRA_to_Y(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=1) + tgt[t] = luminance(src[k+2], src[k+1], src[k]); +} + +void BGRA_to_YA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=2) { + tgt[t] = luminance(src[k+2], src[k+1], src[k]); + tgt[t+1] = T.max; + } +} + +alias RGBA_to_BGR = BGRA_to_RGB; +void BGRA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=3) { + tgt[t ] = src[k+2]; + tgt[t+1] = src[k+1]; + tgt[t+2] = src[k ]; + } +} + +alias RGBA_to_BGRA = BGRA_to_RGBA; +void BGRA_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { + for (size_t k, t; k < src.length; k+=4, t+=4) { + tgt[t ] = src[k+2]; + tgt[t+1] = src[k+1]; + tgt[t+2] = src[k ]; + tgt[t+3] = src[k+3]; + } +} + +// -------------------------------------------------------------------------------- + +package interface Reader { + void readExact(ubyte[], size_t); + void seek(ptrdiff_t, int); +} + +package interface Writer { + void rawWrite(in ubyte[]); + void flush(); +} + +package class FileReader : Reader { + this(in char[] filename) { + this(File(filename.idup, "rb")); + } + + this(File f) { + if (!f.isOpen) throw new ImageIOException("File not open"); + this.f = f; + } + + void readExact(ubyte[] buffer, size_t bytes) { + auto slice = this.f.rawRead(buffer[0..bytes]); + if (slice.length != bytes) + throw new Exception("not enough data"); + } + + void seek(ptrdiff_t offset, int origin) { this.f.seek(offset, origin); } + + private File f; +} + +package class MemReader : Reader { + this(in ubyte[] source) { + this.source = source; + } + + void readExact(ubyte[] buffer, size_t bytes) { + if (source.length - cursor < bytes) + throw new Exception("not enough data"); + buffer[0..bytes] = source[cursor .. cursor+bytes]; + cursor += bytes; + } + + void seek(ptrdiff_t offset, int origin) { + switch (origin) { + case SEEK_SET: + if (offset < 0 || source.length <= offset) + throw new Exception("seek error"); + cursor = offset; + break; + case SEEK_CUR: + ptrdiff_t dst = cursor + offset; + if (dst < 0 || source.length <= dst) + throw new Exception("seek error"); + cursor = dst; + break; + case SEEK_END: + if (0 <= offset || source.length < -offset) + throw new Exception("seek error"); + cursor = cast(ptrdiff_t) source.length + offset; + break; + default: assert(0); + } + } + + private const ubyte[] source; + private ptrdiff_t cursor; +} + +package class FileWriter : Writer { + this(in char[] filename) { + this(File(filename.idup, "wb")); + } + + this(File f) { + if (!f.isOpen) throw new ImageIOException("File not open"); + this.f = f; + } + + void rawWrite(in ubyte[] block) { this.f.rawWrite(block); } + void flush() { this.f.flush(); } + + private File f; +} + +package class MemWriter : Writer { + this() { } + + ubyte[] result() { return buffer; } + + void rawWrite(in ubyte[] block) { this.buffer ~= block; } + void flush() { } + + private ubyte[] buffer; +} + +const(char)[] extract_extension_lowercase(in char[] filename) { + ptrdiff_t di = filename.lastIndexOf('.'); + return (0 < di && di+1 < filename.length) ? filename[di+1..$].toLower() : ""; +} + +unittest { + // The TGA and BMP files are not as varied in format as the PNG files, so + // not as well tested. + string png_path = "tests/pngsuite/"; + string tga_path = "tests/pngsuite-tga/"; + string bmp_path = "tests/pngsuite-bmp/"; + + auto files = [ + "basi0g08", // PNG image data, 32 x 32, 8-bit grayscale, interlaced + "basi2c08", // PNG image data, 32 x 32, 8-bit/color RGB, interlaced + "basi3p08", // PNG image data, 32 x 32, 8-bit colormap, interlaced + "basi4a08", // PNG image data, 32 x 32, 8-bit gray+alpha, interlaced + "basi6a08", // PNG image data, 32 x 32, 8-bit/color RGBA, interlaced + "basn0g08", // PNG image data, 32 x 32, 8-bit grayscale, non-interlaced + "basn2c08", // PNG image data, 32 x 32, 8-bit/color RGB, non-interlaced + "basn3p08", // PNG image data, 32 x 32, 8-bit colormap, non-interlaced + "basn4a08", // PNG image data, 32 x 32, 8-bit gray+alpha, non-interlaced + "basn6a08", // PNG image data, 32 x 32, 8-bit/color RGBA, non-interlaced + ]; + + foreach (file; files) { + //writefln("%s", file); + auto a = read_image(png_path ~ file ~ ".png", ColFmt.RGBA); + auto b = read_image(tga_path ~ file ~ ".tga", ColFmt.RGBA); + auto c = read_image(bmp_path ~ file ~ ".bmp", ColFmt.RGBA); + assert(a.w == b.w && a.w == c.w); + assert(a.h == b.h && a.h == c.h); + assert(a.pixels.length == b.pixels.length && a.pixels.length == c.pixels.length); + assert(a.pixels[0..$] == b.pixels[0..$], "png/tga"); + assert(a.pixels[0..$] == c.pixels[0..$], "png/bmp"); + } +} diff --git a/src/ext_depends/imageformats/imageformats/png.d b/src/ext_depends/imageformats/imageformats/png.d new file mode 100644 index 0000000..2fa9c56 --- /dev/null +++ b/src/ext_depends/imageformats/imageformats/png.d @@ -0,0 +1,789 @@ +module imageformats.png; + +import etc.c.zlib; +import std.algorithm : min, reverse; +import std.bitmanip : bigEndianToNative, nativeToBigEndian; +import std.stdio : File, SEEK_SET; +import std.digest.crc : CRC32, crc32Of; +import std.typecons : scoped; +import imageformats; + +private: + +/// Header of a PNG file. +public struct PNG_Header { + int width; + int height; + ubyte bit_depth; + ubyte color_type; + ubyte compression_method; + ubyte filter_method; + ubyte interlace_method; +} + +/// Returns the header of a PNG file. +public PNG_Header read_png_header(in char[] filename) { + auto reader = scoped!FileReader(filename); + return read_png_header(reader); +} + +/// Returns the header of the image in the buffer. +public PNG_Header read_png_header_from_mem(in ubyte[] source) { + auto reader = scoped!MemReader(source); + return read_png_header(reader); +} + +/// Reads an 8-bit or 16-bit PNG image and returns it as an 8-bit image. +/// req_chans defines the format of returned image (you can use ColFmt here). +public IFImage read_png(in char[] filename, long req_chans = 0) { + auto reader = scoped!FileReader(filename); + return read_png(reader, req_chans); +} + +/// Reads an 8-bit or 16-bit PNG image from a buffer and returns it as an +/// 8-bit image. req_chans defines the format of returned image (you can use +/// ColFmt here). +public IFImage read_png_from_mem(in ubyte[] source, long req_chans = 0) { + auto reader = scoped!MemReader(source); + return read_png(reader, req_chans); +} + +/// Reads an 8-bit or 16-bit PNG image and returns it as a 16-bit image. +/// req_chans defines the format of returned image (you can use ColFmt here). +public IFImage16 read_png16(in char[] filename, long req_chans = 0) { + auto reader = scoped!FileReader(filename); + return read_png16(reader, req_chans); +} + +/// Reads an 8-bit or 16-bit PNG image from a buffer and returns it as a +/// 16-bit image. req_chans defines the format of returned image (you can use +/// ColFmt here). +public IFImage16 read_png16_from_mem(in ubyte[] source, long req_chans = 0) { + auto reader = scoped!MemReader(source); + return read_png16(reader, req_chans); +} + +/// Writes a PNG image into a file. +public void write_png(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) +{ + auto writer = scoped!FileWriter(file); + write_png(writer, w, h, data, tgt_chans); +} + +/// Writes a PNG image into a buffer. +public ubyte[] write_png_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { + auto writer = scoped!MemWriter(); + write_png(writer, w, h, data, tgt_chans); + return writer.result; +} + +/// Returns width, height and color format information via w, h and chans. +public void read_png_info(in char[] filename, out int w, out int h, out int chans) { + auto reader = scoped!FileReader(filename); + return read_png_info(reader, w, h, chans); +} + +/// Returns width, height and color format information via w, h and chans. +public void read_png_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { + auto reader = scoped!MemReader(source); + return read_png_info(reader, w, h, chans); +} + +// Detects whether a PNG image is readable from stream. +package bool detect_png(Reader stream) { + try { + ubyte[8] tmp = void; + stream.readExact(tmp, tmp.length); + return (tmp[0..8] == png_file_header[0..$]); + } catch (Throwable) { + return false; + } finally { + stream.seek(0, SEEK_SET); + } +} + +PNG_Header read_png_header(Reader stream) { + ubyte[33] tmp = void; // file header, IHDR len+type+data+crc + stream.readExact(tmp, tmp.length); + + ubyte[4] crc = crc32Of(tmp[12..29]); + reverse(crc[]); + if ( tmp[0..8] != png_file_header[0..$] || + tmp[8..16] != png_image_header || + crc != tmp[29..33] ) + throw new ImageIOException("corrupt header"); + + PNG_Header header = { + width : bigEndianToNative!int(tmp[16..20]), + height : bigEndianToNative!int(tmp[20..24]), + bit_depth : tmp[24], + color_type : tmp[25], + compression_method : tmp[26], + filter_method : tmp[27], + interlace_method : tmp[28], + }; + return header; +} + +package IFImage read_png(Reader stream, long req_chans = 0) { + PNG_Decoder dc = init_png_decoder(stream, req_chans, 8); + IFImage result = { + w : dc.w, + h : dc.h, + c : cast(ColFmt) dc.tgt_chans, + pixels : decode_png(dc).bpc8 + }; + return result; +} + +IFImage16 read_png16(Reader stream, long req_chans = 0) { + PNG_Decoder dc = init_png_decoder(stream, req_chans, 16); + IFImage16 result = { + w : dc.w, + h : dc.h, + c : cast(ColFmt) dc.tgt_chans, + pixels : decode_png(dc).bpc16 + }; + return result; +} + +PNG_Decoder init_png_decoder(Reader stream, long req_chans, int req_bpc) { + if (req_chans < 0 || 4 < req_chans) + throw new ImageIOException("come on..."); + + PNG_Header hdr = read_png_header(stream); + + if (hdr.width < 1 || hdr.height < 1 || int.max < cast(ulong) hdr.width * hdr.height) + throw new ImageIOException("invalid dimensions"); + if ((hdr.bit_depth != 8 && hdr.bit_depth != 16) || (req_bpc != 8 && req_bpc != 16)) + throw new ImageIOException("only 8-bit and 16-bit images supported"); + if (! (hdr.color_type == PNG_ColorType.Y || + hdr.color_type == PNG_ColorType.RGB || + hdr.color_type == PNG_ColorType.Idx || + hdr.color_type == PNG_ColorType.YA || + hdr.color_type == PNG_ColorType.RGBA) ) + throw new ImageIOException("color type not supported"); + if (hdr.compression_method != 0 || hdr.filter_method != 0 || + (hdr.interlace_method != 0 && hdr.interlace_method != 1)) + throw new ImageIOException("not supported"); + + PNG_Decoder dc = { + stream : stream, + src_indexed : (hdr.color_type == PNG_ColorType.Idx), + src_chans : channels(cast(PNG_ColorType) hdr.color_type), + bpc : hdr.bit_depth, + req_bpc : req_bpc, + ilace : hdr.interlace_method, + w : hdr.width, + h : hdr.height, + }; + dc.tgt_chans = (req_chans == 0) ? dc.src_chans : cast(int) req_chans; + return dc; +} + +immutable ubyte[8] png_file_header = + [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + +immutable ubyte[8] png_image_header = + [0x0, 0x0, 0x0, 0xd, 'I','H','D','R']; + +int channels(PNG_ColorType ct) pure nothrow { + final switch (ct) with (PNG_ColorType) { + case Y: return 1; + case RGB: return 3; + case YA: return 2; + case RGBA, Idx: return 4; + } +} + +PNG_ColorType color_type(long channels) pure nothrow { + switch (channels) { + case 1: return PNG_ColorType.Y; + case 2: return PNG_ColorType.YA; + case 3: return PNG_ColorType.RGB; + case 4: return PNG_ColorType.RGBA; + default: assert(0); + } +} + +struct PNG_Decoder { + Reader stream; + bool src_indexed; + int src_chans; + int tgt_chans; + int bpc; + int req_bpc; + int w, h; + ubyte ilace; + + CRC32 crc; + ubyte[12] chunkmeta; // crc | length and type + ubyte[] read_buf; + ubyte[] palette; + ubyte[] transparency; + + // decompression + z_stream* z; // zlib stream + uint avail_idat; // available bytes in current idat chunk + ubyte[] idat_window; // slice of read_buf +} + +Buffer decode_png(ref PNG_Decoder dc) { + dc.read_buf = new ubyte[4096]; + + enum Stage { + IHDR_parsed, + PLTE_parsed, + IDAT_parsed, + IEND_parsed, + } + + Buffer result; + auto stage = Stage.IHDR_parsed; + dc.stream.readExact(dc.chunkmeta[4..$], 8); // next chunk's len and type + + while (stage != Stage.IEND_parsed) { + int len = bigEndianToNative!int(dc.chunkmeta[4..8]); + if (len < 0) + throw new ImageIOException("chunk too long"); + + // standard allows PLTE chunk for non-indexed images too but we don't + dc.crc.put(dc.chunkmeta[8..12]); // type + switch (cast(char[]) dc.chunkmeta[8..12]) { // chunk type + case "IDAT": + if (! (stage == Stage.IHDR_parsed || + (stage == Stage.PLTE_parsed && dc.src_indexed)) ) + throw new ImageIOException("corrupt chunk stream"); + result = read_IDAT_stream(dc, len); + dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type + ubyte[4] crc = dc.crc.finish; + reverse(crc[]); + if (crc != dc.chunkmeta[0..4]) + throw new ImageIOException("corrupt chunk"); + stage = Stage.IDAT_parsed; + break; + case "PLTE": + if (stage != Stage.IHDR_parsed) + throw new ImageIOException("corrupt chunk stream"); + int entries = len / 3; + if (len % 3 != 0 || 256 < entries) + throw new ImageIOException("corrupt chunk"); + dc.palette = new ubyte[len]; + dc.stream.readExact(dc.palette, dc.palette.length); + dc.crc.put(dc.palette); + dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type + ubyte[4] crc = dc.crc.finish; + reverse(crc[]); + if (crc != dc.chunkmeta[0..4]) + throw new ImageIOException("corrupt chunk"); + stage = Stage.PLTE_parsed; + break; + case "tRNS": + if (! (stage == Stage.IHDR_parsed || + (stage == Stage.PLTE_parsed && dc.src_indexed)) ) + throw new ImageIOException("corrupt chunk stream"); + if (dc.src_indexed) { + size_t entries = dc.palette.length / 3; + if (len > entries) + throw new ImageIOException("corrupt chunk"); + } + dc.transparency = new ubyte[len]; + dc.stream.readExact(dc.transparency, dc.transparency.length); + dc.stream.readExact(dc.chunkmeta, 12); + dc.crc.put(dc.transparency); + ubyte[4] crc = dc.crc.finish; + reverse(crc[]); + if (crc != dc.chunkmeta[0..4]) + throw new ImageIOException("corrupt chunk"); + break; + case "IEND": + if (stage != Stage.IDAT_parsed) + throw new ImageIOException("corrupt chunk stream"); + dc.stream.readExact(dc.chunkmeta, 4); // crc + static immutable ubyte[4] expectedCRC = [0xae, 0x42, 0x60, 0x82]; + if (len != 0 || dc.chunkmeta[0..4] != expectedCRC) + throw new ImageIOException("corrupt chunk"); + stage = Stage.IEND_parsed; + break; + case "IHDR": + throw new ImageIOException("corrupt chunk stream"); + default: + // unknown chunk, ignore but check crc + while (0 < len) { + size_t bytes = min(len, dc.read_buf.length); + dc.stream.readExact(dc.read_buf, bytes); + len -= bytes; + dc.crc.put(dc.read_buf[0..bytes]); + } + dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type + ubyte[4] crc = dc.crc.finish; + reverse(crc[]); + if (crc != dc.chunkmeta[0..4]) + throw new ImageIOException("corrupt chunk"); + } + } + + return result; +} + +enum PNG_ColorType : ubyte { + Y = 0, + RGB = 2, + Idx = 3, + YA = 4, + RGBA = 6, +} + +enum PNG_FilterType : ubyte { + None = 0, + Sub = 1, + Up = 2, + Average = 3, + Paeth = 4, +} + +enum InterlaceMethod { + None = 0, Adam7 = 1 +} + +union Buffer { + ubyte[] bpc8; + ushort[] bpc16; +} + +Buffer read_IDAT_stream(ref PNG_Decoder dc, int len) { + assert(dc.req_bpc == 8 || dc.req_bpc == 16); + + // initialize zlib stream + z_stream z = { zalloc: null, zfree: null, opaque: null }; + if (inflateInit(&z) != Z_OK) + throw new ImageIOException("can't init zlib"); + dc.z = &z; + dc.avail_idat = len; + scope(exit) + inflateEnd(&z); + + const size_t filter_step = dc.src_indexed + ? 1 : dc.src_chans * (dc.bpc == 8 ? 1 : 2); + + ubyte[] depaletted = dc.src_indexed ? new ubyte[dc.w * 4] : null; + + auto cline = new ubyte[dc.w * filter_step + 1]; // +1 for filter type byte + auto pline = new ubyte[dc.w * filter_step + 1]; // +1 for filter type byte + auto cline8 = (dc.req_bpc == 8 && dc.bpc != 8) ? new ubyte[dc.w * dc.src_chans] : null; + auto cline16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.src_chans] : null; + ubyte[] result8 = (dc.req_bpc == 8) ? new ubyte[dc.w * dc.h * dc.tgt_chans] : null; + ushort[] result16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.h * dc.tgt_chans] : null; + + const LineConv!ubyte convert8 = get_converter!ubyte(dc.src_chans, dc.tgt_chans); + const LineConv!ushort convert16 = get_converter!ushort(dc.src_chans, dc.tgt_chans); + + if (dc.ilace == InterlaceMethod.None) { + const size_t src_linelen = dc.w * dc.src_chans; + const size_t tgt_linelen = dc.w * dc.tgt_chans; + + size_t ti = 0; // target index + foreach (j; 0 .. dc.h) { + uncompress(dc, cline); + ubyte filter_type = cline[0]; + + recon(cline[1..$], pline[1..$], filter_type, filter_step); + + ubyte[] bytes; // defiltered bytes or 8-bit samples from palette + if (dc.src_indexed) { + depalette(dc.palette, dc.transparency, cline[1..$], depaletted); + bytes = depaletted[0 .. src_linelen]; + } else { + bytes = cline[1..$]; + } + + // convert colors + if (dc.req_bpc == 8) { + line8_from_bytes(bytes, dc.bpc, cline8); + convert8(cline8[0 .. src_linelen], result8[ti .. ti + tgt_linelen]); + } else { + line16_from_bytes(bytes, dc.bpc, cline16); + convert16(cline16[0 .. src_linelen], result16[ti .. ti + tgt_linelen]); + } + + ti += tgt_linelen; + + ubyte[] _swap = pline; + pline = cline; + cline = _swap; + } + } else { + // Adam7 interlacing + + immutable size_t[7] redw = [(dc.w + 7) / 8, + (dc.w + 3) / 8, + (dc.w + 3) / 4, + (dc.w + 1) / 4, + (dc.w + 1) / 2, + (dc.w + 0) / 2, + (dc.w + 0) / 1]; + + immutable size_t[7] redh = [(dc.h + 7) / 8, + (dc.h + 7) / 8, + (dc.h + 3) / 8, + (dc.h + 3) / 4, + (dc.h + 1) / 4, + (dc.h + 1) / 2, + (dc.h + 0) / 2]; + + auto redline8 = (dc.req_bpc == 8) ? new ubyte[dc.w * dc.tgt_chans] : null; + auto redline16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.tgt_chans] : null; + + foreach (pass; 0 .. 7) { + const A7_Catapult tgt_px = a7_catapults[pass]; // target pixel + const size_t src_linelen = redw[pass] * dc.src_chans; + ubyte[] cln = cline[0 .. redw[pass] * filter_step + 1]; + ubyte[] pln = pline[0 .. redw[pass] * filter_step + 1]; + pln[] = 0; + + foreach (j; 0 .. redh[pass]) { + uncompress(dc, cln); + ubyte filter_type = cln[0]; + + recon(cln[1..$], pln[1..$], filter_type, filter_step); + + ubyte[] bytes; // defiltered bytes or 8-bit samples from palette + if (dc.src_indexed) { + depalette(dc.palette, dc.transparency, cln[1..$], depaletted); + bytes = depaletted[0 .. src_linelen]; + } else { + bytes = cln[1..$]; + } + + // convert colors and sling pixels from reduced image to final buffer + if (dc.req_bpc == 8) { + line8_from_bytes(bytes, dc.bpc, cline8); + convert8(cline8[0 .. src_linelen], redline8[0 .. redw[pass]*dc.tgt_chans]); + for (size_t i, redi; i < redw[pass]; ++i, redi += dc.tgt_chans) { + size_t tgt = tgt_px(i, j, dc.w) * dc.tgt_chans; + result8[tgt .. tgt + dc.tgt_chans] = + redline8[redi .. redi + dc.tgt_chans]; + } + } else { + line16_from_bytes(bytes, dc.bpc, cline16); + convert16(cline16[0 .. src_linelen], redline16[0 .. redw[pass]*dc.tgt_chans]); + for (size_t i, redi; i < redw[pass]; ++i, redi += dc.tgt_chans) { + size_t tgt = tgt_px(i, j, dc.w) * dc.tgt_chans; + result16[tgt .. tgt + dc.tgt_chans] = + redline16[redi .. redi + dc.tgt_chans]; + } + } + + ubyte[] _swap = pln; + pln = cln; + cln = _swap; + } + } + } + + Buffer result; + switch (dc.req_bpc) { + case 8: result.bpc8 = result8; return result; + case 16: result.bpc16 = result16; return result; + default: throw new ImageIOException("internal error"); + } +} + +void line8_from_bytes(ubyte[] src, int bpc, ref ubyte[] tgt) { + switch (bpc) { + case 8: + tgt = src; + break; + case 16: + for (size_t k, t; k < src.length; k+=2, t+=1) { tgt[t] = src[k]; /* truncate */ } + break; + default: throw new ImageIOException("unsupported bit depth (and bug)"); + } +} + +void line16_from_bytes(in ubyte[] src, int bpc, ushort[] tgt) { + switch (bpc) { + case 8: + for (size_t k; k < src.length; k+=1) { tgt[k] = src[k] * 256 + 128; } + break; + case 16: + for (size_t k, t; k < src.length; k+=2, t+=1) { tgt[t] = src[k] << 8 | src[k+1]; } + break; + default: throw new ImageIOException("unsupported bit depth (and bug)"); + } +} + +void depalette(in ubyte[] palette, in ubyte[] transparency, in ubyte[] src_line, ubyte[] depaletted) pure { + for (size_t s, d; s < src_line.length; s+=1, d+=4) { + ubyte pid = src_line[s]; + size_t pidx = pid * 3; + if (palette.length < pidx + 3) + throw new ImageIOException("palette index wrong"); + depaletted[d .. d+3] = palette[pidx .. pidx+3]; + depaletted[d+3] = (pid < transparency.length) ? transparency[pid] : 255; + } +} + +alias A7_Catapult = size_t function(size_t redx, size_t redy, size_t dstw); +immutable A7_Catapult[7] a7_catapults = [ + &a7_red1_to_dst, + &a7_red2_to_dst, + &a7_red3_to_dst, + &a7_red4_to_dst, + &a7_red5_to_dst, + &a7_red6_to_dst, + &a7_red7_to_dst, +]; + +pure nothrow { + size_t a7_red1_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*8*dstw + redx*8; } + size_t a7_red2_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*8*dstw + redx*8+4; } + size_t a7_red3_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*8+4)*dstw + redx*4; } + size_t a7_red4_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*4*dstw + redx*4+2; } + size_t a7_red5_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*4+2)*dstw + redx*2; } + size_t a7_red6_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*2*dstw + redx*2+1; } + size_t a7_red7_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*2+1)*dstw + redx; } +} + +// Uncompresses a line from the IDAT stream into dst. +void uncompress(ref PNG_Decoder dc, ubyte[] dst) +{ + dc.z.avail_out = cast(uint) dst.length; + dc.z.next_out = dst.ptr; + + while (true) { + if (!dc.z.avail_in) { + if (!dc.avail_idat) { + dc.stream.readExact(dc.chunkmeta, 12); // crc | len & type + ubyte[4] crc = dc.crc.finish; + reverse(crc[]); + if (crc != dc.chunkmeta[0..4]) + throw new ImageIOException("corrupt chunk"); + dc.avail_idat = bigEndianToNative!uint(dc.chunkmeta[4..8]); + if (!dc.avail_idat) + throw new ImageIOException("invalid data"); + if (dc.chunkmeta[8..12] != "IDAT") + throw new ImageIOException("not enough data"); + dc.crc.put(dc.chunkmeta[8..12]); + } + + const size_t n = min(dc.avail_idat, dc.read_buf.length); + dc.stream.readExact(dc.read_buf, n); + dc.idat_window = dc.read_buf[0..n]; + + if (!dc.idat_window) + throw new ImageIOException("TODO"); + dc.crc.put(dc.idat_window); + dc.avail_idat -= cast(uint) dc.idat_window.length; + dc.z.avail_in = cast(uint) dc.idat_window.length; + dc.z.next_in = dc.idat_window.ptr; + } + + int q = inflate(dc.z, Z_NO_FLUSH); + + if (dc.z.avail_out == 0) + return; + if (q != Z_OK) + throw new ImageIOException("zlib error"); + } +} + +void recon(ubyte[] cline, in ubyte[] pline, ubyte ftype, size_t fstep) pure { + switch (ftype) with (PNG_FilterType) { + case None: + break; + case Sub: + foreach (k; fstep .. cline.length) + cline[k] += cline[k-fstep]; + break; + case Up: + foreach (k; 0 .. cline.length) + cline[k] += pline[k]; + break; + case Average: + foreach (k; 0 .. fstep) + cline[k] += pline[k] / 2; + foreach (k; fstep .. cline.length) + cline[k] += cast(ubyte) + ((cast(uint) cline[k-fstep] + cast(uint) pline[k]) / 2); + break; + case Paeth: + foreach (i; 0 .. fstep) + cline[i] += paeth(0, pline[i], 0); + foreach (i; fstep .. cline.length) + cline[i] += paeth(cline[i-fstep], pline[i], pline[i-fstep]); + break; + default: + throw new ImageIOException("filter type not supported"); + } +} + +ubyte paeth(ubyte a, ubyte b, ubyte c) pure nothrow { + int pc = c; + int pa = b - pc; + int pb = a - pc; + pc = pa + pb; + if (pa < 0) pa = -pa; + if (pb < 0) pb = -pb; + if (pc < 0) pc = -pc; + + if (pa <= pb && pa <= pc) { + return a; + } else if (pb <= pc) { + return b; + } + return c; +} + +// ---------------------------------------------------------------------- +// PNG encoder + +void write_png(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { + if (w < 1 || h < 1 || int.max < w || int.max < h) + throw new ImageIOException("invalid dimensions"); + uint src_chans = cast(uint) (data.length / w / h); + if (src_chans < 1 || 4 < src_chans || tgt_chans < 0 || 4 < tgt_chans) + throw new ImageIOException("invalid channel count"); + if (src_chans * w * h != data.length) + throw new ImageIOException("mismatching dimensions and length"); + + PNG_Encoder ec = { + stream : stream, + w : cast(size_t) w, + h : cast(size_t) h, + src_chans : src_chans, + tgt_chans : tgt_chans ? cast(uint) tgt_chans : src_chans, + data : data, + }; + + write_png(ec); + stream.flush(); +} + +enum MAXIMUM_CHUNK_SIZE = 8192; + +struct PNG_Encoder { + Writer stream; + size_t w, h; + uint src_chans; + uint tgt_chans; + const(ubyte)[] data; + + CRC32 crc; + z_stream* z; + ubyte[] idatbuf; +} + +void write_png(ref PNG_Encoder ec) { + ubyte[33] hdr = void; + hdr[ 0 .. 8] = png_file_header; + hdr[ 8 .. 16] = png_image_header; + hdr[16 .. 20] = nativeToBigEndian(cast(uint) ec.w); + hdr[20 .. 24] = nativeToBigEndian(cast(uint) ec.h); + hdr[24 ] = 8; // bit depth + hdr[25 ] = color_type(ec.tgt_chans); + hdr[26 .. 29] = 0; // compression, filter and interlace methods + ec.crc.start(); + ec.crc.put(hdr[12 .. 29]); + ubyte[4] crc = ec.crc.finish(); + reverse(crc[]); + hdr[29 .. 33] = crc; + ec.stream.rawWrite(hdr); + + write_IDATs(ec); + + static immutable ubyte[12] iend = + [0, 0, 0, 0, 'I','E','N','D', 0xae, 0x42, 0x60, 0x82]; + ec.stream.rawWrite(iend); +} + +void write_IDATs(ref PNG_Encoder ec) { + // initialize zlib stream + z_stream z = { zalloc: null, zfree: null, opaque: null }; + if (deflateInit(&z, Z_DEFAULT_COMPRESSION) != Z_OK) + throw new ImageIOException("zlib init error"); + scope(exit) + deflateEnd(ec.z); + ec.z = &z; + + const LineConv!ubyte convert = get_converter!ubyte(ec.src_chans, ec.tgt_chans); + + const size_t filter_step = ec.tgt_chans; // step between pixels, in bytes + const size_t slinesz = ec.w * ec.src_chans; + const size_t tlinesz = ec.w * ec.tgt_chans + 1; + const size_t workbufsz = 3 * tlinesz + MAXIMUM_CHUNK_SIZE; + + ubyte[] workbuf = new ubyte[workbufsz]; + ubyte[] cline = workbuf[0 .. tlinesz]; + ubyte[] pline = workbuf[tlinesz .. 2 * tlinesz]; + ubyte[] filtered = workbuf[2 * tlinesz .. 3 * tlinesz]; + ec.idatbuf = workbuf[$-MAXIMUM_CHUNK_SIZE .. $]; + workbuf[0..$] = 0; + ec.z.avail_out = cast(uint) ec.idatbuf.length; + ec.z.next_out = ec.idatbuf.ptr; + + const size_t ssize = ec.w * ec.src_chans * ec.h; + + for (size_t si; si < ssize; si += slinesz) { + convert(ec.data[si .. si + slinesz], cline[1..$]); + + // these loops could be merged with some extra space... + foreach (i; 1 .. filter_step+1) + filtered[i] = cast(ubyte) (cline[i] - paeth(0, pline[i], 0)); + foreach (i; filter_step+1 .. tlinesz) + filtered[i] = cast(ubyte) + (cline[i] - paeth(cline[i-filter_step], pline[i], pline[i-filter_step])); + filtered[0] = PNG_FilterType.Paeth; + + compress(ec, filtered); + ubyte[] _swap = pline; + pline = cline; + cline = _swap; + } + + while (true) { // flush zlib + int q = deflate(ec.z, Z_FINISH); + if (ec.idatbuf.length - ec.z.avail_out > 0) + flush_idat(ec); + if (q == Z_STREAM_END) break; + if (q == Z_OK) continue; // not enough avail_out + throw new ImageIOException("zlib compression error"); + } +} + +void compress(ref PNG_Encoder ec, in ubyte[] line) +{ + ec.z.avail_in = cast(uint) line.length; + ec.z.next_in = line.ptr; + while (ec.z.avail_in) { + int q = deflate(ec.z, Z_NO_FLUSH); + if (q != Z_OK) + throw new ImageIOException("zlib compression error"); + if (ec.z.avail_out == 0) + flush_idat(ec); + } +} + +void flush_idat(ref PNG_Encoder ec) // writes an idat chunk +{ + const uint len = cast(uint) (ec.idatbuf.length - ec.z.avail_out); + ec.crc.put(cast(const(ubyte)[]) "IDAT"); + ec.crc.put(ec.idatbuf[0 .. len]); + ubyte[8] meta; + meta[0..4] = nativeToBigEndian!uint(len); + meta[4..8] = cast(ubyte[4]) "IDAT"; + ec.stream.rawWrite(meta); + ec.stream.rawWrite(ec.idatbuf[0 .. len]); + ubyte[4] crc = ec.crc.finish(); + reverse(crc[]); + ec.stream.rawWrite(crc[0..$]); + ec.z.next_out = ec.idatbuf.ptr; + ec.z.avail_out = cast(uint) ec.idatbuf.length; +} + +package void read_png_info(Reader stream, out int w, out int h, out int chans) { + PNG_Header hdr = read_png_header(stream); + w = hdr.width; + h = hdr.height; + chans = channels(cast(PNG_ColorType) hdr.color_type); +} diff --git a/src/ext_depends/imageformats/imageformats/tga.d b/src/ext_depends/imageformats/imageformats/tga.d new file mode 100644 index 0000000..afc3d0e --- /dev/null +++ b/src/ext_depends/imageformats/imageformats/tga.d @@ -0,0 +1,456 @@ +module imageformats.tga; + +import std.algorithm : min; +import std.bitmanip : littleEndianToNative, nativeToLittleEndian; +import std.stdio : File, SEEK_SET, SEEK_CUR; +import std.typecons : scoped; +import imageformats; + +private: + +/// Header of a TGA file. +public struct TGA_Header { + ubyte id_length; + ubyte palette_type; + ubyte data_type; + ushort palette_start; + ushort palette_length; + ubyte palette_bits; + ushort x_origin; + ushort y_origin; + ushort width; + ushort height; + ubyte bits_pp; + ubyte flags; +} + +/// Returns the header of a TGA file. +public TGA_Header read_tga_header(in char[] filename) { + auto reader = scoped!FileReader(filename); + return read_tga_header(reader); +} + +/// Reads the image header from a buffer containing a TGA image. +public TGA_Header read_tga_header_from_mem(in ubyte[] source) { + auto reader = scoped!MemReader(source); + return read_tga_header(reader); +} + +/// Reads a TGA image. req_chans defines the format of returned image +/// (you can use ColFmt here). +public IFImage read_tga(in char[] filename, long req_chans = 0) { + auto reader = scoped!FileReader(filename); + return read_tga(reader, req_chans); +} + +/// Reads an image from a buffer containing a TGA image. req_chans defines the +/// format of returned image (you can use ColFmt here). +public IFImage read_tga_from_mem(in ubyte[] source, long req_chans = 0) { + auto reader = scoped!MemReader(source); + return read_tga(reader, req_chans); +} + +/// Writes a TGA image into a file. +public void write_tga(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) +{ + auto writer = scoped!FileWriter(file); + write_tga(writer, w, h, data, tgt_chans); +} + +/// Writes a TGA image into a buffer. +public ubyte[] write_tga_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { + auto writer = scoped!MemWriter(); + write_tga(writer, w, h, data, tgt_chans); + return writer.result; +} + +/// Returns width, height and color format information via w, h and chans. +public void read_tga_info(in char[] filename, out int w, out int h, out int chans) { + auto reader = scoped!FileReader(filename); + return read_tga_info(reader, w, h, chans); +} + +/// Returns width, height and color format information via w, h and chans. +public void read_tga_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { + auto reader = scoped!MemReader(source); + return read_tga_info(reader, w, h, chans); +} + +// Detects whether a TGA image is readable from stream. +package bool detect_tga(Reader stream) { + try { + auto hdr = read_tga_header(stream); + return true; + } catch (Throwable) { + return false; + } finally { + stream.seek(0, SEEK_SET); + } +} + +TGA_Header read_tga_header(Reader stream) { + ubyte[18] tmp = void; + stream.readExact(tmp, tmp.length); + + TGA_Header hdr = { + id_length : tmp[0], + palette_type : tmp[1], + data_type : tmp[2], + palette_start : littleEndianToNative!ushort(tmp[3..5]), + palette_length : littleEndianToNative!ushort(tmp[5..7]), + palette_bits : tmp[7], + x_origin : littleEndianToNative!ushort(tmp[8..10]), + y_origin : littleEndianToNative!ushort(tmp[10..12]), + width : littleEndianToNative!ushort(tmp[12..14]), + height : littleEndianToNative!ushort(tmp[14..16]), + bits_pp : tmp[16], + flags : tmp[17], + }; + + if (hdr.width < 1 || hdr.height < 1 || hdr.palette_type > 1 + || (hdr.palette_type == 0 && (hdr.palette_start + || hdr.palette_length + || hdr.palette_bits)) + || (4 <= hdr.data_type && hdr.data_type <= 8) || 12 <= hdr.data_type) + throw new ImageIOException("corrupt TGA header"); + + return hdr; +} + +package IFImage read_tga(Reader stream, long req_chans = 0) { + if (req_chans < 0 || 4 < req_chans) + throw new ImageIOException("come on..."); + + TGA_Header hdr = read_tga_header(stream); + + if (hdr.width < 1 || hdr.height < 1) + throw new ImageIOException("invalid dimensions"); + if (hdr.flags & 0xc0) // two bits + throw new ImageIOException("interlaced TGAs not supported"); + if (hdr.flags & 0x10) + throw new ImageIOException("right-to-left TGAs not supported"); + ubyte attr_bits_pp = (hdr.flags & 0xf); + if (! (attr_bits_pp == 0 || attr_bits_pp == 8)) // some set it 0 although data has 8 + throw new ImageIOException("only 8-bit alpha/attribute(s) supported"); + if (hdr.palette_type) + throw new ImageIOException("paletted TGAs not supported"); + + const bool rle = hdr.data_type == TGA_DataType.TrueColor_RLE // Idx_RLE + || hdr.data_type == TGA_DataType.Gray_RLE; // not supported + + switch (hdr.data_type) with (TGA_DataType) { + case TrueColor: + case TrueColor_RLE: + if (hdr.bits_pp != 24 && hdr.bits_pp != 32) + throw new ImageIOException("not supported"); + break; + case Gray: + case Gray_RLE: + if (hdr.bits_pp != 8 && !(hdr.bits_pp == 16 && attr_bits_pp == 8)) + throw new ImageIOException("not supported"); + break; + default: + throw new ImageIOException("not supported"); + } + + int src_chans = hdr.bits_pp / 8; + + if (hdr.id_length) + stream.seek(hdr.id_length, SEEK_CUR); + + TGA_Decoder dc = { + stream : stream, + w : hdr.width, + h : hdr.height, + origin_at_top : cast(bool) (hdr.flags & 0x20), + bytes_pp : hdr.bits_pp / 8, + rle : rle, + tgt_chans : (req_chans == 0) ? src_chans : cast(int) req_chans, + }; + + switch (dc.bytes_pp) { + case 1: dc.src_fmt = _ColFmt.Y; break; + case 2: dc.src_fmt = _ColFmt.YA; break; + case 3: dc.src_fmt = _ColFmt.BGR; break; + case 4: dc.src_fmt = _ColFmt.BGRA; break; + default: throw new ImageIOException("TGA: format not supported"); + } + + IFImage result = { + w : dc.w, + h : dc.h, + c : cast(ColFmt) dc.tgt_chans, + pixels : decode_tga(dc), + }; + return result; +} + +void write_tga(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { + if (w < 1 || h < 1 || ushort.max < w || ushort.max < h) + throw new ImageIOException("invalid dimensions"); + ulong src_chans = data.length / w / h; + if (src_chans < 1 || 4 < src_chans || tgt_chans < 0 || 4 < tgt_chans) + throw new ImageIOException("invalid channel count"); + if (src_chans * w * h != data.length) + throw new ImageIOException("mismatching dimensions and length"); + + TGA_Encoder ec = { + stream : stream, + w : cast(ushort) w, + h : cast(ushort) h, + src_chans : cast(int) src_chans, + tgt_chans : cast(int) ((tgt_chans) ? tgt_chans : src_chans), + rle : true, + data : data, + }; + + write_tga(ec); + stream.flush(); +} + +struct TGA_Decoder { + Reader stream; + int w, h; + bool origin_at_top; // src + uint bytes_pp; + bool rle; // run length compressed + _ColFmt src_fmt; + uint tgt_chans; +} + +ubyte[] decode_tga(ref TGA_Decoder dc) { + auto result = new ubyte[dc.w * dc.h * dc.tgt_chans]; + + const size_t tgt_linesize = dc.w * dc.tgt_chans; + const size_t src_linesize = dc.w * dc.bytes_pp; + auto src_line = new ubyte[src_linesize]; + + const ptrdiff_t tgt_stride = (dc.origin_at_top) ? tgt_linesize : -tgt_linesize; + ptrdiff_t ti = (dc.origin_at_top) ? 0 : (dc.h-1) * tgt_linesize; + + const LineConv!ubyte convert = get_converter!ubyte(dc.src_fmt, dc.tgt_chans); + + if (!dc.rle) { + foreach (_j; 0 .. dc.h) { + dc.stream.readExact(src_line, src_linesize); + convert(src_line, result[ti .. ti + tgt_linesize]); + ti += tgt_stride; + } + return result; + } + + // ----- RLE ----- + + ubyte[4] rbuf; + size_t plen = 0; // packet length + bool its_rle = false; + + foreach (_j; 0 .. dc.h) { + // fill src_line with uncompressed data (this works like a stream) + size_t wanted = src_linesize; + while (wanted) { + if (plen == 0) { + dc.stream.readExact(rbuf, 1); + its_rle = cast(bool) (rbuf[0] & 0x80); + plen = ((rbuf[0] & 0x7f) + 1) * dc.bytes_pp; // length in bytes + } + const size_t gotten = src_linesize - wanted; + const size_t copysize = min(plen, wanted); + if (its_rle) { + dc.stream.readExact(rbuf, dc.bytes_pp); + for (size_t p = gotten; p < gotten+copysize; p += dc.bytes_pp) + src_line[p .. p+dc.bytes_pp] = rbuf[0 .. dc.bytes_pp]; + } else { // it's raw + auto slice = src_line[gotten .. gotten+copysize]; + dc.stream.readExact(slice, copysize); + } + wanted -= copysize; + plen -= copysize; + } + + convert(src_line, result[ti .. ti + tgt_linesize]); + ti += tgt_stride; + } + + return result; +} + +// ---------------------------------------------------------------------- +// TGA encoder + +immutable ubyte[18] tga_footer_sig = + cast(immutable(ubyte)[18]) "TRUEVISION-XFILE.\0"; + +struct TGA_Encoder { + Writer stream; + ushort w, h; + int src_chans; + int tgt_chans; + bool rle; // run length compression + const(ubyte)[] data; +} + +void write_tga(ref TGA_Encoder ec) { + ubyte data_type; + bool has_alpha = false; + switch (ec.tgt_chans) with (TGA_DataType) { + case 1: data_type = ec.rle ? Gray_RLE : Gray; break; + case 2: data_type = ec.rle ? Gray_RLE : Gray; has_alpha = true; break; + case 3: data_type = ec.rle ? TrueColor_RLE : TrueColor; break; + case 4: data_type = ec.rle ? TrueColor_RLE : TrueColor; has_alpha = true; break; + default: throw new ImageIOException("internal error"); + } + + ubyte[18] hdr = void; + hdr[0] = 0; // id length + hdr[1] = 0; // palette type + hdr[2] = data_type; + hdr[3..8] = 0; // palette start (2), len (2), bits per palette entry (1) + hdr[8..12] = 0; // x origin (2), y origin (2) + hdr[12..14] = nativeToLittleEndian(ec.w); + hdr[14..16] = nativeToLittleEndian(ec.h); + hdr[16] = cast(ubyte) (ec.tgt_chans * 8); // bits per pixel + hdr[17] = (has_alpha) ? 0x8 : 0x0; // flags: attr_bits_pp = 8 + ec.stream.rawWrite(hdr); + + write_image_data(ec); + + ubyte[26] ftr = void; + ftr[0..4] = 0; // extension area offset + ftr[4..8] = 0; // developer directory offset + ftr[8..26] = tga_footer_sig; + ec.stream.rawWrite(ftr); +} + +void write_image_data(ref TGA_Encoder ec) { + _ColFmt tgt_fmt; + switch (ec.tgt_chans) { + case 1: tgt_fmt = _ColFmt.Y; break; + case 2: tgt_fmt = _ColFmt.YA; break; + case 3: tgt_fmt = _ColFmt.BGR; break; + case 4: tgt_fmt = _ColFmt.BGRA; break; + default: throw new ImageIOException("internal error"); + } + + const LineConv!ubyte convert = get_converter!ubyte(ec.src_chans, tgt_fmt); + + const size_t src_linesize = ec.w * ec.src_chans; + const size_t tgt_linesize = ec.w * ec.tgt_chans; + auto tgt_line = new ubyte[tgt_linesize]; + + ptrdiff_t si = (ec.h-1) * src_linesize; // origin at bottom + + if (!ec.rle) { + foreach (_; 0 .. ec.h) { + convert(ec.data[si .. si + src_linesize], tgt_line); + ec.stream.rawWrite(tgt_line); + si -= src_linesize; // origin at bottom + } + return; + } + + // ----- RLE ----- + + const bytes_pp = ec.tgt_chans; + const size_t max_packets_per_line = (tgt_linesize+127) / 128; + auto tgt_cmp = new ubyte[tgt_linesize + max_packets_per_line]; // compressed line + foreach (_; 0 .. ec.h) { + convert(ec.data[si .. si + src_linesize], tgt_line); + ubyte[] compressed_line = rle_compress(tgt_line, tgt_cmp, ec.w, bytes_pp); + ec.stream.rawWrite(compressed_line); + si -= src_linesize; // origin at bottom + } +} + +ubyte[] rle_compress(in ubyte[] line, ubyte[] tgt_cmp, in size_t w, in int bytes_pp) +{ + const int rle_limit = (1 < bytes_pp) ? 2 : 3; // run len worth an RLE packet + size_t runlen = 0; + size_t rawlen = 0; + size_t raw_i = 0; // start of raw packet data in line + size_t cmp_i = 0; + size_t pixels_left = w; + const(ubyte)[] px; + for (size_t i = bytes_pp; pixels_left; i += bytes_pp) { + runlen = 1; + px = line[i-bytes_pp .. i]; + while (i < line.length && line[i .. i+bytes_pp] == px[0..$] && runlen < 128) { + ++runlen; + i += bytes_pp; + } + pixels_left -= runlen; + + if (runlen < rle_limit) { + // data goes to raw packet + rawlen += runlen; + if (128 <= rawlen) { // full packet, need to store it + size_t copysize = 128 * bytes_pp; + tgt_cmp[cmp_i++] = 0x7f; // raw packet header + tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; + cmp_i += copysize; + raw_i += copysize; + rawlen -= 128; + } + } else { + // RLE packet is worth it + + // store raw packet first, if any + if (rawlen) { + assert(rawlen < 128); + size_t copysize = rawlen * bytes_pp; + tgt_cmp[cmp_i++] = cast(ubyte) (rawlen-1); // raw packet header + tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; + cmp_i += copysize; + rawlen = 0; + } + + // store RLE packet + tgt_cmp[cmp_i++] = cast(ubyte) (0x80 | (runlen-1)); // packet header + tgt_cmp[cmp_i .. cmp_i+bytes_pp] = px[0..$]; // packet data + cmp_i += bytes_pp; + raw_i = i; + } + } // for + + if (rawlen) { // last packet of the line + size_t copysize = rawlen * bytes_pp; + tgt_cmp[cmp_i++] = cast(ubyte) (rawlen-1); // raw packet header + tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; + cmp_i += copysize; + } + return tgt_cmp[0 .. cmp_i]; +} + +enum TGA_DataType : ubyte { + Idx = 1, + TrueColor = 2, + Gray = 3, + Idx_RLE = 9, + TrueColor_RLE = 10, + Gray_RLE = 11, +} + +package void read_tga_info(Reader stream, out int w, out int h, out int chans) { + TGA_Header hdr = read_tga_header(stream); + w = hdr.width; + h = hdr.height; + + // TGA is awkward... + auto dt = hdr.data_type; + if ((dt == TGA_DataType.TrueColor || dt == TGA_DataType.Gray || + dt == TGA_DataType.TrueColor_RLE || dt == TGA_DataType.Gray_RLE) + && (hdr.bits_pp % 8) == 0) + { + chans = hdr.bits_pp / 8; + return; + } else if (dt == TGA_DataType.Idx || dt == TGA_DataType.Idx_RLE) { + switch (hdr.palette_bits) { + case 15: chans = 3; return; + case 16: chans = 3; return; // one bit could be for some "interrupt control" + case 24: chans = 3; return; + case 32: chans = 4; return; + default: + } + } + chans = 0; // unknown +} diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basi0g08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi0g08.bmp Binary files differnew file mode 100644 index 0000000..eaaff1b --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi0g08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basi2c08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi2c08.bmp Binary files differnew file mode 100644 index 0000000..ff66a65 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi2c08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basi3p08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi3p08.bmp Binary files differnew file mode 100644 index 0000000..ffe79fe --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi3p08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basi4a08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi4a08.bmp Binary files differnew file mode 100644 index 0000000..e24c9ea --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi4a08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basi6a08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi6a08.bmp Binary files differnew file mode 100644 index 0000000..4fc819f --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basi6a08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basn0g08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn0g08.bmp Binary files differnew file mode 100644 index 0000000..eaaff1b --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn0g08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basn2c08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn2c08.bmp Binary files differnew file mode 100644 index 0000000..ff66a65 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn2c08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basn3p08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn3p08.bmp Binary files differnew file mode 100644 index 0000000..ffe79fe --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn3p08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basn4a08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn4a08.bmp Binary files differnew file mode 100644 index 0000000..e24c9ea --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn4a08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-bmp/basn6a08.bmp b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn6a08.bmp Binary files differnew file mode 100644 index 0000000..4fc819f --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-bmp/basn6a08.bmp diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basi0g08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basi0g08.tga Binary files differnew file mode 100644 index 0000000..188dab5 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basi0g08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basi2c08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basi2c08.tga Binary files differnew file mode 100644 index 0000000..388c86a --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basi2c08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basi3p08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basi3p08.tga Binary files differnew file mode 100644 index 0000000..d79e009 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basi3p08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basi4a08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basi4a08.tga Binary files differnew file mode 100644 index 0000000..064241a --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basi4a08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basi6a08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basi6a08.tga Binary files differnew file mode 100644 index 0000000..594d70f --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basi6a08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basn0g08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basn0g08.tga Binary files differnew file mode 100644 index 0000000..188dab5 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basn0g08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basn2c08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basn2c08.tga Binary files differnew file mode 100644 index 0000000..388c86a --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basn2c08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basn3p08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basn3p08.tga Binary files differnew file mode 100644 index 0000000..d79e009 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basn3p08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basn4a08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basn4a08.tga Binary files differnew file mode 100644 index 0000000..064241a --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basn4a08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite-tga/basn6a08.tga b/src/ext_depends/imageformats/tests/pngsuite-tga/basn6a08.tga Binary files differnew file mode 100644 index 0000000..594d70f --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite-tga/basn6a08.tga diff --git a/src/ext_depends/imageformats/tests/pngsuite/PngSuite.LICENSE b/src/ext_depends/imageformats/tests/pngsuite/PngSuite.LICENSE new file mode 100644 index 0000000..8d4d1d0 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/PngSuite.LICENSE @@ -0,0 +1,9 @@ +PngSuite +-------- + +Permission to use, copy, modify and distribute these images for any +purpose and without fee is hereby granted. + + +(c) Willem van Schaik, 1996, 2011 + diff --git a/src/ext_depends/imageformats/tests/pngsuite/PngSuite.README b/src/ext_depends/imageformats/tests/pngsuite/PngSuite.README new file mode 100644 index 0000000..3ef8f24 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/PngSuite.README @@ -0,0 +1,25 @@ + PNGSUITE
+----------------
+
+ testset for PNG-(de)coders
+ created by Willem van Schaik
+------------------------------------
+
+This is a collection of graphics images created to test the png applications
+like viewers, converters and editors. All (as far as that is possible)
+formats supported by the PNG standard are represented.
+
+The suite consists of the following files:
+
+- PngSuite.README - this file
+- PngSuite.LICENSE - the PngSuite is freeware
+- PngSuite.png - image with PngSuite logo
+- PngSuite.tgz - archive of all PNG testfiles
+- PngSuite.zip - same in .zip format for PCs
+
+
+--------
+ (c) Willem van Schaik
+ willem@schaik.com
+ Calgary, April 2011
+
diff --git a/src/ext_depends/imageformats/tests/pngsuite/basi0g08.png b/src/ext_depends/imageformats/tests/pngsuite/basi0g08.png Binary files differnew file mode 100644 index 0000000..faed8be --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basi0g08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basi2c08.png b/src/ext_depends/imageformats/tests/pngsuite/basi2c08.png Binary files differnew file mode 100644 index 0000000..2aab44d --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basi2c08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basi3p08.png b/src/ext_depends/imageformats/tests/pngsuite/basi3p08.png Binary files differnew file mode 100644 index 0000000..50a6d1c --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basi3p08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basi4a08.png b/src/ext_depends/imageformats/tests/pngsuite/basi4a08.png Binary files differnew file mode 100644 index 0000000..398132b --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basi4a08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basi6a08.png b/src/ext_depends/imageformats/tests/pngsuite/basi6a08.png Binary files differnew file mode 100644 index 0000000..aecb32e --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basi6a08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basn0g08.png b/src/ext_depends/imageformats/tests/pngsuite/basn0g08.png Binary files differnew file mode 100644 index 0000000..23c8237 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basn0g08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basn2c08.png b/src/ext_depends/imageformats/tests/pngsuite/basn2c08.png Binary files differnew file mode 100644 index 0000000..db5ad15 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basn2c08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basn3p08.png b/src/ext_depends/imageformats/tests/pngsuite/basn3p08.png Binary files differnew file mode 100644 index 0000000..0ddad07 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basn3p08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basn4a08.png b/src/ext_depends/imageformats/tests/pngsuite/basn4a08.png Binary files differnew file mode 100644 index 0000000..3e13052 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basn4a08.png diff --git a/src/ext_depends/imageformats/tests/pngsuite/basn6a08.png b/src/ext_depends/imageformats/tests/pngsuite/basn6a08.png Binary files differnew file mode 100644 index 0000000..e608738 --- /dev/null +++ b/src/ext_depends/imageformats/tests/pngsuite/basn6a08.png |