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