1 /// PNG file read and write. Leverages color.d's Image interfaces for interop. 2 module arsd.png; 3 4 import core.memory; 5 6 /// Easily reads a png file into a MemoryImage 7 MemoryImage readPng(string filename) { 8 import std.file; 9 return imageFromPng(readPng(cast(ubyte[]) read(filename))); 10 } 11 12 /// Saves a MemoryImage to a png. See also: writeImageToPngFile which uses memory a little more efficiently 13 void writePng(string filename, MemoryImage mi) { 14 // FIXME: it would be nice to write the file lazily so we don't have so many intermediate buffers here 15 PNG* png; 16 if(auto p = cast(IndexedImage) mi) 17 png = pngFromImage(p); 18 else if(auto p = cast(TrueColorImage) mi) 19 png = pngFromImage(p); 20 else assert(0); 21 import std.file; 22 std.file.write(filename, writePng(png)); 23 } 24 25 /// 26 enum PngType { 27 greyscale = 0, /// The data must be `depth` bits per pixel 28 truecolor = 2, /// The data will be RGB triples, so `depth * 3` bits per pixel. Depth must be 8 or 16. 29 indexed = 3, /// The data must be `depth` bits per pixel, with a palette attached. Use [writePng] with [IndexedImage] for this mode. Depth must be <= 8. 30 greyscale_with_alpha = 4, /// The data must be (grey, alpha) byte pairs for each pixel. Thus `depth * 2` bits per pixel. Depth must be 8 or 16. 31 truecolor_with_alpha = 6 /// The data must be RGBA quads for each pixel. Thus, `depth * 4` bits per pixel. Depth must be 8 or 16. 32 } 33 34 /// Saves an image from an existing array. Note that depth other than 8 may not be implemented yet. Also note depth of 16 must be stored big endian 35 void writePng(string filename, const ubyte[] data, int width, int height, PngType type, ubyte depth = 8) { 36 PngHeader h; 37 h.width = width; 38 h.height = height; 39 h.type = cast(ubyte) type; 40 h.depth = depth; 41 42 auto png = blankPNG(h); 43 addImageDatastreamToPng(data, png); 44 45 import std.file; 46 std.file.write(filename, writePng(png)); 47 } 48 49 50 /* 51 //Here's a simple test program that shows how to write a quick image viewer with simpledisplay: 52 53 import arsd.png; 54 import arsd.simpledisplay; 55 56 import std.file; 57 void main(string[] args) { 58 // older api, the individual functions give you more control if you need it 59 //auto img = imageFromPng(readPng(cast(ubyte[]) read(args[1]))); 60 61 // newer api, simpler but less control 62 auto img = readPng(args[1]); 63 64 // displayImage is from simpledisplay and just pops up a window to show the image 65 // simpledisplay's Images are a little different than MemoryImages that this loads, 66 // but conversion is easy 67 displayImage(Image.fromMemoryImage(img)); 68 } 69 */ 70 71 // By Adam D. Ruppe, 2009-2010, released into the public domain 72 //import std.file; 73 74 //import std.zlib; 75 76 public import arsd.color; 77 78 /** 79 The return value should be casted to indexed or truecolor depending on what the file is. You can 80 also use getAsTrueColorImage to forcibly convert it if needed. 81 82 To get an image from a png file, do something like this: 83 84 auto i = cast(TrueColorImage) imageFromPng(readPng(cast(ubyte)[]) std.file.read("file.png"))); 85 */ 86 MemoryImage imageFromPng(PNG* png) { 87 PngHeader h = getHeader(png); 88 89 /** Types from the PNG spec: 90 0 - greyscale 91 2 - truecolor 92 3 - indexed color 93 4 - grey with alpha 94 6 - true with alpha 95 96 1, 5, and 7 are invalid. 97 98 There's a kind of bitmask going on here: 99 If type&1, it has a palette. 100 If type&2, it is in color. 101 If type&4, it has an alpha channel in the datastream. 102 */ 103 104 MemoryImage i; 105 ubyte[] idata; 106 // FIXME: some duplication with the lazy reader below in the module 107 108 switch(h.type) { 109 case 0: // greyscale 110 case 4: // greyscale with alpha 111 // this might be a different class eventually... 112 auto a = new TrueColorImage(h.width, h.height); 113 idata = a.imageData.bytes; 114 i = a; 115 break; 116 case 2: // truecolor 117 case 6: // truecolor with alpha 118 auto a = new TrueColorImage(h.width, h.height); 119 idata = a.imageData.bytes; 120 i = a; 121 break; 122 case 3: // indexed 123 auto a = new IndexedImage(h.width, h.height); 124 a.palette = fetchPalette(png); 125 a.hasAlpha = true; // FIXME: don't be so conservative here 126 idata = a.data; 127 i = a; 128 break; 129 default: 130 assert(0, "invalid png"); 131 } 132 133 size_t idataIdx = 0; 134 135 auto file = LazyPngFile!(Chunk[])(png.chunks); 136 immutable(ubyte)[] previousLine; 137 auto bpp = bytesPerPixel(h); 138 foreach(line; file.rawDatastreamByChunk()) { 139 auto filter = line[0]; 140 auto data = unfilter(filter, line[1 .. $], previousLine, bpp); 141 previousLine = data; 142 143 convertPngData(h.type, h.depth, data, h.width, idata, idataIdx); 144 } 145 assert(idataIdx == idata.length, "not all filled, wtf"); 146 147 assert(i !is null); 148 149 return i; 150 } 151 152 // idata needs to be already sized for the image! width * height if indexed, width*height*4 if not. 153 void convertPngData(ubyte type, ubyte depth, const(ubyte)[] data, int width, ubyte[] idata, ref size_t idataIdx) { 154 ubyte consumeOne() { 155 ubyte ret = data[0]; 156 data = data[1 .. $]; 157 return ret; 158 } 159 import std.conv; 160 161 loop: for(int pixel = 0; pixel < width; pixel++) 162 switch(type) { 163 case 0: // greyscale 164 case 4: // greyscale with alpha 165 case 3: // indexed 166 167 void acceptPixel(ubyte p) { 168 if(type == 3) { 169 idata[idataIdx++] = p; 170 } else { 171 if(depth == 1) { 172 p = p ? 0xff : 0; 173 } else if (depth == 2) { 174 p |= p << 2; 175 p |= p << 4; 176 } 177 else if (depth == 4) { 178 p |= p << 4; 179 } 180 idata[idataIdx++] = p; 181 idata[idataIdx++] = p; 182 idata[idataIdx++] = p; 183 184 if(type == 0) 185 idata[idataIdx++] = 255; 186 else if(type == 4) 187 idata[idataIdx++] = consumeOne(); 188 } 189 } 190 191 auto b = consumeOne(); 192 switch(depth) { 193 case 1: 194 acceptPixel((b >> 7) & 0x01); 195 pixel++; if(pixel == width) break loop; 196 acceptPixel((b >> 6) & 0x01); 197 pixel++; if(pixel == width) break loop; 198 acceptPixel((b >> 5) & 0x01); 199 pixel++; if(pixel == width) break loop; 200 acceptPixel((b >> 4) & 0x01); 201 pixel++; if(pixel == width) break loop; 202 acceptPixel((b >> 3) & 0x01); 203 pixel++; if(pixel == width) break loop; 204 acceptPixel((b >> 2) & 0x01); 205 pixel++; if(pixel == width) break loop; 206 acceptPixel((b >> 1) & 0x01); 207 pixel++; if(pixel == width) break loop; 208 acceptPixel(b & 0x01); 209 break; 210 case 2: 211 acceptPixel((b >> 6) & 0x03); 212 pixel++; if(pixel == width) break loop; 213 acceptPixel((b >> 4) & 0x03); 214 pixel++; if(pixel == width) break loop; 215 acceptPixel((b >> 2) & 0x03); 216 pixel++; if(pixel == width) break loop; 217 acceptPixel(b & 0x03); 218 break; 219 case 4: 220 acceptPixel((b >> 4) & 0x0f); 221 pixel++; if(pixel == width) break loop; 222 acceptPixel(b & 0x0f); 223 break; 224 case 8: 225 acceptPixel(b); 226 break; 227 case 16: 228 assert(type != 3); // 16 bit indexed isn't supported per png spec 229 acceptPixel(b); 230 consumeOne(); // discarding the least significant byte as we can't store it anyway 231 break; 232 default: 233 assert(0, "bit depth not implemented"); 234 } 235 break; 236 case 2: // truecolor 237 case 6: // true with alpha 238 if(depth == 8) { 239 idata[idataIdx++] = consumeOne(); 240 idata[idataIdx++] = consumeOne(); 241 idata[idataIdx++] = consumeOne(); 242 idata[idataIdx++] = (type == 6) ? consumeOne() : 255; 243 } else if(depth == 16) { 244 idata[idataIdx++] = consumeOne(); 245 consumeOne(); 246 idata[idataIdx++] = consumeOne(); 247 consumeOne(); 248 idata[idataIdx++] = consumeOne(); 249 consumeOne(); 250 idata[idataIdx++] = (type == 6) ? consumeOne() : 255; 251 if(type == 6) 252 consumeOne(); 253 254 } else assert(0, "unsupported truecolor bit depth " ~ to!string(depth)); 255 break; 256 default: assert(0); 257 } 258 assert(data.length == 0, "not all consumed, wtf " ~ to!string(data)); 259 } 260 261 /* 262 struct PngHeader { 263 uint width; 264 uint height; 265 ubyte depth = 8; 266 ubyte type = 6; // 0 - greyscale, 2 - truecolor, 3 - indexed color, 4 - grey with alpha, 6 - true with alpha 267 ubyte compressionMethod = 0; // should be zero 268 ubyte filterMethod = 0; // should be zero 269 ubyte interlaceMethod = 0; // bool 270 } 271 */ 272 273 274 PNG* pngFromImage(IndexedImage i) { 275 PngHeader h; 276 h.width = i.width; 277 h.height = i.height; 278 h.type = 3; 279 if(i.numColors() <= 2) 280 h.depth = 1; 281 else if(i.numColors() <= 4) 282 h.depth = 2; 283 else if(i.numColors() <= 16) 284 h.depth = 4; 285 else if(i.numColors() <= 256) 286 h.depth = 8; 287 else throw new Exception("can't save this as an indexed png"); 288 289 auto png = blankPNG(h); 290 291 // do palette and alpha 292 // FIXME: if there is only one transparent color, set it as the special chunk for that 293 294 // FIXME: we'd get a smaller file size if the transparent pixels were arranged first 295 Chunk palette; 296 palette.type = ['P', 'L', 'T', 'E']; 297 palette.size = cast(int) i.palette.length * 3; 298 palette.payload.length = palette.size; 299 300 Chunk alpha; 301 if(i.hasAlpha) { 302 alpha.type = ['t', 'R', 'N', 'S']; 303 alpha.size = cast(uint) i.palette.length; 304 alpha.payload.length = alpha.size; 305 } 306 307 for(int a = 0; a < i.palette.length; a++) { 308 palette.payload[a*3+0] = i.palette[a].r; 309 palette.payload[a*3+1] = i.palette[a].g; 310 palette.payload[a*3+2] = i.palette[a].b; 311 if(i.hasAlpha) 312 alpha.payload[a] = i.palette[a].a; 313 } 314 315 palette.checksum = crc("PLTE", palette.payload); 316 png.chunks ~= palette; 317 if(i.hasAlpha) { 318 alpha.checksum = crc("tRNS", alpha.payload); 319 png.chunks ~= alpha; 320 } 321 322 // do the datastream 323 if(h.depth == 8) { 324 addImageDatastreamToPng(i.data, png); 325 } else { 326 // gotta convert it 327 ubyte[] datastream = new ubyte[cast(size_t)i.width * i.height * h.depth / 8]; // FIXME? 328 int shift = 0; 329 330 switch(h.depth) { 331 default: assert(0); 332 case 1: shift = 7; break; 333 case 2: shift = 6; break; 334 case 4: shift = 4; break; 335 case 8: shift = 0; break; 336 } 337 size_t dsp = 0; 338 size_t dpos = 0; 339 bool justAdvanced; 340 for(int y = 0; y < i.height; y++) { 341 for(int x = 0; x < i.width; x++) { 342 datastream[dsp] |= i.data[dpos++] << shift; 343 344 switch(h.depth) { 345 default: assert(0); 346 case 1: shift-= 1; break; 347 case 2: shift-= 2; break; 348 case 4: shift-= 4; break; 349 case 8: shift-= 8; break; 350 } 351 352 justAdvanced = shift < 0; 353 if(shift < 0) { 354 dsp++; 355 switch(h.depth) { 356 default: assert(0); 357 case 1: shift = 7; break; 358 case 2: shift = 6; break; 359 case 4: shift = 4; break; 360 case 8: shift = 0; break; 361 } 362 } 363 } 364 if(!justAdvanced) 365 dsp++; 366 switch(h.depth) { 367 default: assert(0); 368 case 1: shift = 7; break; 369 case 2: shift = 6; break; 370 case 4: shift = 4; break; 371 case 8: shift = 0; break; 372 } 373 374 } 375 376 addImageDatastreamToPng(datastream, png); 377 } 378 379 return png; 380 } 381 382 PNG* pngFromImage(TrueColorImage i) { 383 PngHeader h; 384 h.width = i.width; 385 h.height = i.height; 386 // FIXME: optimize it if it is greyscale or doesn't use alpha alpha 387 388 auto png = blankPNG(h); 389 addImageDatastreamToPng(i.imageData.bytes, png); 390 391 return png; 392 } 393 394 /* 395 void main(string[] args) { 396 auto a = readPng(cast(ubyte[]) read(args[1])); 397 auto f = getDatastream(a); 398 399 foreach(i; f) { 400 writef("%d ", i); 401 } 402 403 writefln("\n\n%d", f.length); 404 } 405 */ 406 407 struct PNG { 408 uint length; 409 ubyte[8] header; 410 Chunk[] chunks; 411 412 Chunk* getChunk(string what) { 413 foreach(ref c; chunks) { 414 if(c.stype == what) 415 return &c; 416 } 417 throw new Exception("no such chunk " ~ what); 418 } 419 420 Chunk* getChunkNullable(string what) { 421 foreach(ref c; chunks) { 422 if(c.stype == what) 423 return &c; 424 } 425 return null; 426 } 427 428 // Insert chunk before IDAT. PNG specs allows to drop all chunks after IDAT, 429 // so we have to insert our custom chunks right before it. 430 // Use `Chunk.create()` to create new chunk, and then `insertChunk()` to add it. 431 // Return `true` if we did replacement. 432 bool insertChunk (Chunk* chk, bool replaceExisting=false) { 433 if (chk is null) return false; // just in case 434 // use reversed loop, as "IDAT" is usually present, and it is usually the last, 435 // so we will somewhat amortize painter's algorithm here. 436 foreach_reverse (immutable idx, ref cc; chunks) { 437 if (replaceExisting && cc.type == chk.type) { 438 // replace existing chunk, the easiest case 439 chunks[idx] = *chk; 440 return true; 441 } 442 if (cc.stype == "IDAT") { 443 // ok, insert it; and don't use phobos 444 chunks.length += 1; 445 foreach_reverse (immutable c; idx+1..chunks.length) chunks.ptr[c] = chunks.ptr[c-1]; 446 chunks.ptr[idx] = *chk; 447 return false; 448 } 449 } 450 chunks ~= *chk; 451 return false; 452 } 453 454 // Convenient wrapper for `insertChunk()`. 455 bool replaceChunk (Chunk* chk) { return insertChunk(chk, true); } 456 } 457 458 // this is just like writePng(filename, pngFromImage(image)), but it manages 459 // is own memory and writes straight to the file instead of using intermediate buffers that might not get gc'd right 460 void writeImageToPngFile(in char[] filename, TrueColorImage image) { 461 PNG* png; 462 ubyte[] com; 463 { 464 import std.zlib; 465 PngHeader h; 466 h.width = image.width; 467 h.height = image.height; 468 png = blankPNG(h); 469 470 size_t bytesPerLine = cast(size_t)h.width * 4; 471 if(h.type == 3) 472 bytesPerLine = cast(size_t)h.width * 8 / h.depth; 473 Chunk dat; 474 dat.type = ['I', 'D', 'A', 'T']; 475 size_t pos = 0; 476 477 auto compressor = new Compress(); 478 479 import core.stdc.stdlib; 480 auto lineBuffer = (cast(ubyte*)malloc(1 + bytesPerLine))[0 .. 1+bytesPerLine]; 481 scope(exit) free(lineBuffer.ptr); 482 483 while(pos+bytesPerLine <= image.imageData.bytes.length) { 484 lineBuffer[0] = 0; 485 lineBuffer[1..1+bytesPerLine] = image.imageData.bytes[pos.. pos+bytesPerLine]; 486 com ~= cast(ubyte[]) compressor.compress(lineBuffer); 487 pos += bytesPerLine; 488 } 489 490 com ~= cast(ubyte[]) compressor.flush(); 491 492 assert(com.length <= uint.max); 493 dat.size = cast(uint) com.length; 494 dat.payload = com; 495 dat.checksum = crc("IDAT", dat.payload); 496 497 png.chunks ~= dat; 498 499 Chunk c; 500 501 c.size = 0; 502 c.type = ['I', 'E', 'N', 'D']; 503 c.checksum = crc("IEND", c.payload); 504 505 png.chunks ~= c; 506 } 507 assert(png !is null); 508 509 import core.stdc.stdio; 510 import std.string; 511 FILE* fp = fopen(toStringz(filename), "wb"); 512 if(fp is null) 513 throw new Exception("Couldn't open png file for writing."); 514 scope(exit) fclose(fp); 515 516 fwrite(png.header.ptr, 1, 8, fp); 517 foreach(c; png.chunks) { 518 fputc((c.size & 0xff000000) >> 24, fp); 519 fputc((c.size & 0x00ff0000) >> 16, fp); 520 fputc((c.size & 0x0000ff00) >> 8, fp); 521 fputc((c.size & 0x000000ff) >> 0, fp); 522 523 fwrite(c.type.ptr, 1, 4, fp); 524 fwrite(c.payload.ptr, 1, c.size, fp); 525 526 fputc((c.checksum & 0xff000000) >> 24, fp); 527 fputc((c.checksum & 0x00ff0000) >> 16, fp); 528 fputc((c.checksum & 0x0000ff00) >> 8, fp); 529 fputc((c.checksum & 0x000000ff) >> 0, fp); 530 } 531 532 { import core.memory : GC; GC.free(com.ptr); } // there is a reference to this in the PNG struct, but it is going out of scope here too, so who cares 533 // just wanna make sure this crap doesn't stick around 534 } 535 536 ubyte[] writePng(PNG* p) { 537 ubyte[] a; 538 if(p.length) 539 a.length = p.length; 540 else { 541 a.length = 8; 542 foreach(c; p.chunks) 543 a.length += c.size + 12; 544 } 545 size_t pos; 546 547 a[0..8] = p.header[0..8]; 548 pos = 8; 549 foreach(c; p.chunks) { 550 a[pos++] = (c.size & 0xff000000) >> 24; 551 a[pos++] = (c.size & 0x00ff0000) >> 16; 552 a[pos++] = (c.size & 0x0000ff00) >> 8; 553 a[pos++] = (c.size & 0x000000ff) >> 0; 554 555 a[pos..pos+4] = c.type[0..4]; 556 pos += 4; 557 a[pos..pos+c.size] = c.payload[0..c.size]; 558 pos += c.size; 559 560 a[pos++] = (c.checksum & 0xff000000) >> 24; 561 a[pos++] = (c.checksum & 0x00ff0000) >> 16; 562 a[pos++] = (c.checksum & 0x0000ff00) >> 8; 563 a[pos++] = (c.checksum & 0x000000ff) >> 0; 564 } 565 566 return a; 567 } 568 569 PngHeader getHeaderFromFile(string filename) { 570 import std.stdio; 571 auto file = File(filename, "rb"); 572 ubyte[12] initialBuffer; // file header + size of first chunk (should be IHDR) 573 auto data = file.rawRead(initialBuffer[]); 574 if(data.length != 12) 575 throw new Exception("couldn't get png file header off " ~ filename); 576 577 if(data[0..8] != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) 578 throw new Exception("file " ~ filename ~ " is not a png"); 579 580 size_t pos = 8; 581 size_t size; 582 size |= data[pos++] << 24; 583 size |= data[pos++] << 16; 584 size |= data[pos++] << 8; 585 size |= data[pos++] << 0; 586 587 size += 4; // chunk type 588 size += 4; // checksum 589 590 ubyte[] more; 591 more.length = size; 592 593 auto chunk = file.rawRead(more); 594 if(chunk.length != size) 595 throw new Exception("couldn't get png image header off " ~ filename); 596 597 598 more = data ~ chunk; 599 600 auto png = readPng(more); 601 return getHeader(png); 602 } 603 604 PNG* readPng(in ubyte[] data) { 605 auto p = new PNG; 606 607 p.length = cast(int) data.length; 608 p.header[0..8] = data[0..8]; 609 610 if(p.header != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) 611 throw new Exception("not a png, header wrong"); 612 613 size_t pos = 8; 614 615 while(pos < data.length) { 616 Chunk n; 617 n.size |= data[pos++] << 24; 618 n.size |= data[pos++] << 16; 619 n.size |= data[pos++] << 8; 620 n.size |= data[pos++] << 0; 621 n.type[0..4] = data[pos..pos+4]; 622 pos += 4; 623 n.payload.length = n.size; 624 if(pos + n.size > data.length) 625 throw new Exception(format("malformed png, chunk '%s' %d @ %d longer than data %d", n.type, n.size, pos, data.length)); 626 if(pos + n.size < pos) 627 throw new Exception("uint overflow: chunk too large"); 628 n.payload[0..n.size] = data[pos..pos+n.size]; 629 pos += n.size; 630 631 n.checksum |= data[pos++] << 24; 632 n.checksum |= data[pos++] << 16; 633 n.checksum |= data[pos++] << 8; 634 n.checksum |= data[pos++] << 0; 635 636 p.chunks ~= n; 637 } 638 639 return p; 640 } 641 642 PNG* blankPNG(PngHeader h) { 643 auto p = new PNG; 644 p.header = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 645 646 Chunk c; 647 648 c.size = 13; 649 c.type = ['I', 'H', 'D', 'R']; 650 651 c.payload.length = 13; 652 size_t pos = 0; 653 654 c.payload[pos++] = h.width >> 24; 655 c.payload[pos++] = (h.width >> 16) & 0xff; 656 c.payload[pos++] = (h.width >> 8) & 0xff; 657 c.payload[pos++] = h.width & 0xff; 658 659 c.payload[pos++] = h.height >> 24; 660 c.payload[pos++] = (h.height >> 16) & 0xff; 661 c.payload[pos++] = (h.height >> 8) & 0xff; 662 c.payload[pos++] = h.height & 0xff; 663 664 c.payload[pos++] = h.depth; 665 c.payload[pos++] = h.type; 666 c.payload[pos++] = h.compressionMethod; 667 c.payload[pos++] = h.filterMethod; 668 c.payload[pos++] = h.interlaceMethod; 669 670 671 c.checksum = crc("IHDR", c.payload); 672 673 p.chunks ~= c; 674 675 return p; 676 } 677 678 // should NOT have any idata already. 679 // FIXME: doesn't handle palettes 680 void addImageDatastreamToPng(const(ubyte)[] data, PNG* png) { 681 // we need to go through the lines and add the filter byte 682 // then compress it into an IDAT chunk 683 // then add the IEND chunk 684 import std.zlib; 685 686 PngHeader h = getHeader(png); 687 688 if(h.depth == 0) 689 throw new Exception("depth of zero makes no sense"); 690 if(h.width == 0) 691 throw new Exception("width zero?!!?!?!"); 692 693 size_t bytesPerLine; 694 switch(h.type) { 695 case 0: 696 // FIXME: < 8 depth not supported here but should be 697 bytesPerLine = cast(size_t)h.width * 1 * h.depth / 8; 698 break; 699 case 2: 700 bytesPerLine = cast(size_t)h.width * 3 * h.depth / 8; 701 break; 702 case 3: 703 bytesPerLine = cast(size_t)h.width * 1 * h.depth / 8; 704 break; 705 case 4: 706 // FIXME: < 8 depth not supported here but should be 707 bytesPerLine = cast(size_t)h.width * 2 * h.depth / 8; 708 break; 709 case 6: 710 bytesPerLine = cast(size_t)h.width * 4 * h.depth / 8; 711 break; 712 default: assert(0); 713 714 } 715 Chunk dat; 716 dat.type = ['I', 'D', 'A', 'T']; 717 size_t pos = 0; 718 719 const(ubyte)[] output; 720 while(pos+bytesPerLine <= data.length) { 721 output ~= 0; 722 output ~= data[pos..pos+bytesPerLine]; 723 pos += bytesPerLine; 724 } 725 726 auto com = cast(ubyte[]) compress(output); 727 dat.size = cast(int) com.length; 728 dat.payload = com; 729 dat.checksum = crc("IDAT", dat.payload); 730 731 png.chunks ~= dat; 732 733 Chunk c; 734 735 c.size = 0; 736 c.type = ['I', 'E', 'N', 'D']; 737 c.checksum = crc("IEND", c.payload); 738 739 png.chunks ~= c; 740 741 } 742 743 deprecated alias PngHeader PNGHeader; 744 745 // bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey 746 747 ubyte[] getDatastream(PNG* p) { 748 import std.zlib; 749 ubyte[] compressed; 750 751 foreach(c; p.chunks) { 752 if(c.stype != "IDAT") 753 continue; 754 compressed ~= c.payload; 755 } 756 757 return cast(ubyte[]) uncompress(compressed); 758 } 759 760 // FIXME: Assuming 8 bits per pixel 761 ubyte[] getUnfilteredDatastream(PNG* p) { 762 PngHeader h = getHeader(p); 763 assert(h.filterMethod == 0); 764 765 assert(h.type == 3); // FIXME 766 assert(h.depth == 8); // FIXME 767 768 ubyte[] data = getDatastream(p); 769 ubyte[] ufdata = new ubyte[data.length - h.height]; 770 771 int bytesPerLine = cast(int) ufdata.length / h.height; 772 773 int pos = 0, pos2 = 0; 774 for(int a = 0; a < h.height; a++) { 775 assert(data[pos2] == 0); 776 ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1]; 777 pos+= bytesPerLine; 778 pos2+= bytesPerLine + 1; 779 } 780 781 return ufdata; 782 } 783 784 ubyte[] getFlippedUnfilteredDatastream(PNG* p) { 785 PngHeader h = getHeader(p); 786 assert(h.filterMethod == 0); 787 788 assert(h.type == 3); // FIXME 789 assert(h.depth == 8 || h.depth == 4); // FIXME 790 791 ubyte[] data = getDatastream(p); 792 ubyte[] ufdata = new ubyte[data.length - h.height]; 793 794 int bytesPerLine = cast(int) ufdata.length / h.height; 795 796 797 int pos = cast(int) ufdata.length - bytesPerLine, pos2 = 0; 798 for(int a = 0; a < h.height; a++) { 799 assert(data[pos2] == 0); 800 ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1]; 801 pos-= bytesPerLine; 802 pos2+= bytesPerLine + 1; 803 } 804 805 return ufdata; 806 } 807 808 ubyte getHighNybble(ubyte a) { 809 return cast(ubyte)(a >> 4); // FIXME 810 } 811 812 ubyte getLowNybble(ubyte a) { 813 return a & 0x0f; 814 } 815 816 // Takes the transparency info and returns 817 ubyte[] getANDMask(PNG* p) { 818 PngHeader h = getHeader(p); 819 assert(h.filterMethod == 0); 820 821 assert(h.type == 3); // FIXME 822 assert(h.depth == 8 || h.depth == 4); // FIXME 823 824 assert(h.width % 8 == 0); // might actually be %2 825 826 ubyte[] data = getDatastream(p); 827 ubyte[] ufdata = new ubyte[h.height*((((h.width+7)/8)+3)&~3)]; // gotta pad to DWORDs... 828 829 Color[] colors = fetchPalette(p); 830 831 int pos = 0, pos2 = (h.width/((h.depth == 8) ? 1 : 2)+1)*(h.height-1); 832 bool bits = false; 833 for(int a = 0; a < h.height; a++) { 834 assert(data[pos2++] == 0); 835 for(int b = 0; b < h.width; b++) { 836 if(h.depth == 4) { 837 ufdata[pos/8] |= ((colors[bits? getLowNybble(data[pos2]) : getHighNybble(data[pos2])].a <= 30) << (7-(pos%8))); 838 } else 839 ufdata[pos/8] |= ((colors[data[pos2]].a == 0) << (7-(pos%8))); 840 pos++; 841 if(h.depth == 4) { 842 if(bits) { 843 pos2++; 844 } 845 bits = !bits; 846 } else 847 pos2++; 848 } 849 850 int pad = 0; 851 for(; pad < ((pos/8) % 4); pad++) { 852 ufdata[pos/8] = 0; 853 pos+=8; 854 } 855 if(h.depth == 4) 856 pos2 -= h.width + 2; 857 else 858 pos2-= 2*(h.width) +2; 859 } 860 861 return ufdata; 862 } 863 864 // Done with assumption 865 866 PngHeader getHeader(PNG* p) { 867 PngHeader h; 868 ubyte[] data = p.getChunk("IHDR").payload; 869 870 int pos = 0; 871 872 h.width |= data[pos++] << 24; 873 h.width |= data[pos++] << 16; 874 h.width |= data[pos++] << 8; 875 h.width |= data[pos++] << 0; 876 877 h.height |= data[pos++] << 24; 878 h.height |= data[pos++] << 16; 879 h.height |= data[pos++] << 8; 880 h.height |= data[pos++] << 0; 881 882 h.depth = data[pos++]; 883 h.type = data[pos++]; 884 h.compressionMethod = data[pos++]; 885 h.filterMethod = data[pos++]; 886 h.interlaceMethod = data[pos++]; 887 888 return h; 889 } 890 891 /* 892 struct Color { 893 ubyte r; 894 ubyte g; 895 ubyte b; 896 ubyte a; 897 } 898 */ 899 900 /+ 901 class Image { 902 Color[][] trueColorData; 903 ubyte[] indexData; 904 905 Color[] palette; 906 907 uint width; 908 uint height; 909 910 this(uint w, uint h) {} 911 } 912 913 Image fromPNG(PNG* p) { 914 915 } 916 917 PNG* toPNG(Image i) { 918 919 } 920 +/ struct RGBQUAD { 921 ubyte rgbBlue; 922 ubyte rgbGreen; 923 ubyte rgbRed; 924 ubyte rgbReserved; 925 } 926 927 RGBQUAD[] fetchPaletteWin32(PNG* p) { 928 RGBQUAD[] colors; 929 930 auto palette = p.getChunk("PLTE"); 931 932 colors.length = (palette.size) / 3; 933 934 for(int i = 0; i < colors.length; i++) { 935 colors[i].rgbRed = palette.payload[i*3+0]; 936 colors[i].rgbGreen = palette.payload[i*3+1]; 937 colors[i].rgbBlue = palette.payload[i*3+2]; 938 colors[i].rgbReserved = 0; 939 } 940 941 return colors; 942 943 } 944 945 Color[] fetchPalette(PNG* p) { 946 Color[] colors; 947 948 auto header = getHeader(p); 949 if(header.type == 0) { // greyscale 950 colors.length = 256; 951 foreach(i; 0..256) 952 colors[i] = Color(cast(ubyte) i, cast(ubyte) i, cast(ubyte) i); 953 return colors; 954 } 955 956 // assuming this is indexed 957 assert(header.type == 3); 958 959 auto palette = p.getChunk("PLTE"); 960 961 Chunk* alpha = p.getChunkNullable("tRNS"); 962 963 colors.length = palette.size / 3; 964 965 for(int i = 0; i < colors.length; i++) { 966 colors[i].r = palette.payload[i*3+0]; 967 colors[i].g = palette.payload[i*3+1]; 968 colors[i].b = palette.payload[i*3+2]; 969 if(alpha !is null && i < alpha.size) 970 colors[i].a = alpha.payload[i]; 971 else 972 colors[i].a = 255; 973 974 //writefln("%2d: %3d %3d %3d %3d", i, colors[i].r, colors[i].g, colors[i].b, colors[i].a); 975 } 976 977 return colors; 978 } 979 980 void replacePalette(PNG* p, Color[] colors) { 981 auto palette = p.getChunk("PLTE"); 982 auto alpha = p.getChunkNullable("tRNS"); 983 984 //import std.string; 985 //assert(0, format("%s %s", colors.length, alpha.size)); 986 //assert(colors.length == alpha.size); 987 if(alpha) { 988 alpha.size = cast(int) colors.length; 989 alpha.payload.length = colors.length; // we make sure there's room for our simple method below 990 } 991 p.length = 0; // so write will recalculate 992 993 for(int i = 0; i < colors.length; i++) { 994 palette.payload[i*3+0] = colors[i].r; 995 palette.payload[i*3+1] = colors[i].g; 996 palette.payload[i*3+2] = colors[i].b; 997 if(alpha) 998 alpha.payload[i] = colors[i].a; 999 } 1000 1001 palette.checksum = crc("PLTE", palette.payload); 1002 if(alpha) 1003 alpha.checksum = crc("tRNS", alpha.payload); 1004 } 1005 1006 uint update_crc(in uint crc, in ubyte[] buf){ 1007 static const uint[256] crc_table = [0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117]; 1008 1009 uint c = crc; 1010 1011 foreach(b; buf) 1012 c = crc_table[(c ^ b) & 0xff] ^ (c >> 8); 1013 1014 return c; 1015 } 1016 1017 // lol is just the chunk name 1018 uint crc(in string lol, in ubyte[] buf){ 1019 uint c = update_crc(0xffffffffL, cast(ubyte[]) lol); 1020 return update_crc(c, buf) ^ 0xffffffffL; 1021 } 1022 1023 1024 /* former module arsd.lazypng follows */ 1025 1026 // this is like png.d but all range based so more complicated... 1027 // and I don't remember how to actually use it. 1028 1029 // some day I'll prolly merge it with png.d but for now just throwing it up there 1030 1031 //module arsd.lazypng; 1032 1033 //import arsd.color; 1034 1035 //import std.stdio; 1036 1037 import std.range; 1038 import std.traits; 1039 import std.exception; 1040 import std.string; 1041 //import std.conv; 1042 1043 /* 1044 struct Color { 1045 ubyte r; 1046 ubyte g; 1047 ubyte b; 1048 ubyte a; 1049 1050 string toString() { 1051 return format("#%2x%2x%2x %2x", r, g, b, a); 1052 } 1053 } 1054 */ 1055 1056 //import arsd.simpledisplay; 1057 1058 struct RgbaScanline { 1059 Color[] pixels; 1060 } 1061 1062 1063 auto convertToGreyscale(ImageLines)(ImageLines lines) 1064 if(isInputRange!ImageLines && is(ElementType!ImageLines == RgbaScanline)) 1065 { 1066 struct GreyscaleLines { 1067 ImageLines lines; 1068 bool isEmpty; 1069 this(ImageLines lines) { 1070 this.lines = lines; 1071 if(!empty()) 1072 popFront(); // prime 1073 } 1074 1075 int length() { 1076 return lines.length; 1077 } 1078 1079 bool empty() { 1080 return isEmpty; 1081 } 1082 1083 RgbaScanline current; 1084 RgbaScanline front() { 1085 return current; 1086 } 1087 1088 void popFront() { 1089 if(lines.empty()) { 1090 isEmpty = true; 1091 return; 1092 } 1093 auto old = lines.front(); 1094 current.pixels.length = old.pixels.length; 1095 foreach(i, c; old.pixels) { 1096 ubyte v = cast(ubyte) ( 1097 cast(int) c.r * 0.30 + 1098 cast(int) c.g * 0.59 + 1099 cast(int) c.b * 0.11); 1100 current.pixels[i] = Color(v, v, v, c.a); 1101 } 1102 lines.popFront; 1103 } 1104 } 1105 1106 return GreyscaleLines(lines); 1107 } 1108 1109 1110 1111 1112 /// Lazily breaks the buffered input range into 1113 /// png chunks, as defined in the PNG spec 1114 /// 1115 /// Note: bufferedInputRange is defined in this file too. 1116 LazyPngChunks!(Range) readPngChunks(Range)(Range r) 1117 if(isBufferedInputRange!(Range) && is(ElementType!(Range) == ubyte[])) 1118 { 1119 // First, we need to check the header 1120 // Then we'll lazily pull the chunks 1121 1122 while(r.front.length < 8) { 1123 enforce(!r.empty(), "This isn't big enough to be a PNG file"); 1124 r.appendToFront(); 1125 } 1126 1127 enforce(r.front[0..8] == PNG_MAGIC_NUMBER, 1128 "The file's magic number doesn't look like PNG"); 1129 1130 r.consumeFromFront(8); 1131 1132 return LazyPngChunks!Range(r); 1133 } 1134 1135 /// Same as above, but takes a regular input range instead of a buffered one. 1136 /// Provided for easier compatibility with standard input ranges 1137 /// (for example, std.stdio.File.byChunk) 1138 auto readPngChunks(Range)(Range r) 1139 if(!isBufferedInputRange!(Range) && isInputRange!(Range)) 1140 { 1141 return readPngChunks(BufferedInputRange!Range(r)); 1142 } 1143 1144 /// Given an input range of bytes, return a lazy PNG file 1145 auto pngFromBytes(Range)(Range r) 1146 if(isInputRange!(Range) && is(ElementType!Range == ubyte[])) 1147 { 1148 auto chunks = readPngChunks(r); 1149 auto file = LazyPngFile!(typeof(chunks))(chunks); 1150 1151 return file; 1152 } 1153 1154 struct LazyPngChunks(T) 1155 if(isBufferedInputRange!(T) && is(ElementType!T == ubyte[])) 1156 { 1157 T bytes; 1158 Chunk current; 1159 1160 this(T range) { 1161 bytes = range; 1162 popFront(); // priming it 1163 } 1164 1165 Chunk front() { 1166 return current; 1167 } 1168 1169 bool empty() { 1170 return (bytes.front.length == 0 && bytes.empty); 1171 } 1172 1173 void popFront() { 1174 enforce(!empty()); 1175 1176 while(bytes.front().length < 4) { 1177 enforce(!bytes.empty, 1178 format("Malformed PNG file - chunk size too short (%s < 4)", 1179 bytes.front().length)); 1180 bytes.appendToFront(); 1181 } 1182 1183 Chunk n; 1184 n.size |= bytes.front()[0] << 24; 1185 n.size |= bytes.front()[1] << 16; 1186 n.size |= bytes.front()[2] << 8; 1187 n.size |= bytes.front()[3] << 0; 1188 1189 bytes.consumeFromFront(4); 1190 1191 while(bytes.front().length < n.size + 8) { 1192 enforce(!bytes.empty, 1193 format("Malformed PNG file - chunk too short (%s < %s)", 1194 bytes.front.length, n.size)); 1195 bytes.appendToFront(); 1196 } 1197 n.type[0 .. 4] = bytes.front()[0 .. 4]; 1198 bytes.consumeFromFront(4); 1199 1200 n.payload.length = n.size; 1201 n.payload[0 .. n.size] = bytes.front()[0 .. n.size]; 1202 bytes.consumeFromFront(n.size); 1203 1204 n.checksum |= bytes.front()[0] << 24; 1205 n.checksum |= bytes.front()[1] << 16; 1206 n.checksum |= bytes.front()[2] << 8; 1207 n.checksum |= bytes.front()[3] << 0; 1208 1209 bytes.consumeFromFront(4); 1210 1211 enforce(n.checksum == crcPng(n.stype, n.payload), "Chunk checksum didn't match"); 1212 1213 current = n; 1214 } 1215 } 1216 1217 /// Lazily reads out basic info from a png (header, palette, image data) 1218 /// It will only allocate memory to read a palette, and only copies on 1219 /// the header and the palette. It ignores everything else. 1220 /// 1221 /// FIXME: it doesn't handle interlaced files. 1222 struct LazyPngFile(LazyPngChunksProvider) 1223 if(isInputRange!(LazyPngChunksProvider) && 1224 is(ElementType!(LazyPngChunksProvider) == Chunk)) 1225 { 1226 LazyPngChunksProvider chunks; 1227 1228 this(LazyPngChunksProvider chunks) { 1229 enforce(!chunks.empty(), "There are no chunks in this png"); 1230 1231 header = PngHeader.fromChunk(chunks.front()); 1232 chunks.popFront(); 1233 1234 // And now, find the datastream so we're primed for lazy 1235 // reading, saving the palette and transparency info, if 1236 // present 1237 1238 chunkLoop: 1239 while(!chunks.empty()) { 1240 auto chunk = chunks.front(); 1241 switch(chunks.front.stype) { 1242 case "PLTE": 1243 // if it is in color, palettes are 1244 // always stored as 8 bit per channel 1245 // RGB triplets Alpha is stored elsewhere. 1246 1247 // FIXME: doesn't do greyscale palettes! 1248 1249 enforce(chunk.size % 3 == 0); 1250 palette.length = chunk.size / 3; 1251 1252 auto offset = 0; 1253 foreach(i; 0 .. palette.length) { 1254 palette[i] = Color( 1255 chunk.payload[offset+0], 1256 chunk.payload[offset+1], 1257 chunk.payload[offset+2], 1258 255); 1259 offset += 3; 1260 } 1261 break; 1262 case "tRNS": 1263 // 8 bit channel in same order as 1264 // palette 1265 1266 if(chunk.size > palette.length) 1267 palette.length = chunk.size; 1268 1269 foreach(i, a; chunk.payload) 1270 palette[i].a = a; 1271 break; 1272 case "IDAT": 1273 // leave the datastream for later 1274 break chunkLoop; 1275 default: 1276 // ignore chunks we don't care about 1277 } 1278 chunks.popFront(); 1279 } 1280 1281 this.chunks = chunks; 1282 enforce(!chunks.empty() && chunks.front().stype == "IDAT", 1283 "Malformed PNG file - no image data is present"); 1284 } 1285 1286 /// Lazily reads and decompresses the image datastream, returning chunkSize bytes of 1287 /// it per front. It does *not* change anything, so the filter byte is still there. 1288 /// 1289 /// If chunkSize == 0, it automatically calculates chunk size to give you data by line. 1290 auto rawDatastreamByChunk(int chunkSize = 0) { 1291 assert(chunks.front().stype == "IDAT"); 1292 1293 if(chunkSize == 0) 1294 chunkSize = bytesPerLine(); 1295 1296 struct DatastreamByChunk(T) { 1297 private import etc.c.zlib; 1298 z_stream* zs; // we have to malloc this too, as dmd can move the struct, and zlib 1.2.10 is intolerant to that 1299 int chunkSize; 1300 int bufpos; 1301 int plpos; // bytes eaten in current chunk payload 1302 T chunks; 1303 bool eoz; 1304 1305 this(int cs, T chunks) { 1306 import core.stdc.stdlib : malloc; 1307 import core.stdc.string : memset; 1308 this.chunkSize = cs; 1309 this.chunks = chunks; 1310 assert(chunkSize > 0); 1311 buffer = (cast(ubyte*)malloc(chunkSize))[0..chunkSize]; 1312 pkbuf = (cast(ubyte*)malloc(32768))[0..32768]; // arbitrary number 1313 zs = cast(z_stream*)malloc(z_stream.sizeof); 1314 memset(zs, 0, z_stream.sizeof); 1315 zs.avail_in = 0; 1316 zs.avail_out = 0; 1317 auto res = inflateInit2(zs, 15); 1318 assert(res == Z_OK); 1319 popFront(); // priming 1320 } 1321 1322 ~this () { 1323 version(arsdpng_debug) { import core.stdc.stdio : printf; printf("destroying lazy PNG reader...\n"); } 1324 import core.stdc.stdlib : free; 1325 if (zs !is null) { inflateEnd(zs); free(zs); } 1326 if (pkbuf.ptr !is null) free(pkbuf.ptr); 1327 if (buffer.ptr !is null) free(buffer.ptr); 1328 } 1329 1330 @disable this (this); // no copies! 1331 1332 ubyte[] front () { return (bufpos > 0 ? buffer[0..bufpos] : null); } 1333 1334 ubyte[] buffer; 1335 ubyte[] pkbuf; // we will keep some packed data here in case payload moves, lol 1336 1337 void popFront () { 1338 bufpos = 0; 1339 while (plpos != plpos.max && bufpos < chunkSize) { 1340 // do we have some bytes in zstream? 1341 if (zs.avail_in > 0) { 1342 // just unpack 1343 zs.next_out = cast(typeof(zs.next_out))(buffer.ptr+bufpos); 1344 int rd = chunkSize-bufpos; 1345 zs.avail_out = rd; 1346 auto err = inflate(zs, Z_SYNC_FLUSH); 1347 if (err != Z_STREAM_END && err != Z_OK) throw new Exception("PNG unpack error"); 1348 if (err == Z_STREAM_END) { 1349 assert(zs.avail_in == 0); 1350 eoz = true; 1351 } 1352 bufpos += rd-zs.avail_out; 1353 continue; 1354 } 1355 // no more zstream bytes; do we have something in current chunk? 1356 if (plpos == plpos.max || plpos >= chunks.front.payload.length) { 1357 // current chunk is complete, do we have more chunks? 1358 if (chunks.front.stype != "IDAT") break; // this chunk is not IDAT, that means that... alas 1359 chunks.popFront(); // remove current IDAT 1360 plpos = 0; 1361 if (chunks.empty || chunks.front.stype != "IDAT") plpos = plpos.max; // special value 1362 continue; 1363 } 1364 if (plpos < chunks.front.payload.length) { 1365 // current chunk is not complete, get some more bytes from it 1366 int rd = cast(int)(chunks.front.payload.length-plpos <= pkbuf.length ? chunks.front.payload.length-plpos : pkbuf.length); 1367 assert(rd > 0); 1368 pkbuf[0..rd] = chunks.front.payload[plpos..plpos+rd]; 1369 plpos += rd; 1370 if (eoz) { 1371 // we did hit end-of-stream, reinit zlib (well, well, i know that we can reset it... meh) 1372 inflateEnd(zs); 1373 zs.avail_in = 0; 1374 zs.avail_out = 0; 1375 auto res = inflateInit2(zs, 15); 1376 assert(res == Z_OK); 1377 eoz = false; 1378 } 1379 // setup read pointer 1380 zs.next_in = cast(typeof(zs.next_in))pkbuf.ptr; 1381 zs.avail_in = cast(uint)rd; 1382 continue; 1383 } 1384 assert(0, "wtf?! we should not be here!"); 1385 } 1386 } 1387 1388 bool empty () { return (bufpos == 0); } 1389 } 1390 1391 return DatastreamByChunk!(typeof(chunks))(chunkSize, chunks); 1392 } 1393 1394 // FIXME: no longer compiles 1395 version(none) 1396 auto byRgbaScanline() { 1397 static struct ByRgbaScanline { 1398 ReturnType!(rawDatastreamByChunk) datastream; 1399 RgbaScanline current; 1400 PngHeader header; 1401 int bpp; 1402 Color[] palette; 1403 1404 bool isEmpty = false; 1405 1406 bool empty() { 1407 return isEmpty; 1408 } 1409 1410 @property int length() { 1411 return header.height; 1412 } 1413 1414 // This is needed for the filter algorithms 1415 immutable(ubyte)[] previousLine; 1416 1417 // FIXME: I think my range logic got screwed somewhere 1418 // in the stack... this is messed up. 1419 void popFront() { 1420 assert(!empty()); 1421 if(datastream.empty()) { 1422 isEmpty = true; 1423 return; 1424 } 1425 current.pixels.length = header.width; 1426 1427 // ensure it is primed 1428 if(datastream.front.length == 0) 1429 datastream.popFront; 1430 1431 auto rawData = datastream.front(); 1432 auto filter = rawData[0]; 1433 auto data = unfilter(filter, rawData[1 .. $], previousLine, bpp); 1434 1435 if(data.length == 0) { 1436 isEmpty = true; 1437 return; 1438 } 1439 1440 assert(data.length); 1441 1442 previousLine = data; 1443 1444 // FIXME: if it's rgba, this could probably be faster 1445 assert(header.depth == 8, 1446 "Sorry, depths other than 8 aren't implemented yet."); 1447 1448 auto offset = 0; 1449 foreach(i; 0 .. header.width) { 1450 switch(header.type) { 1451 case 0: // greyscale 1452 case 4: // grey with alpha 1453 auto value = data[offset++]; 1454 current.pixels[i] = Color( 1455 value, 1456 value, 1457 value, 1458 (header.type == 4) 1459 ? data[offset++] : 255 1460 ); 1461 break; 1462 case 3: // indexed 1463 current.pixels[i] = palette[data[offset++]]; 1464 break; 1465 case 2: // truecolor 1466 case 6: // true with alpha 1467 current.pixels[i] = Color( 1468 data[offset++], 1469 data[offset++], 1470 data[offset++], 1471 (header.type == 6) 1472 ? data[offset++] : 255 1473 ); 1474 break; 1475 default: 1476 throw new Exception("invalid png file"); 1477 } 1478 } 1479 1480 assert(offset == data.length); 1481 if(!datastream.empty()) 1482 datastream.popFront(); 1483 } 1484 1485 RgbaScanline front() { 1486 return current; 1487 } 1488 } 1489 1490 assert(chunks.front.stype == "IDAT"); 1491 1492 ByRgbaScanline range; 1493 range.header = header; 1494 range.bpp = bytesPerPixel; 1495 range.palette = palette; 1496 range.datastream = rawDatastreamByChunk(bytesPerLine()); 1497 range.popFront(); 1498 1499 return range; 1500 } 1501 1502 int bytesPerPixel() { 1503 return .bytesPerPixel(header); 1504 } 1505 1506 int bytesPerLine() { 1507 return .bytesPerLineOfPng(header.depth, header.type, header.width); 1508 } 1509 1510 PngHeader header; 1511 Color[] palette; 1512 } 1513 1514 // FIXME: doesn't handle interlacing... I think 1515 // note it returns the length including the filter byte!! 1516 @nogc @safe pure nothrow 1517 int bytesPerLineOfPng(ubyte depth, ubyte type, uint width) { 1518 immutable bitsPerChannel = depth; 1519 1520 int bitsPerPixel = bitsPerChannel; 1521 if(type & 2 && !(type & 1)) // in color, but no palette 1522 bitsPerPixel *= 3; 1523 if(type & 4) // has alpha channel 1524 bitsPerPixel += bitsPerChannel; 1525 1526 immutable int sizeInBits = width * bitsPerPixel; 1527 1528 // need to round up to the nearest byte 1529 int sizeInBytes = (sizeInBits + 7) / 8; 1530 1531 return sizeInBytes + 1; // the +1 is for the filter byte that precedes all lines 1532 } 1533 1534 /************************************************** 1535 * Buffered input range - generic, non-image code 1536 ***************************************************/ 1537 1538 /// Is the given range a buffered input range? That is, an input range 1539 /// that also provides consumeFromFront(int) and appendToFront() 1540 template isBufferedInputRange(R) { 1541 enum bool isBufferedInputRange = 1542 isInputRange!(R) && is(typeof( 1543 { 1544 R r; 1545 r.consumeFromFront(0); 1546 r.appendToFront(); 1547 }())); 1548 } 1549 1550 /// Allows appending to front on a regular input range, if that range is 1551 /// an array. It appends to the array rather than creating an array of 1552 /// arrays; it's meant to make the illusion of one continuous front rather 1553 /// than simply adding capability to walk backward to an existing input range. 1554 /// 1555 /// I think something like this should be standard; I find File.byChunk 1556 /// to be almost useless without this capability. 1557 1558 // FIXME: what if Range is actually an array itself? We should just use 1559 // slices right into it... I guess maybe r.front() would be the whole 1560 // thing in that case though, so we would indeed be slicing in right now. 1561 // Gotta check it though. 1562 struct BufferedInputRange(Range) 1563 if(isInputRange!(Range) && isArray!(ElementType!(Range))) 1564 { 1565 private Range underlyingRange; 1566 private ElementType!(Range) buffer; 1567 1568 /// Creates a buffer for the given range. You probably shouldn't 1569 /// keep using the underlying range directly. 1570 /// 1571 /// It assumes the underlying range has already been primed. 1572 this(Range r) { 1573 underlyingRange = r; 1574 // Is this really correct? Want to make sure r.front 1575 // is valid but it doesn't necessarily need to have 1576 // more elements... 1577 enforce(!r.empty()); 1578 1579 buffer = r.front(); 1580 usingUnderlyingBuffer = true; 1581 } 1582 1583 /// Forwards to the underlying range's empty function 1584 bool empty() { 1585 return underlyingRange.empty(); 1586 } 1587 1588 /// Returns the current buffer 1589 ElementType!(Range) front() { 1590 return buffer; 1591 } 1592 1593 // actually, not terribly useful IMO. appendToFront calls it 1594 // implicitly when necessary 1595 1596 /// Discard the current buffer and get the next item off the 1597 /// underlying range. Be sure to call at least once to prime 1598 /// the range (after checking if it is empty, of course) 1599 void popFront() { 1600 enforce(!empty()); 1601 underlyingRange.popFront(); 1602 buffer = underlyingRange.front(); 1603 usingUnderlyingBuffer = true; 1604 } 1605 1606 bool usingUnderlyingBuffer = false; 1607 1608 /// Remove the first count items from the buffer 1609 void consumeFromFront(int count) { 1610 buffer = buffer[count .. $]; 1611 } 1612 1613 /// Append the next item available on the underlying range to 1614 /// our buffer. 1615 void appendToFront() { 1616 if(buffer.length == 0) { 1617 // may let us reuse the underlying range's buffer, 1618 // hopefully avoiding an extra allocation 1619 popFront(); 1620 } else { 1621 enforce(!underlyingRange.empty()); 1622 1623 // need to make sure underlyingRange.popFront doesn't overwrite any 1624 // of our buffer... 1625 if(usingUnderlyingBuffer) { 1626 buffer = buffer.dup; 1627 usingUnderlyingBuffer = false; 1628 } 1629 1630 underlyingRange.popFront(); 1631 1632 buffer ~= underlyingRange.front(); 1633 } 1634 } 1635 } 1636 1637 /************************************************** 1638 * Lower level implementations of image formats. 1639 * and associated helper functions. 1640 * 1641 * Related to the module, but not particularly 1642 * interesting, so it's at the bottom. 1643 ***************************************************/ 1644 1645 1646 /* PNG file format implementation */ 1647 1648 //import std.zlib; 1649 import std.math; 1650 1651 /// All PNG files are supposed to open with these bytes according to the spec 1652 enum immutable(ubyte[]) PNG_MAGIC_NUMBER = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 1653 1654 /// A PNG file consists of the magic number then a stream of chunks. This 1655 /// struct represents those chunks. 1656 struct Chunk { 1657 uint size; 1658 ubyte[4] type; 1659 ubyte[] payload; 1660 uint checksum; 1661 1662 /// returns the type as a string for easier comparison with literals 1663 const(char)[] stype() return const { 1664 return cast(const(char)[]) type; 1665 } 1666 1667 static Chunk* create(string type, ubyte[] payload) 1668 in { 1669 assert(type.length == 4); 1670 } 1671 body { 1672 Chunk* c = new Chunk; 1673 c.size = cast(int) payload.length; 1674 c.type[] = (cast(ubyte[]) type)[]; 1675 c.payload = payload; 1676 1677 c.checksum = crcPng(type, payload); 1678 1679 return c; 1680 } 1681 1682 /// Puts it into the format for outputting to a file 1683 ubyte[] toArray() { 1684 ubyte[] a; 1685 a.length = size + 12; 1686 1687 int pos = 0; 1688 1689 a[pos++] = (size & 0xff000000) >> 24; 1690 a[pos++] = (size & 0x00ff0000) >> 16; 1691 a[pos++] = (size & 0x0000ff00) >> 8; 1692 a[pos++] = (size & 0x000000ff) >> 0; 1693 1694 a[pos .. pos + 4] = type[0 .. 4]; 1695 pos += 4; 1696 1697 a[pos .. pos + size] = payload[0 .. size]; 1698 1699 pos += size; 1700 1701 assert(checksum); 1702 1703 a[pos++] = (checksum & 0xff000000) >> 24; 1704 a[pos++] = (checksum & 0x00ff0000) >> 16; 1705 a[pos++] = (checksum & 0x0000ff00) >> 8; 1706 a[pos++] = (checksum & 0x000000ff) >> 0; 1707 1708 return a; 1709 } 1710 } 1711 1712 /// The first chunk in a PNG file is a header that contains this info 1713 struct PngHeader { 1714 /// Width of the image, in pixels. 1715 uint width; 1716 1717 /// Height of the image, in pixels. 1718 uint height; 1719 1720 /** 1721 This is bits per channel - per color for truecolor or grey 1722 and per pixel for palette. 1723 1724 Indexed ones can have depth of 1,2,4, or 8, 1725 1726 Greyscale can be 1,2,4,8,16 1727 1728 Everything else must be 8 or 16. 1729 */ 1730 ubyte depth = 8; 1731 1732 /** Types from the PNG spec: 1733 0 - greyscale 1734 2 - truecolor 1735 3 - indexed color 1736 4 - grey with alpha 1737 6 - true with alpha 1738 1739 1, 5, and 7 are invalid. 1740 1741 There's a kind of bitmask going on here: 1742 If type&1, it has a palette. 1743 If type&2, it is in color. 1744 If type&4, it has an alpha channel in the datastream. 1745 */ 1746 ubyte type = 6; 1747 1748 ubyte compressionMethod = 0; /// should be zero 1749 ubyte filterMethod = 0; /// should be zero 1750 /// 0 is non interlaced, 1 if Adam7. No more are defined in the spec 1751 ubyte interlaceMethod = 0; 1752 1753 static PngHeader fromChunk(in Chunk c) { 1754 enforce(c.stype == "IHDR", 1755 "The chunk is not an image header"); 1756 1757 PngHeader h; 1758 auto data = c.payload; 1759 int pos = 0; 1760 1761 enforce(data.length == 13, 1762 "Malformed PNG file - the IHDR is the wrong size"); 1763 1764 h.width |= data[pos++] << 24; 1765 h.width |= data[pos++] << 16; 1766 h.width |= data[pos++] << 8; 1767 h.width |= data[pos++] << 0; 1768 1769 h.height |= data[pos++] << 24; 1770 h.height |= data[pos++] << 16; 1771 h.height |= data[pos++] << 8; 1772 h.height |= data[pos++] << 0; 1773 1774 h.depth = data[pos++]; 1775 h.type = data[pos++]; 1776 h.compressionMethod = data[pos++]; 1777 h.filterMethod = data[pos++]; 1778 h.interlaceMethod = data[pos++]; 1779 1780 return h; 1781 } 1782 1783 Chunk* toChunk() { 1784 ubyte[] data; 1785 data.length = 13; 1786 int pos = 0; 1787 1788 data[pos++] = width >> 24; 1789 data[pos++] = (width >> 16) & 0xff; 1790 data[pos++] = (width >> 8) & 0xff; 1791 data[pos++] = width & 0xff; 1792 1793 data[pos++] = height >> 24; 1794 data[pos++] = (height >> 16) & 0xff; 1795 data[pos++] = (height >> 8) & 0xff; 1796 data[pos++] = height & 0xff; 1797 1798 data[pos++] = depth; 1799 data[pos++] = type; 1800 data[pos++] = compressionMethod; 1801 data[pos++] = filterMethod; 1802 data[pos++] = interlaceMethod; 1803 1804 assert(pos == 13); 1805 1806 return Chunk.create("IHDR", data); 1807 } 1808 } 1809 1810 void writePngLazy(OutputRange, InputRange)(ref OutputRange where, InputRange image) 1811 if( 1812 isOutputRange!(OutputRange, ubyte[]) && 1813 isInputRange!(InputRange) && 1814 is(ElementType!InputRange == RgbaScanline)) 1815 { 1816 import std.zlib; 1817 where.put(PNG_MAGIC_NUMBER); 1818 PngHeader header; 1819 1820 assert(!image.empty()); 1821 1822 // using the default values for header here... FIXME not super clear 1823 1824 header.width = image.front.pixels.length; 1825 header.height = image.length; 1826 1827 enforce(header.width > 0, "Image width <= 0"); 1828 enforce(header.height > 0, "Image height <= 0"); 1829 1830 where.put(header.toChunk().toArray()); 1831 1832 auto compressor = new std.zlib.Compress(); 1833 const(void)[] compressedData; 1834 int cnt; 1835 foreach(line; image) { 1836 // YOU'VE GOT TO BE FUCKING KIDDING ME! 1837 // I have to /cast/ to void[]!??!? 1838 1839 ubyte[] data; 1840 data.length = 1 + header.width * 4; 1841 data[0] = 0; // filter type 1842 int offset = 1; 1843 foreach(pixel; line.pixels) { 1844 data[offset++] = pixel.r; 1845 data[offset++] = pixel.g; 1846 data[offset++] = pixel.b; 1847 data[offset++] = pixel.a; 1848 } 1849 1850 compressedData ~= compressor.compress(cast(void[]) 1851 data); 1852 if(compressedData.length > 2_000) { 1853 where.put(Chunk.create("IDAT", cast(ubyte[]) 1854 compressedData).toArray()); 1855 compressedData = null; 1856 } 1857 1858 cnt++; 1859 } 1860 1861 assert(cnt == header.height, format("Got %d lines instead of %d", cnt, header.height)); 1862 1863 compressedData ~= compressor.flush(); 1864 if(compressedData.length) 1865 where.put(Chunk.create("IDAT", cast(ubyte[]) 1866 compressedData).toArray()); 1867 1868 where.put(Chunk.create("IEND", null).toArray()); 1869 } 1870 1871 // bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey 1872 1873 uint crcPng(in string chunkName, in ubyte[] buf){ 1874 uint c = update_crc(0xffffffffL, cast(ubyte[]) chunkName); 1875 return update_crc(c, buf) ^ 0xffffffffL; 1876 } 1877 1878 immutable(ubyte)[] unfilter(ubyte filterType, in ubyte[] data, in ubyte[] previousLine, int bpp) { 1879 // Note: the overflow arithmetic on the ubytes in here is intentional 1880 switch(filterType) { 1881 case 0: 1882 return data.idup; // FIXME is copying really necessary? 1883 case 1: 1884 auto arr = data.dup; 1885 // first byte gets zero added to it so nothing special 1886 foreach(i; bpp .. arr.length) { 1887 arr[i] += arr[i - bpp]; 1888 } 1889 1890 return assumeUnique(arr); 1891 case 2: 1892 auto arr = data.dup; 1893 if(previousLine.length) 1894 foreach(i; 0 .. arr.length) { 1895 arr[i] += previousLine[i]; 1896 } 1897 1898 return assumeUnique(arr); 1899 case 3: 1900 auto arr = data.dup; 1901 if(previousLine.length) 1902 foreach(i; 0 .. arr.length) { 1903 auto prev = i < bpp ? 0 : arr[i - bpp]; 1904 arr[i] += cast(ubyte) 1905 /*std.math.floor*/( cast(int) (prev + previousLine[i]) / 2); 1906 } 1907 1908 return assumeUnique(arr); 1909 case 4: 1910 auto arr = data.dup; 1911 foreach(i; 0 .. arr.length) { 1912 ubyte prev = i < bpp ? 0 : arr[i - bpp]; 1913 ubyte prevLL = i < bpp ? 0 : (i < previousLine.length ? previousLine[i - bpp] : 0); 1914 1915 arr[i] += PaethPredictor(prev, (i < previousLine.length ? previousLine[i] : 0), prevLL); 1916 } 1917 1918 return assumeUnique(arr); 1919 default: 1920 throw new Exception("invalid PNG file, bad filter type"); 1921 } 1922 } 1923 1924 ubyte PaethPredictor(ubyte a, ubyte b, ubyte c) { 1925 int p = cast(int) a + b - c; 1926 auto pa = abs(p - a); 1927 auto pb = abs(p - b); 1928 auto pc = abs(p - c); 1929 1930 if(pa <= pb && pa <= pc) 1931 return a; 1932 if(pb <= pc) 1933 return b; 1934 return c; 1935 } 1936 1937 int bytesPerPixel(PngHeader header) { 1938 immutable bitsPerChannel = header.depth; 1939 1940 int bitsPerPixel = bitsPerChannel; 1941 if(header.type & 2 && !(header.type & 1)) // in color, but no palette 1942 bitsPerPixel *= 3; 1943 if(header.type & 4) // has alpha channel 1944 bitsPerPixel += bitsPerChannel; 1945 1946 return (bitsPerPixel + 7) / 8; 1947 }