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 }