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