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