1 /*
2 Copyright (c) 2011-2015 Timur Gafarov, Martin Cejp
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 
6 Permission is hereby granted, free of charge, to any person or organization
7 obtaining a copy of the software and accompanying documentation covered by
8 this license (the "Software") to use, reproduce, display, distribute,
9 execute, and transmit the Software, and to prepare derivative works of the
10 Software, and to permit third-parties to whom the Software is furnished to
11 do so, all subject to the following:
12 
13 The copyright notices in the Software and this entire statement, including
14 the above license grant, this restriction and the following disclaimer,
15 must be included in all copies of the Software, in whole or in part, and
16 all derivative works of the Software, unless such copies or derivative
17 works are solely in the form of machine-executable object code generated by
18 a source language processor.
19 
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 DEALINGS IN THE SOFTWARE.
27 */
28 
29 // dimage is actually stripped out part of dlib - just to support reading PNG and JPEG
30 module dimage.png; //dlib.image.io.png
31 
32 private
33 {
34     import std.stdio;
35     import std.math;
36     import std.string;
37     import std.range;
38 
39     import dimage.memory;
40     import dimage.stream;
41     import dimage.compound;
42     //import dlib.filesystem.local;
43     //import dlib.math.utils;
44     import dimage.zlib;
45     import dimage.image;
46     //import dlib.image.io.io;
47 
48     //import dlib.core.memory;
49     //import dlib.core.stream;
50     //import dlib.core.compound;
51     //import dlib.filesystem.local;
52     //import dlib.math.utils;
53     //import dlib.coding.zlib;
54     //import dlib.image.image;
55     //import dlib.image.io.io;
56 }
57 
58 // uncomment this to see debug messages:
59 //version = PNGDebug;
60 
61 static const ubyte[8] PNGSignature = [137, 80, 78, 71, 13, 10, 26, 10];
62 static const ubyte[4] IHDR = ['I', 'H', 'D', 'R'];
63 static const ubyte[4] IEND = ['I', 'E', 'N', 'D'];
64 static const ubyte[4] IDAT = ['I', 'D', 'A', 'T'];
65 static const ubyte[4] PLTE = ['P', 'L', 'T', 'E'];
66 static const ubyte[4] tRNS = ['t', 'R', 'N', 'S'];
67 static const ubyte[4] bKGD = ['b', 'K', 'G', 'D'];
68 static const ubyte[4] tEXt = ['t', 'E', 'X', 't'];
69 static const ubyte[4] iTXt = ['i', 'T', 'X', 't'];
70 static const ubyte[4] zTXt = ['z', 'T', 'X', 't'];
71 
72 enum ColorType: ubyte
73 {
74     Greyscale = 0,      // allowed bit depths: 1, 2, 4, 8 and 16
75     RGB = 2,            // allowed bit depths: 8 and 16
76     Palette = 3,        // allowed bit depths: 1, 2, 4 and 8
77     GreyscaleAlpha = 4, // allowed bit depths: 8 and 16
78     RGBA = 6,           // allowed bit depths: 8 and 16
79     Any = 7             // one of the above
80 }
81 
82 enum FilterMethod: ubyte
83 {
84     None = 0,
85     Sub = 1,
86     Up = 2,
87     Average = 3,
88     Paeth = 4
89 }
90 
91 struct PNGChunk
92 {
93     uint length;
94     ubyte[4] type;
95     ubyte[] data;
96     uint crc;
97 
98     void free()
99     {
100         if (data.ptr)
101             Delete(data);
102     }
103 }
104 
105 struct PNGHeader
106 {
107     union
108     {
109         struct
110         {
111             uint width;
112             uint height;
113             ubyte bitDepth;
114             ubyte colorType;
115             ubyte compressionMethod;
116             ubyte filterMethod;
117             ubyte interlaceMethod;
118         };
119         ubyte[13] bytes;
120     }
121 }
122 class PNGLoadException: ImageLoadException
123 {
124     this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
125     {
126         super(msg, file, line, next);
127     }
128 }
129 
130 /*
131  * Load PNG from file using local FileSystem.
132  * Causes GC allocation
133  */
134 //SuperImage loadPNG(string filename)
135 //{
136 //    InputStream input = openForInput(filename);
137 //    auto img = loadPNG(input);
138 //    input.close();
139 //    return img;
140 //}
141 
142 /*
143  * Save PNG to file using local FileSystem.
144  * Causes GC allocation
145  */
146 //void savePNG(SuperImage img, string filename)
147 //{
148 //    OutputStream output = openForOutput(filename);
149 //    Compound!(bool, string) res =
150 //        savePNG(img, output);
151 //    output.close();
152 //
153 //    if (!res[0])
154 //        throw new PNGLoadException(res[1]);
155 //}
156 
157 /*
158  * Load PNG from stream using default image factory.
159  * Causes GC allocation
160  */
161 SuperImage loadPNG(InputStream istrm)
162 {
163     Compound!(SuperImage, string) res =
164         loadPNG(istrm, defaultImageFactory);
165     if (res[0] is null)
166         throw new PNGLoadException(res[1]);
167     else
168         return res[0];
169 }
170 
171 /*
172  * Load PNG from stream using specified image factory.
173  * GC-free
174  */
175 Compound!(SuperImage, string) loadPNG(
176     InputStream istrm,
177     SuperImageFactory imgFac)
178 {
179     SuperImage img = null;
180 
181     Compound!(SuperImage, string) error(string errorMsg)
182     {
183         if (img)
184         {
185             img.free();
186             img = null;
187         }
188         return compound(img, errorMsg);
189     }
190 
191     bool readChunk(PNGChunk* chunk)
192     {
193         if (!istrm.readBE!uint(&chunk.length)
194             || !istrm.fillArray(chunk.type))
195         {
196             return false;
197         }
198 
199         version(PNGDebug) writefln("Chunk length = %s", chunk.length);
200         version(PNGDebug) writefln("Chunk type = %s", cast(char[])chunk.type);
201 
202         if (chunk.length > 0)
203         {
204             chunk.data = New!(ubyte[])(chunk.length);
205 
206             if (!istrm.fillArray(chunk.data))
207             {
208                 return false;
209             }
210         }
211 
212         version(PNGDebug) writefln("Chunk data.length = %s", chunk.data.length);
213 
214         if (!istrm.readBE!uint(&chunk.crc))
215         {
216             return false;
217         }
218 
219         // TODO: reimplement CRC check with ranges instead of concatenation
220         uint calculatedCRC = crc32(chain(chunk.type[0..$], chunk.data));
221 
222         version(PNGDebug)
223         {
224             writefln("Chunk CRC = %X", chunk.crc);
225             writefln("Calculated CRC = %X", calculatedCRC);
226             writeln("-------------------");
227         }
228 
229         if (chunk.crc != calculatedCRC)
230         {
231             return false;
232         }
233 
234         return true;
235     }
236 
237     bool readHeader(PNGHeader* hdr, PNGChunk* chunk)
238     {
239         hdr.bytes[] = chunk.data[];
240         hdr.width = bigEndian(hdr.width);
241         hdr.height = bigEndian(hdr.height);
242 
243         version(PNGDebug)
244         {
245             writefln("width = %s", hdr.width);
246             writefln("height = %s", hdr.height);
247             writefln("bitDepth = %s", hdr.bitDepth);
248             writefln("colorType = %s", hdr.colorType);
249             writefln("compressionMethod = %s", hdr.compressionMethod);
250             writefln("filterMethod = %s", hdr.filterMethod);
251             writefln("interlaceMethod = %s", hdr.interlaceMethod);
252             writeln("----------------");
253         }
254 
255         return true;
256     }
257 
258     ubyte[8] signatureBuffer;
259 
260     if (!istrm.fillArray(signatureBuffer))
261     {
262         return error("loadPNG error: signature check failed");
263     }
264 
265     version(PNGDebug)
266     {
267         writeln("----------------");
268         writeln("PNG Signature: ", signatureBuffer);
269         writeln("----------------");
270     }
271 
272     PNGHeader hdr;
273 
274     ZlibDecoder zlibDecoder;
275 
276     ubyte[] palette;
277     ubyte[] transparency;
278     uint paletteSize = 0;
279 
280     bool endChunk = false;
281     while (!endChunk && istrm.readable)
282     {
283         PNGChunk chunk;
284         bool res = readChunk(&chunk);
285         if (!res)
286         {
287             chunk.free();
288             return error("loadPNG error: failed to read chunk");
289         }
290         else
291         {
292             if (chunk.type == IEND)
293             {
294                 endChunk = true;
295                 chunk.free();
296             }
297             else if (chunk.type == IHDR)
298             {
299                 if (chunk.data.length < hdr.bytes.length)
300                     return error("loadPNG error: illegal header chunk");
301 
302                 readHeader(&hdr, &chunk);
303                 chunk.free();
304 
305                 bool supportedIndexed =
306                     (hdr.colorType == ColorType.Palette) &&
307                     (hdr.bitDepth == 1 ||
308                      hdr.bitDepth == 2 ||
309                      hdr.bitDepth == 4 ||
310                      hdr.bitDepth == 8);
311 
312                 if (hdr.bitDepth != 8 && hdr.bitDepth != 16 && !supportedIndexed)
313                     return error("loadPNG error: unsupported bit depth");
314 
315                 if (hdr.compressionMethod != 0)
316                     return error("loadPNG error: unsupported compression method");
317 
318                 if (hdr.filterMethod != 0)
319                     return error("loadPNG error: unsupported filter method");
320 
321                 if (hdr.interlaceMethod != 0)
322                     return error("loadPNG error: interlacing is not supported");
323 
324                 uint bufferLength = ((hdr.width * hdr.bitDepth + 7) / 8) * hdr.height + hdr.height;
325                 ubyte[] buffer = New!(ubyte[])(bufferLength);
326 
327                 zlibDecoder = ZlibDecoder(buffer);
328 
329                 version(PNGDebug)
330                 {
331                     writefln("buffer.length = %s", bufferLength);
332                     writeln("----------------");
333                 }
334             }
335             else if (chunk.type == IDAT)
336             {
337                 zlibDecoder.decode(chunk.data);
338                 chunk.free();
339             }
340             else if (chunk.type == PLTE)
341             {
342                 palette = chunk.data;
343             }
344             else if (chunk.type == tRNS)
345             {
346                 transparency = chunk.data;
347                 version(PNGDebug)
348                 {
349                     writeln("----------------");
350                     writefln("transparency.length = %s", transparency.length);
351                     writeln("----------------");
352                 }
353             }
354             else
355             {
356                 chunk.free();
357             }
358         }
359     }
360 
361     // finalize decoder
362     version(PNGDebug) writefln("zlibDecoder.hasEnded = %s", zlibDecoder.hasEnded);
363     if (!zlibDecoder.hasEnded)
364         return error("loadPNG error: unexpected end of zlib stream");
365 
366     ubyte[] buffer = zlibDecoder.buffer;
367     version(PNGDebug) writefln("buffer.length = %s", buffer.length);
368 
369     bool transparencyPalette;
370 
371     // create image
372     if (hdr.colorType == ColorType.Greyscale)
373     {
374         if (hdr.bitDepth == 8)
375             img = imgFac.createImage(hdr.width, hdr.height, 1, 8);
376         else if (hdr.bitDepth == 16)
377             img = imgFac.createImage(hdr.width, hdr.height, 1, 16);
378     }
379     else if (hdr.colorType == ColorType.GreyscaleAlpha)
380     {
381         if (hdr.bitDepth == 8)
382             img = imgFac.createImage(hdr.width, hdr.height, 2, 8);
383         else if (hdr.bitDepth == 16)
384             img = imgFac.createImage(hdr.width, hdr.height, 2, 16);
385     }
386     else if (hdr.colorType == ColorType.RGB)
387     {
388         if (hdr.bitDepth == 8)
389             img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
390         else if (hdr.bitDepth == 16)
391             img = imgFac.createImage(hdr.width, hdr.height, 3, 16);
392     }
393     else if (hdr.colorType == ColorType.RGBA)
394     {
395         if (hdr.bitDepth == 8)
396             img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
397         else if (hdr.bitDepth == 16)
398             img = imgFac.createImage(hdr.width, hdr.height, 4, 16);
399     }
400     else if (hdr.colorType == ColorType.Palette)
401     {
402         if (transparency.length > 0) {
403             img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
404             transparencyPalette = true;
405         } else
406             img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
407     }
408     else
409         return error("loadPNG error: unsupported color type");
410 
411     version(PNGDebug)
412     {
413         writefln("img.width = %s", img.width);
414         writefln("img.height = %s", img.height);
415         writefln("img.bitDepth = %s", img.bitDepth);
416         writefln("img.channels = %s", img.channels);
417         writeln("----------------");
418     }
419 
420     bool indexed = (hdr.colorType == ColorType.Palette);
421 
422     // don't close the stream, just release our reference
423     istrm = null;
424 
425     // apply filtering to the image data
426     ubyte[] buffer2;
427     string errorMsg;
428     if (!filter(&hdr, img.channels, indexed, buffer, buffer2, errorMsg))
429     {
430         return error(errorMsg);
431     }
432     Delete(buffer);
433     buffer = buffer2;
434 
435     // if a palette is used, substitute target colors
436     if (indexed)
437     {
438         if (palette.length == 0)
439             return error("loadPNG error: palette chunk not found");
440 
441         ubyte[] pdata = New!(ubyte[])(img.width * img.height * img.channels);
442         if (hdr.bitDepth == 8)
443         {
444             for (int i = 0; i < buffer.length; ++i)
445             {
446                 ubyte b = buffer[i];
447                 pdata[i * img.channels + 0] = palette[b * 3 + 0];
448                 pdata[i * img.channels + 1] = palette[b * 3 + 1];
449                 pdata[i * img.channels + 2] = palette[b * 3 + 2];
450                 if (transparency.length > 0)
451                     pdata[i * img.channels + 3] =
452                         b < transparency.length ? transparency[b] : 0;
453             }
454         }
455         else // bit depths 1, 2, 4
456         {
457             int srcindex = 0;
458             int srcshift = 8 - hdr.bitDepth;
459             ubyte mask = cast(ubyte)((1 << hdr.bitDepth) - 1);
460             int sz = img.width * img.height;
461             for (int dstindex = 0; dstindex < sz; dstindex++)
462             {
463                 auto b = ((buffer[srcindex] >> srcshift) & mask);
464                 //assert(b * 3 + 2 < palette.length);
465                 pdata[dstindex * img.channels + 0] = palette[b * 3 + 0];
466                 pdata[dstindex * img.channels + 1] = palette[b * 3 + 1];
467                 pdata[dstindex * img.channels + 2] = palette[b * 3 + 2];
468 
469                 if (transparency.length > 0)
470                     pdata[dstindex * img.channels + 3] =
471                         b < transparency.length ? transparency[b] : 0;
472 
473                 if (srcshift <= 0)
474                 {
475                     srcshift = 8 - hdr.bitDepth;
476                     srcindex++;
477                 }
478                 else
479                 {
480                     srcshift -= hdr.bitDepth;
481                 }
482             }
483         }
484 
485         Delete(buffer);
486         buffer = pdata;
487 
488         Delete(palette);
489 
490         if (transparency.length > 0)
491             Delete(transparency);
492     }
493 
494     //if (img.data.length != buffer.length)
495     //    return error("loadPNG error: uncompressed data length mismatch");
496     //
497     //foreach(i, v; buffer)
498     //    img.data[i] = v;
499 
500     int bufindex = 0;
501     if (hdr.colorType == ColorType.Greyscale)
502     {
503         if (hdr.bitDepth == 8) {
504             //img = imgFac.createImage(hdr.width, hdr.height, 1, 8);
505             for (int i = 0; i < img.length; i++) {
506                 img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex])<<8) | ((cast(uint)buffer[bufindex])<<0) | 0xFF000000;
507                 bufindex += 1;
508             }
509         } else if (hdr.bitDepth == 16) {
510             //img = imgFac.createImage(hdr.width, hdr.height, 1, 16);
511             assert(false);
512         }
513     }
514     else if (hdr.colorType == ColorType.GreyscaleAlpha)
515     {
516         if (hdr.bitDepth == 8) {
517             //img = imgFac.createImage(hdr.width, hdr.height, 2, 8);
518             for (int i = 0; i < img.length; i++) {
519                 img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex])<<8) | ((cast(uint)buffer[bufindex])<<0) | ((cast(uint)buffer[bufindex + 1])<<24);
520                 bufindex += 2;
521             }
522         } else if (hdr.bitDepth == 16) {
523             //img = imgFac.createImage(hdr.width, hdr.height, 2, 16);
524             assert(false);
525         }
526     }
527     else if (hdr.colorType == ColorType.RGB)
528     {
529         if (hdr.bitDepth == 8) {
530             //img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
531             for (int i = 0; i < img.length; i++) {
532                 img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | 0xFF000000;
533                 bufindex += 3;
534             }
535         } else if (hdr.bitDepth == 16) {
536             //img = imgFac.createImage(hdr.width, hdr.height, 3, 16);
537             assert(false);
538         }
539     }
540     else if (hdr.colorType == ColorType.RGBA)
541     {
542         if (hdr.bitDepth == 8) {
543             //img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
544             for (int i = 0; i < img.length; i++) {
545                 img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | ((cast(uint)buffer[bufindex + 3])<<24);
546                 bufindex += 4;
547             }
548         } else if (hdr.bitDepth == 16) {
549             //img = imgFac.createImage(hdr.width, hdr.height, 4, 16);
550             assert(false);
551         }
552     }
553     else if (hdr.colorType == ColorType.Palette)
554     {
555         if (transparencyPalette) {
556             for (int i = 0; i < img.length; i++) {
557                 img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | ((cast(uint)buffer[bufindex + 3])<<24);
558                 bufindex += 4;
559             }
560             //img = imgFac.createImage(hdr.width, hdr.height, 4, 8);
561         } else {
562             //img = imgFac.createImage(hdr.width, hdr.height, 3, 8);
563             for (int i = 0; i < img.length; i++) {
564                 img.data[i] = ((cast(uint)buffer[bufindex])<<16) | ((cast(uint)buffer[bufindex + 1])<<8) | ((cast(uint)buffer[bufindex + 2])<<0) | 0xFF000000;
565                 bufindex += 3;
566             }
567         }
568     }
569 
570     Delete(buffer);
571 
572     return compound(img, "");
573 }
574 
575 version (ENABLE_SAVE_PNG) {
576 /*
577  * Save PNG to stream.
578  * GC-free
579  */
580 Compound!(bool, string) savePNG(SuperImage img, OutputStream output)
581 in
582 {
583     assert (img.data.length);
584 }
585 do
586 {
587     Compound!(bool, string) error(string errorMsg)
588     {
589         return compound(false, errorMsg);
590     }
591 
592     if (img.bitDepth != 8)
593         return error("savePNG error: only 8-bit images are supported by encoder");
594 
595     bool writeChunk(ubyte[4] chunkType, ubyte[] chunkData)
596     {
597         PNGChunk hdrChunk;
598         hdrChunk.length = cast(uint)chunkData.length;
599         hdrChunk.type = chunkType;
600         hdrChunk.data = chunkData;
601         hdrChunk.crc = crc32(chain(chunkType[0..$], hdrChunk.data));
602 
603         if (!output.writeBE!uint(hdrChunk.length)
604             || !output.writeArray(hdrChunk.type))
605             return false;
606 
607         if (chunkData.length)
608             if (!output.writeArray(hdrChunk.data))
609                 return false;
610 
611         if (!output.writeBE!uint(hdrChunk.crc))
612             return false;
613 
614         return true;
615     }
616 
617     bool writeHeader()
618     {
619         PNGHeader hdr;
620         hdr.width = networkByteOrder(img.width);
621         hdr.height = networkByteOrder(img.height);
622         hdr.bitDepth = 8;
623         if (img.channels == 4)
624             hdr.colorType = ColorType.RGBA;
625         else if (img.channels == 3)
626             hdr.colorType = ColorType.RGB;
627         else if (img.channels == 2)
628             hdr.colorType = ColorType.GreyscaleAlpha;
629         else if (img.channels == 1)
630             hdr.colorType = ColorType.Greyscale;
631         hdr.compressionMethod = 0;
632         hdr.filterMethod = 0;
633         hdr.interlaceMethod = 0;
634 
635         return writeChunk(IHDR, hdr.bytes);
636     }
637 
638     output.writeArray(PNGSignature);
639     if (!writeHeader())
640         return error("savePNG error: write failed (disk full?)");
641 
642     //TODO: filtering
643     ubyte[] raw = New!(ubyte[])(img.width * img.height * img.channels + img.height);
644     foreach(y; 0..img.height)
645     {
646         auto rowStart = y * (img.width * img.channels + 1);
647         raw[rowStart] = 0; // No filter
648 
649         foreach(x; 0..img.width)
650         {
651             auto dataIndex = (y * img.width + x) * img.channels;
652             auto rawIndex = rowStart + 1 + x * img.channels;
653 
654             foreach(ch; 0..img.channels)
655                 raw[rawIndex + ch] = img.data[dataIndex + ch];
656         }
657     }
658 
659     ubyte[] buffer = New!(ubyte[])(64 * 1024);
660     ZlibBufferedEncoder zlibEncoder = ZlibBufferedEncoder(buffer, raw);
661     while (!zlibEncoder.ended)
662     {
663         auto len = zlibEncoder.encode();
664         if (len > 0)
665             writeChunk(IDAT, zlibEncoder.buffer[0..len]);
666     }
667 
668     writeChunk(IEND, []);
669 
670     Delete(buffer);
671     Delete(raw);
672 
673     return compound(true, "");
674 }
675 }
676 
677 /*
678  * performs the paeth PNG filter from pixels values:
679  *   a = back
680  *   b = up
681  *   c = up and back
682  */
683 pure ubyte paeth(ubyte a, ubyte b, ubyte c)
684 {
685     int p = a + b - c;
686     int pa = abs(p - a);
687     int pb = abs(p - b);
688     int pc = abs(p - c);
689     if (pa <= pb && pa <= pc) return a;
690     else if (pb <= pc) return b;
691     else return c;
692 }
693 
694 bool filter(PNGHeader* hdr,
695             uint channels,
696             bool indexed,
697             ubyte[] ibuffer,
698         out ubyte[] obuffer,
699         out string errorMsg)
700 {
701     uint dataSize = cast(uint)ibuffer.length;
702     uint scanlineSize;
703 
704     uint calculatedSize;
705     if (indexed)
706     {
707         calculatedSize = hdr.width * hdr.height * hdr.bitDepth / 8 + hdr.height;
708         scanlineSize = hdr.width * hdr.bitDepth / 8 + 1;
709     }
710     else
711     {
712         calculatedSize = hdr.width * hdr.height * channels + hdr.height;
713         scanlineSize = hdr.width * channels + 1;
714     }
715 
716     version(PNGDebug)
717     {
718         writefln("[filter] dataSize = %s", dataSize);
719         writefln("[filter] calculatedSize = %s", calculatedSize);
720     }
721 
722     if (dataSize != calculatedSize)
723     {
724         errorMsg = "loadPNG error: image size and data mismatch";
725         return false;
726     }
727 
728     obuffer = New!(ubyte[])(calculatedSize - hdr.height);
729 
730     ubyte pback, pup, pupback, cbyte;
731 
732     for (int i = 0; i < hdr.height; ++i)
733     {
734         pback = 0;
735 
736         // get the first byte of a scanline
737         ubyte scanFilter = ibuffer[i * scanlineSize];
738 
739         if (indexed)
740         {
741             // TODO: support filtering for indexed images
742             if (scanFilter != FilterMethod.None)
743             {
744                 errorMsg = "loadPNG error: filtering is not supported for indexed images";
745                 return false;
746             }
747 
748             for (int j = 1; j < scanlineSize; ++j)
749             {
750                 ubyte b = ibuffer[(i * scanlineSize) + j];
751                 obuffer[(i * (scanlineSize-1) + j - 1)] = b;
752             }
753             continue;
754         }
755 
756         for (int j = 0; j < hdr.width; ++j)
757         {
758             for (int k = 0; k < channels; ++k)
759             {
760                 if (i == 0)    pup = 0;
761                 else pup = obuffer[((i-1) * hdr.width + j) * channels + k]; // (hdr.height-(i-1)-1)
762                 if (j == 0)    pback = 0;
763                 else pback = obuffer[(i * hdr.width + j-1) * channels + k];
764                 if (i == 0 || j == 0) pupback = 0;
765                 else pupback = obuffer[((i-1) * hdr.width + j - 1) * channels + k];
766 
767                 // get the current byte from ibuffer
768                 cbyte = ibuffer[i * (hdr.width * channels + 1) + j * channels + k + 1];
769 
770                 // filter, then set the current byte in data
771                 switch (scanFilter)
772                 {
773                     case FilterMethod.None:
774                         obuffer[(i * hdr.width + j) * channels + k] = cbyte;
775                         break;
776                     case FilterMethod.Sub:
777                         obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + pback);
778                         break;
779                     case FilterMethod.Up:
780                         obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + pup);
781                         break;
782                     case FilterMethod.Average:
783                         obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + (pback + pup) / 2);
784                         break;
785                     case FilterMethod.Paeth:
786                         obuffer[(i * hdr.width + j) * channels + k] = cast(ubyte)(cbyte + paeth(pback, pup, pupback));
787                         break;
788                     default:
789                         errorMsg = format("loadPNG error: unknown scanline filter (%s)", scanFilter);
790                         return false;
791                 }
792             }
793         }
794     }
795 
796     return true;
797 }
798 
799 uint crc32(R)(R range, uint inCrc = 0) if (isInputRange!R)
800 {
801     uint[256] generateTable()
802     {
803         uint[256] table;
804         uint crc;
805         for (int i = 0; i < 256; i++)
806         {
807             crc = i;
808             for (int j = 0; j < 8; j++)
809                 crc = crc & 1 ? (crc >> 1) ^ 0xEDB88320UL : crc >> 1;
810             table[i] = crc;
811         }
812         return table;
813     }
814 
815     static const uint[256] table = generateTable();
816 
817     uint crc;
818 
819     crc = inCrc ^ 0xFFFFFFFF;
820     foreach(v; range)
821         crc = (crc >> 8) ^ table[(crc ^ v) & 0xFF];
822 
823     return (crc ^ 0xFFFFFFFF);
824 }
825 
826 static if (false) {
827 unittest
828 {
829     import std.base64;
830 
831     InputStream png() {
832         string minimal =
833             "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADklEQVR42mL4z8AAEGAAAwEBAGb9nyQAAAAASUVORK5CYII=";
834 
835         ubyte[] bytes = Base64.decode(minimal);
836         return new ArrayStream(bytes, bytes.length);
837     }
838 
839     SuperImage img = loadPNG(png());
840 
841     assert(img.width == 1);
842     assert(img.height == 1);
843     assert(img.channels == 3);
844     assert(img.pixelSize == 3);
845     assert(img.data == [0xff, 0x00, 0x00]);
846 
847     createDir("tests", false);
848     savePNG(img, "tests/minimal.png");
849     loadPNG("tests/minimal.png");
850 }
851 }