1 module imageformats.png;
2 
3 import etc.c.zlib;
4 import std.algorithm  : min, reverse;
5 import std.bitmanip   : bigEndianToNative, nativeToBigEndian;
6 import std.stdio      : File, SEEK_SET;
7 import std.digest.crc : CRC32, crc32Of;
8 import std.typecons   : scoped;
9 import imageformats;
10 
11 private:
12 
13 /// Header of a PNG file.
14 public struct PNG_Header {
15     int     width;
16     int     height;
17     ubyte   bit_depth;
18     ubyte   color_type;
19     ubyte   compression_method;
20     ubyte   filter_method;
21     ubyte   interlace_method;
22 }
23 
24 /// Returns the header of a PNG file.
25 public PNG_Header read_png_header(in char[] filename) {
26     auto reader = scoped!FileReader(filename);
27     return read_png_header(reader);
28 }
29 
30 /// Returns the header of the image in the buffer.
31 public PNG_Header read_png_header_from_mem(in ubyte[] source) {
32     auto reader = scoped!MemReader(source);
33     return read_png_header(reader);
34 }
35 
36 /// Reads an 8-bit or 16-bit PNG image and returns it as an 8-bit image.
37 /// req_chans defines the format of returned image (you can use ColFmt here).
38 public IFImage read_png(in char[] filename, long req_chans = 0) {
39     auto reader = scoped!FileReader(filename);
40     return read_png(reader, req_chans);
41 }
42 
43 /// Reads an 8-bit or 16-bit PNG image from a buffer and returns it as an
44 /// 8-bit image.  req_chans defines the format of returned image (you can use
45 /// ColFmt here).
46 public IFImage read_png_from_mem(in ubyte[] source, long req_chans = 0) {
47     auto reader = scoped!MemReader(source);
48     return read_png(reader, req_chans);
49 }
50 
51 /// Reads an 8-bit or 16-bit PNG image and returns it as a 16-bit image.
52 /// req_chans defines the format of returned image (you can use ColFmt here).
53 public IFImage16 read_png16(in char[] filename, long req_chans = 0) {
54     auto reader = scoped!FileReader(filename);
55     return read_png16(reader, req_chans);
56 }
57 
58 /// Reads an 8-bit or 16-bit PNG image from a buffer and returns it as a
59 /// 16-bit image.  req_chans defines the format of returned image (you can use
60 /// ColFmt here).
61 public IFImage16 read_png16_from_mem(in ubyte[] source, long req_chans = 0) {
62     auto reader = scoped!MemReader(source);
63     return read_png16(reader, req_chans);
64 }
65 
66 /// Writes a PNG image into a file.
67 public void write_png(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0)
68 {
69     auto writer = scoped!FileWriter(file);
70     write_png(writer, w, h, data, tgt_chans);
71 }
72 
73 /// Writes a PNG image into a buffer.
74 public ubyte[] write_png_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) {
75     auto writer = scoped!MemWriter();
76     write_png(writer, w, h, data, tgt_chans);
77     return writer.result;
78 }
79 
80 /// Returns width, height and color format information via w, h and chans.
81 public void read_png_info(in char[] filename, out int w, out int h, out int chans) {
82     auto reader = scoped!FileReader(filename);
83     return read_png_info(reader, w, h, chans);
84 }
85 
86 /// Returns width, height and color format information via w, h and chans.
87 public void read_png_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) {
88     auto reader = scoped!MemReader(source);
89     return read_png_info(reader, w, h, chans);
90 }
91 
92 // Detects whether a PNG image is readable from stream.
93 package bool detect_png(Reader stream) {
94     try {
95         ubyte[8] tmp = void;
96         stream.readExact(tmp, tmp.length);
97         return (tmp[0..8] == png_file_header[0..$]);
98     } catch (Throwable) {
99         return false;
100     } finally {
101         stream.seek(0, SEEK_SET);
102     }
103 }
104 
105 PNG_Header read_png_header(Reader stream) {
106     ubyte[33] tmp = void;  // file header, IHDR len+type+data+crc
107     stream.readExact(tmp, tmp.length);
108 
109     ubyte[4] crc = crc32Of(tmp[12..29]);
110     reverse(crc[]);
111     if ( tmp[0..8] != png_file_header[0..$]              ||
112          tmp[8..16] != png_image_header                  ||
113          crc != tmp[29..33] )
114         throw new ImageIOException("corrupt header");
115 
116     PNG_Header header = {
117         width              : bigEndianToNative!int(tmp[16..20]),
118         height             : bigEndianToNative!int(tmp[20..24]),
119         bit_depth          : tmp[24],
120         color_type         : tmp[25],
121         compression_method : tmp[26],
122         filter_method      : tmp[27],
123         interlace_method   : tmp[28],
124     };
125     return header;
126 }
127 
128 package IFImage read_png(Reader stream, long req_chans = 0) {
129     PNG_Decoder dc = init_png_decoder(stream, req_chans, 8);
130     IFImage result = {
131         w      : dc.w,
132         h      : dc.h,
133         c      : cast(ColFmt) dc.tgt_chans,
134         pixels : decode_png(dc).bpc8
135     };
136     return result;
137 }
138 
139 IFImage16 read_png16(Reader stream, long req_chans = 0) {
140     PNG_Decoder dc = init_png_decoder(stream, req_chans, 16);
141     IFImage16 result = {
142         w      : dc.w,
143         h      : dc.h,
144         c      : cast(ColFmt) dc.tgt_chans,
145         pixels : decode_png(dc).bpc16
146     };
147     return result;
148 }
149 
150 PNG_Decoder init_png_decoder(Reader stream, long req_chans, int req_bpc) {
151     if (req_chans < 0 || 4 < req_chans)
152         throw new ImageIOException("come on...");
153 
154     PNG_Header hdr = read_png_header(stream);
155 
156     if (hdr.width < 1 || hdr.height < 1 || int.max < cast(ulong) hdr.width * hdr.height)
157         throw new ImageIOException("invalid dimensions");
158     if ((hdr.bit_depth != 8 && hdr.bit_depth != 16) || (req_bpc != 8 && req_bpc != 16))
159         throw new ImageIOException("only 8-bit and 16-bit images supported");
160     if (! (hdr.color_type == PNG_ColorType.Y    ||
161            hdr.color_type == PNG_ColorType.RGB  ||
162            hdr.color_type == PNG_ColorType.Idx  ||
163            hdr.color_type == PNG_ColorType.YA   ||
164            hdr.color_type == PNG_ColorType.RGBA) )
165         throw new ImageIOException("color type not supported");
166     if (hdr.compression_method != 0 || hdr.filter_method != 0 ||
167         (hdr.interlace_method != 0 && hdr.interlace_method != 1))
168         throw new ImageIOException("not supported");
169 
170     PNG_Decoder dc = {
171         stream      : stream,
172         src_indexed : (hdr.color_type == PNG_ColorType.Idx),
173         src_chans   : channels(cast(PNG_ColorType) hdr.color_type),
174         bpc         : hdr.bit_depth,
175         req_bpc     : req_bpc,
176         ilace       : hdr.interlace_method,
177         w           : hdr.width,
178         h           : hdr.height,
179     };
180     dc.tgt_chans = (req_chans == 0) ? dc.src_chans : cast(int) req_chans;
181     return dc;
182 }
183 
184 immutable ubyte[8] png_file_header =
185     [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
186 
187 immutable ubyte[8] png_image_header =
188     [0x0, 0x0, 0x0, 0xd, 'I','H','D','R'];
189 
190 int channels(PNG_ColorType ct) pure nothrow {
191     final switch (ct) with (PNG_ColorType) {
192         case Y: return 1;
193         case RGB: return 3;
194         case YA: return 2;
195         case RGBA, Idx: return 4;
196     }
197 }
198 
199 PNG_ColorType color_type(long channels) pure nothrow {
200     switch (channels) {
201         case 1: return PNG_ColorType.Y;
202         case 2: return PNG_ColorType.YA;
203         case 3: return PNG_ColorType.RGB;
204         case 4: return PNG_ColorType.RGBA;
205         default: assert(0);
206     }
207 }
208 
209 struct PNG_Decoder {
210     Reader stream;
211     bool src_indexed;
212     int src_chans;
213     int tgt_chans;
214     int bpc;
215     int req_bpc;
216     int w, h;
217     ubyte ilace;
218 
219     CRC32 crc;
220     ubyte[12] chunkmeta;  // crc | length and type
221     ubyte[] read_buf;
222     ubyte[] palette;
223     ubyte[] transparency;
224 
225     // decompression
226     z_stream*   z;              // zlib stream
227     uint        avail_idat;     // available bytes in current idat chunk
228     ubyte[]     idat_window;    // slice of read_buf
229 }
230 
231 Buffer decode_png(ref PNG_Decoder dc) {
232     dc.read_buf = new ubyte[4096];
233 
234     enum Stage {
235         IHDR_parsed,
236         PLTE_parsed,
237         IDAT_parsed,
238         IEND_parsed,
239     }
240 
241     Buffer result;
242     auto stage = Stage.IHDR_parsed;
243     dc.stream.readExact(dc.chunkmeta[4..$], 8);  // next chunk's len and type
244 
245     while (stage != Stage.IEND_parsed) {
246         int len = bigEndianToNative!int(dc.chunkmeta[4..8]);
247         if (len < 0)
248             throw new ImageIOException("chunk too long");
249 
250         // standard allows PLTE chunk for non-indexed images too but we don't
251         dc.crc.put(dc.chunkmeta[8..12]);  // type
252         switch (cast(char[]) dc.chunkmeta[8..12]) {    // chunk type
253             case "IDAT":
254                 if (! (stage == Stage.IHDR_parsed ||
255                       (stage == Stage.PLTE_parsed && dc.src_indexed)) )
256                     throw new ImageIOException("corrupt chunk stream");
257                 result = read_IDAT_stream(dc, len);
258                 dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type
259                 ubyte[4] crc = dc.crc.finish;
260                 reverse(crc[]);
261                 if (crc != dc.chunkmeta[0..4])
262                     throw new ImageIOException("corrupt chunk");
263                 stage = Stage.IDAT_parsed;
264                 break;
265             case "PLTE":
266                 if (stage != Stage.IHDR_parsed)
267                     throw new ImageIOException("corrupt chunk stream");
268                 int entries = len / 3;
269                 if (len % 3 != 0 || 256 < entries)
270                     throw new ImageIOException("corrupt chunk");
271                 dc.palette = new ubyte[len];
272                 dc.stream.readExact(dc.palette, dc.palette.length);
273                 dc.crc.put(dc.palette);
274                 dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type
275                 ubyte[4] crc = dc.crc.finish;
276                 reverse(crc[]);
277                 if (crc != dc.chunkmeta[0..4])
278                     throw new ImageIOException("corrupt chunk");
279                 stage = Stage.PLTE_parsed;
280                 break;
281             case "tRNS":
282                 if (! (stage == Stage.IHDR_parsed ||
283                       (stage == Stage.PLTE_parsed && dc.src_indexed)) )
284                     throw new ImageIOException("corrupt chunk stream");
285                 if (dc.src_indexed) {
286                     size_t entries = dc.palette.length / 3;
287                     if (len > entries)
288                         throw new ImageIOException("corrupt chunk");
289                 }
290                 dc.transparency = new ubyte[len];
291                 dc.stream.readExact(dc.transparency, dc.transparency.length);
292                 dc.stream.readExact(dc.chunkmeta, 12);
293                 dc.crc.put(dc.transparency);
294                 ubyte[4] crc = dc.crc.finish;
295                 reverse(crc[]);
296                 if (crc != dc.chunkmeta[0..4])
297                     throw new ImageIOException("corrupt chunk");
298                 break;
299             case "IEND":
300                 if (stage != Stage.IDAT_parsed)
301                     throw new ImageIOException("corrupt chunk stream");
302                 dc.stream.readExact(dc.chunkmeta, 4); // crc
303                 static immutable ubyte[4] expectedCRC = [0xae, 0x42, 0x60, 0x82];
304                 if (len != 0 || dc.chunkmeta[0..4] != expectedCRC)
305                     throw new ImageIOException("corrupt chunk");
306                 stage = Stage.IEND_parsed;
307                 break;
308             case "IHDR":
309                 throw new ImageIOException("corrupt chunk stream");
310             default:
311                 // unknown chunk, ignore but check crc
312                 while (0 < len) {
313                     size_t bytes = min(len, dc.read_buf.length);
314                     dc.stream.readExact(dc.read_buf, bytes);
315                     len -= bytes;
316                     dc.crc.put(dc.read_buf[0..bytes]);
317                 }
318                 dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type
319                 ubyte[4] crc = dc.crc.finish;
320                 reverse(crc[]);
321                 if (crc != dc.chunkmeta[0..4])
322                     throw new ImageIOException("corrupt chunk");
323         }
324     }
325 
326     return result;
327 }
328 
329 enum PNG_ColorType : ubyte {
330     Y    = 0,
331     RGB  = 2,
332     Idx  = 3,
333     YA   = 4,
334     RGBA = 6,
335 }
336 
337 enum PNG_FilterType : ubyte {
338     None    = 0,
339     Sub     = 1,
340     Up      = 2,
341     Average = 3,
342     Paeth   = 4,
343 }
344 
345 enum InterlaceMethod {
346     None = 0, Adam7 = 1
347 }
348 
349 union Buffer {
350     ubyte[] bpc8;
351     ushort[] bpc16;
352 }
353 
354 Buffer read_IDAT_stream(ref PNG_Decoder dc, int len) {
355     assert(dc.req_bpc == 8 || dc.req_bpc == 16);
356 
357     // initialize zlib stream
358     z_stream z = { zalloc: null, zfree: null, opaque: null };
359     if (inflateInit(&z) != Z_OK)
360         throw new ImageIOException("can't init zlib");
361     dc.z = &z;
362     dc.avail_idat = len;
363     scope(exit)
364         inflateEnd(&z);
365 
366     const size_t filter_step = dc.src_indexed
367                              ? 1 : dc.src_chans * (dc.bpc == 8 ? 1 : 2);
368 
369     ubyte[] depaletted = dc.src_indexed ? new ubyte[dc.w * 4] : null;
370 
371     auto cline = new ubyte[dc.w * filter_step + 1]; // +1 for filter type byte
372     auto pline = new ubyte[dc.w * filter_step + 1]; // +1 for filter type byte
373     auto cline8 = (dc.req_bpc == 8 && dc.bpc != 8) ? new ubyte[dc.w * dc.src_chans] : null;
374     auto cline16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.src_chans] : null;
375     ubyte[]  result8  = (dc.req_bpc == 8)  ? new ubyte[dc.w * dc.h * dc.tgt_chans] : null;
376     ushort[] result16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.h * dc.tgt_chans] : null;
377 
378     const LineConv!ubyte convert8   = get_converter!ubyte(dc.src_chans, dc.tgt_chans);
379     const LineConv!ushort convert16 = get_converter!ushort(dc.src_chans, dc.tgt_chans);
380 
381     if (dc.ilace == InterlaceMethod.None) {
382         const size_t src_linelen = dc.w * dc.src_chans;
383         const size_t tgt_linelen = dc.w * dc.tgt_chans;
384 
385         size_t ti = 0;    // target index
386         foreach (j; 0 .. dc.h) {
387             uncompress(dc, cline);
388             ubyte filter_type = cline[0];
389 
390             recon(cline[1..$], pline[1..$], filter_type, filter_step);
391 
392             ubyte[] bytes;  // defiltered bytes or 8-bit samples from palette
393             if (dc.src_indexed) {
394                 depalette(dc.palette, dc.transparency, cline[1..$], depaletted);
395                 bytes = depaletted[0 .. src_linelen];
396             } else {
397                 bytes = cline[1..$];
398             }
399 
400             // convert colors
401             if (dc.req_bpc == 8) {
402                 line8_from_bytes(bytes, dc.bpc, cline8);
403                 convert8(cline8[0 .. src_linelen], result8[ti .. ti + tgt_linelen]);
404             } else {
405                 line16_from_bytes(bytes, dc.bpc, cline16);
406                 convert16(cline16[0 .. src_linelen], result16[ti .. ti + tgt_linelen]);
407             }
408 
409             ti += tgt_linelen;
410 
411             ubyte[] _swap = pline;
412             pline = cline;
413             cline = _swap;
414         }
415     } else {
416         // Adam7 interlacing
417 
418         immutable size_t[7] redw = [(dc.w + 7) / 8,
419                                     (dc.w + 3) / 8,
420                                     (dc.w + 3) / 4,
421                                     (dc.w + 1) / 4,
422                                     (dc.w + 1) / 2,
423                                     (dc.w + 0) / 2,
424                                     (dc.w + 0) / 1];
425 
426         immutable size_t[7] redh = [(dc.h + 7) / 8,
427                                     (dc.h + 7) / 8,
428                                     (dc.h + 3) / 8,
429                                     (dc.h + 3) / 4,
430                                     (dc.h + 1) / 4,
431                                     (dc.h + 1) / 2,
432                                     (dc.h + 0) / 2];
433 
434         auto redline8 = (dc.req_bpc == 8) ? new ubyte[dc.w * dc.tgt_chans] : null;
435         auto redline16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.tgt_chans] : null;
436 
437         foreach (pass; 0 .. 7) {
438             const A7_Catapult tgt_px = a7_catapults[pass];   // target pixel
439             const size_t src_linelen = redw[pass] * dc.src_chans;
440             ubyte[] cln = cline[0 .. redw[pass] * filter_step + 1];
441             ubyte[] pln = pline[0 .. redw[pass] * filter_step + 1];
442             pln[] = 0;
443 
444             foreach (j; 0 .. redh[pass]) {
445                 uncompress(dc, cln);
446                 ubyte filter_type = cln[0];
447 
448                 recon(cln[1..$], pln[1..$], filter_type, filter_step);
449 
450                 ubyte[] bytes;  // defiltered bytes or 8-bit samples from palette
451                 if (dc.src_indexed) {
452                     depalette(dc.palette, dc.transparency, cln[1..$], depaletted);
453                     bytes = depaletted[0 .. src_linelen];
454                 } else {
455                     bytes = cln[1..$];
456                 }
457 
458                 // convert colors and sling pixels from reduced image to final buffer
459                 if (dc.req_bpc == 8) {
460                     line8_from_bytes(bytes, dc.bpc, cline8);
461                     convert8(cline8[0 .. src_linelen], redline8[0 .. redw[pass]*dc.tgt_chans]);
462                     for (size_t i, redi; i < redw[pass]; ++i, redi += dc.tgt_chans) {
463                         size_t tgt = tgt_px(i, j, dc.w) * dc.tgt_chans;
464                         result8[tgt .. tgt + dc.tgt_chans] =
465                             redline8[redi .. redi + dc.tgt_chans];
466                     }
467                 } else {
468                     line16_from_bytes(bytes, dc.bpc, cline16);
469                     convert16(cline16[0 .. src_linelen], redline16[0 .. redw[pass]*dc.tgt_chans]);
470                     for (size_t i, redi; i < redw[pass]; ++i, redi += dc.tgt_chans) {
471                         size_t tgt = tgt_px(i, j, dc.w) * dc.tgt_chans;
472                         result16[tgt .. tgt + dc.tgt_chans] =
473                             redline16[redi .. redi + dc.tgt_chans];
474                     }
475                 }
476 
477                 ubyte[] _swap = pln;
478                 pln = cln;
479                 cln = _swap;
480             }
481         }
482     }
483 
484     Buffer result;
485     switch (dc.req_bpc) {
486         case 8: result.bpc8 = result8; return result;
487         case 16: result.bpc16 = result16; return result;
488         default: throw new ImageIOException("internal error");
489     }
490 }
491 
492 void line8_from_bytes(ubyte[] src, int bpc, ref ubyte[] tgt) {
493     switch (bpc) {
494     case 8:
495         tgt = src;
496         break;
497     case 16:
498         for (size_t k, t;   k < src.length;   k+=2, t+=1) { tgt[t] = src[k]; /* truncate */ }
499         break;
500     default: throw new ImageIOException("unsupported bit depth (and bug)");
501     }
502 }
503 
504 void line16_from_bytes(in ubyte[] src, int bpc, ushort[] tgt) {
505     switch (bpc) {
506     case 8:
507         for (size_t k;   k < src.length;   k+=1) { tgt[k] = src[k] * 256 + 128; }
508         break;
509     case 16:
510         for (size_t k, t;   k < src.length;   k+=2, t+=1) { tgt[t] = src[k] << 8 | src[k+1]; }
511         break;
512     default: throw new ImageIOException("unsupported bit depth (and bug)");
513     }
514 }
515 
516 void depalette(in ubyte[] palette, in ubyte[] transparency, in ubyte[] src_line, ubyte[] depaletted) pure {
517     for (size_t s, d;  s < src_line.length;  s+=1, d+=4) {
518         ubyte pid = src_line[s];
519         size_t pidx = pid * 3;
520         if (palette.length < pidx + 3)
521             throw new ImageIOException("palette index wrong");
522         depaletted[d .. d+3] = palette[pidx .. pidx+3];
523         depaletted[d+3] = (pid < transparency.length) ? transparency[pid] : 255;
524     }
525 }
526 
527 alias A7_Catapult = size_t function(size_t redx, size_t redy, size_t dstw);
528 immutable A7_Catapult[7] a7_catapults = [
529     &a7_red1_to_dst,
530     &a7_red2_to_dst,
531     &a7_red3_to_dst,
532     &a7_red4_to_dst,
533     &a7_red5_to_dst,
534     &a7_red6_to_dst,
535     &a7_red7_to_dst,
536 ];
537 
538 pure nothrow {
539   size_t a7_red1_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*8*dstw + redx*8;     }
540   size_t a7_red2_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*8*dstw + redx*8+4;   }
541   size_t a7_red3_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*8+4)*dstw + redx*4; }
542   size_t a7_red4_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*4*dstw + redx*4+2;   }
543   size_t a7_red5_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*4+2)*dstw + redx*2; }
544   size_t a7_red6_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*2*dstw + redx*2+1;   }
545   size_t a7_red7_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*2+1)*dstw + redx;   }
546 }
547 
548 // Uncompresses a line from the IDAT stream into dst.
549 void uncompress(ref PNG_Decoder dc, ubyte[] dst)
550 {
551     dc.z.avail_out = cast(uint) dst.length;
552     dc.z.next_out = dst.ptr;
553 
554     while (true) {
555         if (!dc.z.avail_in) {
556             if (!dc.avail_idat) {
557                 dc.stream.readExact(dc.chunkmeta, 12);   // crc | len & type
558                 ubyte[4] crc = dc.crc.finish;
559                 reverse(crc[]);
560                 if (crc != dc.chunkmeta[0..4])
561                     throw new ImageIOException("corrupt chunk");
562                 dc.avail_idat = bigEndianToNative!uint(dc.chunkmeta[4..8]);
563                 if (!dc.avail_idat)
564                     throw new ImageIOException("invalid data");
565                 if (dc.chunkmeta[8..12] != "IDAT")
566                     throw new ImageIOException("not enough data");
567                 dc.crc.put(dc.chunkmeta[8..12]);
568             }
569 
570             const size_t n = min(dc.avail_idat, dc.read_buf.length);
571             dc.stream.readExact(dc.read_buf, n);
572             dc.idat_window = dc.read_buf[0..n];
573 
574             if (!dc.idat_window)
575                 throw new ImageIOException("TODO");
576             dc.crc.put(dc.idat_window);
577             dc.avail_idat -= cast(uint) dc.idat_window.length;
578             dc.z.avail_in = cast(uint) dc.idat_window.length;
579             dc.z.next_in = dc.idat_window.ptr;
580         }
581 
582         int q = inflate(dc.z, Z_NO_FLUSH);
583 
584         if (dc.z.avail_out == 0)
585             return;
586         if (q != Z_OK)
587             throw new ImageIOException("zlib error");
588     }
589 }
590 
591 void recon(ubyte[] cline, in ubyte[] pline, ubyte ftype, size_t fstep) pure {
592     switch (ftype) with (PNG_FilterType) {
593         case None:
594             break;
595         case Sub:
596             foreach (k; fstep .. cline.length)
597                 cline[k] += cline[k-fstep];
598             break;
599         case Up:
600             foreach (k; 0 .. cline.length)
601                 cline[k] += pline[k];
602             break;
603         case Average:
604             foreach (k; 0 .. fstep)
605                 cline[k] += pline[k] / 2;
606             foreach (k; fstep .. cline.length)
607                 cline[k] += cast(ubyte)
608                     ((cast(uint) cline[k-fstep] + cast(uint) pline[k]) / 2);
609             break;
610         case Paeth:
611             foreach (i; 0 .. fstep)
612                 cline[i] += paeth(0, pline[i], 0);
613             foreach (i; fstep .. cline.length)
614                 cline[i] += paeth(cline[i-fstep], pline[i], pline[i-fstep]);
615             break;
616         default:
617             throw new ImageIOException("filter type not supported");
618     }
619 }
620 
621 ubyte paeth(ubyte a, ubyte b, ubyte c) pure nothrow {
622     int pc = c;
623     int pa = b - pc;
624     int pb = a - pc;
625     pc = pa + pb;
626     if (pa < 0) pa = -pa;
627     if (pb < 0) pb = -pb;
628     if (pc < 0) pc = -pc;
629 
630     if (pa <= pb && pa <= pc) {
631         return a;
632     } else if (pb <= pc) {
633         return b;
634     }
635     return c;
636 }
637 
638 // ----------------------------------------------------------------------
639 // PNG encoder
640 
641 void write_png(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) {
642     if (w < 1 || h < 1 || int.max < w || int.max < h)
643         throw new ImageIOException("invalid dimensions");
644     uint src_chans = cast(uint) (data.length / w / h);
645     if (src_chans < 1 || 4 < src_chans || tgt_chans < 0 || 4 < tgt_chans)
646         throw new ImageIOException("invalid channel count");
647     if (src_chans * w * h != data.length)
648         throw new ImageIOException("mismatching dimensions and length");
649 
650     PNG_Encoder ec = {
651         stream    : stream,
652         w         : cast(size_t) w,
653         h         : cast(size_t) h,
654         src_chans : src_chans,
655         tgt_chans : tgt_chans ? cast(uint) tgt_chans : src_chans,
656         data      : data,
657     };
658 
659     write_png(ec);
660     stream.flush();
661 }
662 
663 enum MAXIMUM_CHUNK_SIZE = 8192;
664 
665 struct PNG_Encoder {
666     Writer stream;
667     size_t w, h;
668     uint src_chans;
669     uint tgt_chans;
670     const(ubyte)[] data;
671 
672     CRC32       crc;
673     z_stream*   z;
674     ubyte[]     idatbuf;
675 }
676 
677 void write_png(ref PNG_Encoder ec) {
678     ubyte[33] hdr = void;
679     hdr[ 0 ..  8] = png_file_header;
680     hdr[ 8 .. 16] = png_image_header;
681     hdr[16 .. 20] = nativeToBigEndian(cast(uint) ec.w);
682     hdr[20 .. 24] = nativeToBigEndian(cast(uint) ec.h);
683     hdr[24      ] = 8;  // bit depth
684     hdr[25      ] = color_type(ec.tgt_chans);
685     hdr[26 .. 29] = 0;  // compression, filter and interlace methods
686     ec.crc.start();
687     ec.crc.put(hdr[12 .. 29]);
688     ubyte[4] crc = ec.crc.finish();
689     reverse(crc[]);
690     hdr[29 .. 33] = crc;
691     ec.stream.rawWrite(hdr);
692 
693     write_IDATs(ec);
694 
695     static immutable ubyte[12] iend =
696         [0, 0, 0, 0, 'I','E','N','D', 0xae, 0x42, 0x60, 0x82];
697     ec.stream.rawWrite(iend);
698 }
699 
700 void write_IDATs(ref PNG_Encoder ec) {
701     // initialize zlib stream
702     z_stream z = { zalloc: null, zfree: null, opaque: null };
703     if (deflateInit(&z, Z_DEFAULT_COMPRESSION) != Z_OK)
704         throw new ImageIOException("zlib init error");
705     scope(exit)
706         deflateEnd(ec.z);
707     ec.z = &z;
708 
709     const LineConv!ubyte convert = get_converter!ubyte(ec.src_chans, ec.tgt_chans);
710 
711     const size_t filter_step = ec.tgt_chans;   // step between pixels, in bytes
712     const size_t slinesz = ec.w * ec.src_chans;
713     const size_t tlinesz = ec.w * ec.tgt_chans + 1;
714     const size_t workbufsz = 3 * tlinesz + MAXIMUM_CHUNK_SIZE;
715 
716     ubyte[] workbuf  = new ubyte[workbufsz];
717     ubyte[] cline    = workbuf[0 .. tlinesz];
718     ubyte[] pline    = workbuf[tlinesz .. 2 * tlinesz];
719     ubyte[] filtered = workbuf[2 * tlinesz .. 3 * tlinesz];
720     ec.idatbuf       = workbuf[$-MAXIMUM_CHUNK_SIZE .. $];
721     workbuf[0..$] = 0;
722     ec.z.avail_out = cast(uint) ec.idatbuf.length;
723     ec.z.next_out = ec.idatbuf.ptr;
724 
725     const size_t ssize = ec.w * ec.src_chans * ec.h;
726 
727     for (size_t si; si < ssize; si += slinesz) {
728         convert(ec.data[si .. si + slinesz], cline[1..$]);
729 
730         // these loops could be merged with some extra space...
731         foreach (i; 1 .. filter_step+1)
732             filtered[i] = cast(ubyte) (cline[i] - paeth(0, pline[i], 0));
733         foreach (i; filter_step+1 .. tlinesz)
734             filtered[i] = cast(ubyte)
735             (cline[i] - paeth(cline[i-filter_step], pline[i], pline[i-filter_step]));
736         filtered[0] = PNG_FilterType.Paeth;
737 
738         compress(ec, filtered);
739         ubyte[] _swap = pline;
740         pline = cline;
741         cline = _swap;
742     }
743 
744     while (true) {  // flush zlib
745         int q = deflate(ec.z, Z_FINISH);
746         if (ec.idatbuf.length - ec.z.avail_out > 0)
747             flush_idat(ec);
748         if (q == Z_STREAM_END) break;
749         if (q == Z_OK) continue;    // not enough avail_out
750         throw new ImageIOException("zlib compression error");
751     }
752 }
753 
754 void compress(ref PNG_Encoder ec, in ubyte[] line)
755 {
756     ec.z.avail_in = cast(uint) line.length;
757     ec.z.next_in = line.ptr;
758     while (ec.z.avail_in) {
759         int q = deflate(ec.z, Z_NO_FLUSH);
760         if (q != Z_OK)
761             throw new ImageIOException("zlib compression error");
762         if (ec.z.avail_out == 0)
763             flush_idat(ec);
764     }
765 }
766 
767 void flush_idat(ref PNG_Encoder ec)      // writes an idat chunk
768 {
769     const uint len = cast(uint) (ec.idatbuf.length - ec.z.avail_out);
770     ec.crc.put(cast(const(ubyte)[]) "IDAT");
771     ec.crc.put(ec.idatbuf[0 .. len]);
772     ubyte[8] meta;
773     meta[0..4] = nativeToBigEndian!uint(len);
774     meta[4..8] = cast(ubyte[4]) "IDAT";
775     ec.stream.rawWrite(meta);
776     ec.stream.rawWrite(ec.idatbuf[0 .. len]);
777     ubyte[4] crc = ec.crc.finish();
778     reverse(crc[]);
779     ec.stream.rawWrite(crc[0..$]);
780     ec.z.next_out = ec.idatbuf.ptr;
781     ec.z.avail_out = cast(uint) ec.idatbuf.length;
782 }
783 
784 package void read_png_info(Reader stream, out int w, out int h, out int chans) {
785     PNG_Header hdr = read_png_header(stream);
786     w = hdr.width;
787     h = hdr.height;
788     chans = channels(cast(PNG_ColorType) hdr.color_type);
789 }