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 }