1 /// PNG file handling for color.d's Image interfaces 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 size_t bytesPerLine; 689 switch(h.type) { 690 case 0: 691 // FIXME: < 8 depth not supported here but should be 692 bytesPerLine = cast(size_t)h.width * 1 * h.depth / 8; 693 break; 694 case 2: 695 bytesPerLine = cast(size_t)h.width * 3 * h.depth / 8; 696 break; 697 case 3: 698 bytesPerLine = cast(size_t)h.width * 1 * h.depth / 8; 699 break; 700 case 4: 701 // FIXME: < 8 depth not supported here but should be 702 bytesPerLine = cast(size_t)h.width * 2 * h.depth / 8; 703 break; 704 case 6: 705 bytesPerLine = cast(size_t)h.width * 4 * h.depth / 8; 706 break; 707 default: assert(0); 708 709 } 710 Chunk dat; 711 dat.type = ['I', 'D', 'A', 'T']; 712 size_t pos = 0; 713 714 const(ubyte)[] output; 715 while(pos+bytesPerLine <= data.length) { 716 output ~= 0; 717 output ~= data[pos..pos+bytesPerLine]; 718 pos += bytesPerLine; 719 } 720 721 auto com = cast(ubyte[]) compress(output); 722 dat.size = cast(int) com.length; 723 dat.payload = com; 724 dat.checksum = crc("IDAT", dat.payload); 725 726 png.chunks ~= dat; 727 728 Chunk c; 729 730 c.size = 0; 731 c.type = ['I', 'E', 'N', 'D']; 732 c.checksum = crc("IEND", c.payload); 733 734 png.chunks ~= c; 735 736 } 737 738 deprecated alias PngHeader PNGHeader; 739 740 // bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey 741 742 ubyte[] getDatastream(PNG* p) { 743 import std.zlib; 744 ubyte[] compressed; 745 746 foreach(c; p.chunks) { 747 if(c.stype != "IDAT") 748 continue; 749 compressed ~= c.payload; 750 } 751 752 return cast(ubyte[]) uncompress(compressed); 753 } 754 755 // FIXME: Assuming 8 bits per pixel 756 ubyte[] getUnfilteredDatastream(PNG* p) { 757 PngHeader h = getHeader(p); 758 assert(h.filterMethod == 0); 759 760 assert(h.type == 3); // FIXME 761 assert(h.depth == 8); // FIXME 762 763 ubyte[] data = getDatastream(p); 764 ubyte[] ufdata = new ubyte[data.length - h.height]; 765 766 int bytesPerLine = cast(int) ufdata.length / h.height; 767 768 int pos = 0, pos2 = 0; 769 for(int a = 0; a < h.height; a++) { 770 assert(data[pos2] == 0); 771 ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1]; 772 pos+= bytesPerLine; 773 pos2+= bytesPerLine + 1; 774 } 775 776 return ufdata; 777 } 778 779 ubyte[] getFlippedUnfilteredDatastream(PNG* p) { 780 PngHeader h = getHeader(p); 781 assert(h.filterMethod == 0); 782 783 assert(h.type == 3); // FIXME 784 assert(h.depth == 8 || h.depth == 4); // FIXME 785 786 ubyte[] data = getDatastream(p); 787 ubyte[] ufdata = new ubyte[data.length - h.height]; 788 789 int bytesPerLine = cast(int) ufdata.length / h.height; 790 791 792 int pos = cast(int) ufdata.length - bytesPerLine, pos2 = 0; 793 for(int a = 0; a < h.height; a++) { 794 assert(data[pos2] == 0); 795 ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1]; 796 pos-= bytesPerLine; 797 pos2+= bytesPerLine + 1; 798 } 799 800 return ufdata; 801 } 802 803 ubyte getHighNybble(ubyte a) { 804 return cast(ubyte)(a >> 4); // FIXME 805 } 806 807 ubyte getLowNybble(ubyte a) { 808 return a & 0x0f; 809 } 810 811 // Takes the transparency info and returns 812 ubyte[] getANDMask(PNG* p) { 813 PngHeader h = getHeader(p); 814 assert(h.filterMethod == 0); 815 816 assert(h.type == 3); // FIXME 817 assert(h.depth == 8 || h.depth == 4); // FIXME 818 819 assert(h.width % 8 == 0); // might actually be %2 820 821 ubyte[] data = getDatastream(p); 822 ubyte[] ufdata = new ubyte[h.height*((((h.width+7)/8)+3)&~3)]; // gotta pad to DWORDs... 823 824 Color[] colors = fetchPalette(p); 825 826 int pos = 0, pos2 = (h.width/((h.depth == 8) ? 1 : 2)+1)*(h.height-1); 827 bool bits = false; 828 for(int a = 0; a < h.height; a++) { 829 assert(data[pos2++] == 0); 830 for(int b = 0; b < h.width; b++) { 831 if(h.depth == 4) { 832 ufdata[pos/8] |= ((colors[bits? getLowNybble(data[pos2]) : getHighNybble(data[pos2])].a <= 30) << (7-(pos%8))); 833 } else 834 ufdata[pos/8] |= ((colors[data[pos2]].a == 0) << (7-(pos%8))); 835 pos++; 836 if(h.depth == 4) { 837 if(bits) { 838 pos2++; 839 } 840 bits = !bits; 841 } else 842 pos2++; 843 } 844 845 int pad = 0; 846 for(; pad < ((pos/8) % 4); pad++) { 847 ufdata[pos/8] = 0; 848 pos+=8; 849 } 850 if(h.depth == 4) 851 pos2 -= h.width + 2; 852 else 853 pos2-= 2*(h.width) +2; 854 } 855 856 return ufdata; 857 } 858 859 // Done with assumption 860 861 PngHeader getHeader(PNG* p) { 862 PngHeader h; 863 ubyte[] data = p.getChunk("IHDR").payload; 864 865 int pos = 0; 866 867 h.width |= data[pos++] << 24; 868 h.width |= data[pos++] << 16; 869 h.width |= data[pos++] << 8; 870 h.width |= data[pos++] << 0; 871 872 h.height |= data[pos++] << 24; 873 h.height |= data[pos++] << 16; 874 h.height |= data[pos++] << 8; 875 h.height |= data[pos++] << 0; 876 877 h.depth = data[pos++]; 878 h.type = data[pos++]; 879 h.compressionMethod = data[pos++]; 880 h.filterMethod = data[pos++]; 881 h.interlaceMethod = data[pos++]; 882 883 return h; 884 } 885 886 /* 887 struct Color { 888 ubyte r; 889 ubyte g; 890 ubyte b; 891 ubyte a; 892 } 893 */ 894 895 /+ 896 class Image { 897 Color[][] trueColorData; 898 ubyte[] indexData; 899 900 Color[] palette; 901 902 uint width; 903 uint height; 904 905 this(uint w, uint h) {} 906 } 907 908 Image fromPNG(PNG* p) { 909 910 } 911 912 PNG* toPNG(Image i) { 913 914 } 915 +/ struct RGBQUAD { 916 ubyte rgbBlue; 917 ubyte rgbGreen; 918 ubyte rgbRed; 919 ubyte rgbReserved; 920 } 921 922 RGBQUAD[] fetchPaletteWin32(PNG* p) { 923 RGBQUAD[] colors; 924 925 auto palette = p.getChunk("PLTE"); 926 927 colors.length = (palette.size) / 3; 928 929 for(int i = 0; i < colors.length; i++) { 930 colors[i].rgbRed = palette.payload[i*3+0]; 931 colors[i].rgbGreen = palette.payload[i*3+1]; 932 colors[i].rgbBlue = palette.payload[i*3+2]; 933 colors[i].rgbReserved = 0; 934 } 935 936 return colors; 937 938 } 939 940 Color[] fetchPalette(PNG* p) { 941 Color[] colors; 942 943 auto header = getHeader(p); 944 if(header.type == 0) { // greyscale 945 colors.length = 256; 946 foreach(i; 0..256) 947 colors[i] = Color(cast(ubyte) i, cast(ubyte) i, cast(ubyte) i); 948 return colors; 949 } 950 951 // assuming this is indexed 952 assert(header.type == 3); 953 954 auto palette = p.getChunk("PLTE"); 955 956 Chunk* alpha = p.getChunkNullable("tRNS"); 957 958 colors.length = palette.size / 3; 959 960 for(int i = 0; i < colors.length; i++) { 961 colors[i].r = palette.payload[i*3+0]; 962 colors[i].g = palette.payload[i*3+1]; 963 colors[i].b = palette.payload[i*3+2]; 964 if(alpha !is null && i < alpha.size) 965 colors[i].a = alpha.payload[i]; 966 else 967 colors[i].a = 255; 968 969 //writefln("%2d: %3d %3d %3d %3d", i, colors[i].r, colors[i].g, colors[i].b, colors[i].a); 970 } 971 972 return colors; 973 } 974 975 void replacePalette(PNG* p, Color[] colors) { 976 auto palette = p.getChunk("PLTE"); 977 auto alpha = p.getChunkNullable("tRNS"); 978 979 //import std.string; 980 //assert(0, format("%s %s", colors.length, alpha.size)); 981 //assert(colors.length == alpha.size); 982 if(alpha) { 983 alpha.size = cast(int) colors.length; 984 alpha.payload.length = colors.length; // we make sure there's room for our simple method below 985 } 986 p.length = 0; // so write will recalculate 987 988 for(int i = 0; i < colors.length; i++) { 989 palette.payload[i*3+0] = colors[i].r; 990 palette.payload[i*3+1] = colors[i].g; 991 palette.payload[i*3+2] = colors[i].b; 992 if(alpha) 993 alpha.payload[i] = colors[i].a; 994 } 995 996 palette.checksum = crc("PLTE", palette.payload); 997 if(alpha) 998 alpha.checksum = crc("tRNS", alpha.payload); 999 } 1000 1001 uint update_crc(in uint crc, in ubyte[] buf){ 1002 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]; 1003 1004 uint c = crc; 1005 1006 foreach(b; buf) 1007 c = crc_table[(c ^ b) & 0xff] ^ (c >> 8); 1008 1009 return c; 1010 } 1011 1012 // lol is just the chunk name 1013 uint crc(in string lol, in ubyte[] buf){ 1014 uint c = update_crc(0xffffffffL, cast(ubyte[]) lol); 1015 return update_crc(c, buf) ^ 0xffffffffL; 1016 } 1017 1018 1019 /* former module arsd.lazypng follows */ 1020 1021 // this is like png.d but all range based so more complicated... 1022 // and I don't remember how to actually use it. 1023 1024 // some day I'll prolly merge it with png.d but for now just throwing it up there 1025 1026 //module arsd.lazypng; 1027 1028 //import arsd.color; 1029 1030 //import std.stdio; 1031 1032 import std.range; 1033 import std.traits; 1034 import std.exception; 1035 import std.string; 1036 //import std.conv; 1037 1038 /* 1039 struct Color { 1040 ubyte r; 1041 ubyte g; 1042 ubyte b; 1043 ubyte a; 1044 1045 string toString() { 1046 return format("#%2x%2x%2x %2x", r, g, b, a); 1047 } 1048 } 1049 */ 1050 1051 //import arsd.simpledisplay; 1052 1053 struct RgbaScanline { 1054 Color[] pixels; 1055 } 1056 1057 1058 auto convertToGreyscale(ImageLines)(ImageLines lines) 1059 if(isInputRange!ImageLines && is(ElementType!ImageLines == RgbaScanline)) 1060 { 1061 struct GreyscaleLines { 1062 ImageLines lines; 1063 bool isEmpty; 1064 this(ImageLines lines) { 1065 this.lines = lines; 1066 if(!empty()) 1067 popFront(); // prime 1068 } 1069 1070 int length() { 1071 return lines.length; 1072 } 1073 1074 bool empty() { 1075 return isEmpty; 1076 } 1077 1078 RgbaScanline current; 1079 RgbaScanline front() { 1080 return current; 1081 } 1082 1083 void popFront() { 1084 if(lines.empty()) { 1085 isEmpty = true; 1086 return; 1087 } 1088 auto old = lines.front(); 1089 current.pixels.length = old.pixels.length; 1090 foreach(i, c; old.pixels) { 1091 ubyte v = cast(ubyte) ( 1092 cast(int) c.r * 0.30 + 1093 cast(int) c.g * 0.59 + 1094 cast(int) c.b * 0.11); 1095 current.pixels[i] = Color(v, v, v, c.a); 1096 } 1097 lines.popFront; 1098 } 1099 } 1100 1101 return GreyscaleLines(lines); 1102 } 1103 1104 1105 1106 1107 /// Lazily breaks the buffered input range into 1108 /// png chunks, as defined in the PNG spec 1109 /// 1110 /// Note: bufferedInputRange is defined in this file too. 1111 LazyPngChunks!(Range) readPngChunks(Range)(Range r) 1112 if(isBufferedInputRange!(Range) && is(ElementType!(Range) == ubyte[])) 1113 { 1114 // First, we need to check the header 1115 // Then we'll lazily pull the chunks 1116 1117 while(r.front.length < 8) { 1118 enforce(!r.empty(), "This isn't big enough to be a PNG file"); 1119 r.appendToFront(); 1120 } 1121 1122 enforce(r.front[0..8] == PNG_MAGIC_NUMBER, 1123 "The file's magic number doesn't look like PNG"); 1124 1125 r.consumeFromFront(8); 1126 1127 return LazyPngChunks!Range(r); 1128 } 1129 1130 /// Same as above, but takes a regular input range instead of a buffered one. 1131 /// Provided for easier compatibility with standard input ranges 1132 /// (for example, std.stdio.File.byChunk) 1133 auto readPngChunks(Range)(Range r) 1134 if(!isBufferedInputRange!(Range) && isInputRange!(Range)) 1135 { 1136 return readPngChunks(BufferedInputRange!Range(r)); 1137 } 1138 1139 /// Given an input range of bytes, return a lazy PNG file 1140 auto pngFromBytes(Range)(Range r) 1141 if(isInputRange!(Range) && is(ElementType!Range == ubyte[])) 1142 { 1143 auto chunks = readPngChunks(r); 1144 auto file = LazyPngFile!(typeof(chunks))(chunks); 1145 1146 return file; 1147 } 1148 1149 struct LazyPngChunks(T) 1150 if(isBufferedInputRange!(T) && is(ElementType!T == ubyte[])) 1151 { 1152 T bytes; 1153 Chunk current; 1154 1155 this(T range) { 1156 bytes = range; 1157 popFront(); // priming it 1158 } 1159 1160 Chunk front() { 1161 return current; 1162 } 1163 1164 bool empty() { 1165 return (bytes.front.length == 0 && bytes.empty); 1166 } 1167 1168 void popFront() { 1169 enforce(!empty()); 1170 1171 while(bytes.front().length < 4) { 1172 enforce(!bytes.empty, 1173 format("Malformed PNG file - chunk size too short (%s < 4)", 1174 bytes.front().length)); 1175 bytes.appendToFront(); 1176 } 1177 1178 Chunk n; 1179 n.size |= bytes.front()[0] << 24; 1180 n.size |= bytes.front()[1] << 16; 1181 n.size |= bytes.front()[2] << 8; 1182 n.size |= bytes.front()[3] << 0; 1183 1184 bytes.consumeFromFront(4); 1185 1186 while(bytes.front().length < n.size + 8) { 1187 enforce(!bytes.empty, 1188 format("Malformed PNG file - chunk too short (%s < %s)", 1189 bytes.front.length, n.size)); 1190 bytes.appendToFront(); 1191 } 1192 n.type[0 .. 4] = bytes.front()[0 .. 4]; 1193 bytes.consumeFromFront(4); 1194 1195 n.payload.length = n.size; 1196 n.payload[0 .. n.size] = bytes.front()[0 .. n.size]; 1197 bytes.consumeFromFront(n.size); 1198 1199 n.checksum |= bytes.front()[0] << 24; 1200 n.checksum |= bytes.front()[1] << 16; 1201 n.checksum |= bytes.front()[2] << 8; 1202 n.checksum |= bytes.front()[3] << 0; 1203 1204 bytes.consumeFromFront(4); 1205 1206 enforce(n.checksum == crcPng(n.stype, n.payload), "Chunk checksum didn't match"); 1207 1208 current = n; 1209 } 1210 } 1211 1212 /// Lazily reads out basic info from a png (header, palette, image data) 1213 /// It will only allocate memory to read a palette, and only copies on 1214 /// the header and the palette. It ignores everything else. 1215 /// 1216 /// FIXME: it doesn't handle interlaced files. 1217 struct LazyPngFile(LazyPngChunksProvider) 1218 if(isInputRange!(LazyPngChunksProvider) && 1219 is(ElementType!(LazyPngChunksProvider) == Chunk)) 1220 { 1221 LazyPngChunksProvider chunks; 1222 1223 this(LazyPngChunksProvider chunks) { 1224 enforce(!chunks.empty(), "There are no chunks in this png"); 1225 1226 header = PngHeader.fromChunk(chunks.front()); 1227 chunks.popFront(); 1228 1229 // And now, find the datastream so we're primed for lazy 1230 // reading, saving the palette and transparency info, if 1231 // present 1232 1233 chunkLoop: 1234 while(!chunks.empty()) { 1235 auto chunk = chunks.front(); 1236 switch(chunks.front.stype) { 1237 case "PLTE": 1238 // if it is in color, palettes are 1239 // always stored as 8 bit per channel 1240 // RGB triplets Alpha is stored elsewhere. 1241 1242 // FIXME: doesn't do greyscale palettes! 1243 1244 enforce(chunk.size % 3 == 0); 1245 palette.length = chunk.size / 3; 1246 1247 auto offset = 0; 1248 foreach(i; 0 .. palette.length) { 1249 palette[i] = Color( 1250 chunk.payload[offset+0], 1251 chunk.payload[offset+1], 1252 chunk.payload[offset+2], 1253 255); 1254 offset += 3; 1255 } 1256 break; 1257 case "tRNS": 1258 // 8 bit channel in same order as 1259 // palette 1260 1261 if(chunk.size > palette.length) 1262 palette.length = chunk.size; 1263 1264 foreach(i, a; chunk.payload) 1265 palette[i].a = a; 1266 break; 1267 case "IDAT": 1268 // leave the datastream for later 1269 break chunkLoop; 1270 default: 1271 // ignore chunks we don't care about 1272 } 1273 chunks.popFront(); 1274 } 1275 1276 this.chunks = chunks; 1277 enforce(!chunks.empty() && chunks.front().stype == "IDAT", 1278 "Malformed PNG file - no image data is present"); 1279 } 1280 1281 /// Lazily reads and decompresses the image datastream, returning chunkSize bytes of 1282 /// it per front. It does *not* change anything, so the filter byte is still there. 1283 /// 1284 /// If chunkSize == 0, it automatically calculates chunk size to give you data by line. 1285 auto rawDatastreamByChunk(int chunkSize = 0) { 1286 assert(chunks.front().stype == "IDAT"); 1287 1288 if(chunkSize == 0) 1289 chunkSize = bytesPerLine(); 1290 1291 struct DatastreamByChunk(T) { 1292 private import etc.c.zlib; 1293 z_stream* zs; // we have to malloc this too, as dmd can move the struct, and zlib 1.2.10 is intolerant to that 1294 int chunkSize; 1295 int bufpos; 1296 int plpos; // bytes eaten in current chunk payload 1297 T chunks; 1298 bool eoz; 1299 1300 this(int cs, T chunks) { 1301 import core.stdc.stdlib : malloc; 1302 import core.stdc.string : memset; 1303 this.chunkSize = cs; 1304 this.chunks = chunks; 1305 assert(chunkSize > 0); 1306 buffer = (cast(ubyte*)malloc(chunkSize))[0..chunkSize]; 1307 pkbuf = (cast(ubyte*)malloc(32768))[0..32768]; // arbitrary number 1308 zs = cast(z_stream*)malloc(z_stream.sizeof); 1309 memset(zs, 0, z_stream.sizeof); 1310 zs.avail_in = 0; 1311 zs.avail_out = 0; 1312 auto res = inflateInit2(zs, 15); 1313 assert(res == Z_OK); 1314 popFront(); // priming 1315 } 1316 1317 ~this () { 1318 version(arsdpng_debug) { import core.stdc.stdio : printf; printf("destroying lazy PNG reader...\n"); } 1319 import core.stdc.stdlib : free; 1320 if (zs !is null) { inflateEnd(zs); free(zs); } 1321 if (pkbuf.ptr !is null) free(pkbuf.ptr); 1322 if (buffer.ptr !is null) free(buffer.ptr); 1323 } 1324 1325 @disable this (this); // no copies! 1326 1327 ubyte[] front () { return (bufpos > 0 ? buffer[0..bufpos] : null); } 1328 1329 ubyte[] buffer; 1330 ubyte[] pkbuf; // we will keep some packed data here in case payload moves, lol 1331 1332 void popFront () { 1333 bufpos = 0; 1334 while (plpos != plpos.max && bufpos < chunkSize) { 1335 // do we have some bytes in zstream? 1336 if (zs.avail_in > 0) { 1337 // just unpack 1338 zs.next_out = cast(typeof(zs.next_out))(buffer.ptr+bufpos); 1339 int rd = chunkSize-bufpos; 1340 zs.avail_out = rd; 1341 auto err = inflate(zs, Z_SYNC_FLUSH); 1342 if (err != Z_STREAM_END && err != Z_OK) throw new Exception("PNG unpack error"); 1343 if (err == Z_STREAM_END) { 1344 assert(zs.avail_in == 0); 1345 eoz = true; 1346 } 1347 bufpos += rd-zs.avail_out; 1348 continue; 1349 } 1350 // no more zstream bytes; do we have something in current chunk? 1351 if (plpos == plpos.max || plpos >= chunks.front.payload.length) { 1352 // current chunk is complete, do we have more chunks? 1353 if (chunks.front.stype != "IDAT") break; // this chunk is not IDAT, that means that... alas 1354 chunks.popFront(); // remove current IDAT 1355 plpos = 0; 1356 if (chunks.empty || chunks.front.stype != "IDAT") plpos = plpos.max; // special value 1357 continue; 1358 } 1359 if (plpos < chunks.front.payload.length) { 1360 // current chunk is not complete, get some more bytes from it 1361 int rd = cast(int)(chunks.front.payload.length-plpos <= pkbuf.length ? chunks.front.payload.length-plpos : pkbuf.length); 1362 assert(rd > 0); 1363 pkbuf[0..rd] = chunks.front.payload[plpos..plpos+rd]; 1364 plpos += rd; 1365 if (eoz) { 1366 // we did hit end-of-stream, reinit zlib (well, well, i know that we can reset it... meh) 1367 inflateEnd(zs); 1368 zs.avail_in = 0; 1369 zs.avail_out = 0; 1370 auto res = inflateInit2(zs, 15); 1371 assert(res == Z_OK); 1372 eoz = false; 1373 } 1374 // setup read pointer 1375 zs.next_in = cast(typeof(zs.next_in))pkbuf.ptr; 1376 zs.avail_in = cast(uint)rd; 1377 continue; 1378 } 1379 assert(0, "wtf?! we should not be here!"); 1380 } 1381 } 1382 1383 bool empty () { return (bufpos == 0); } 1384 } 1385 1386 return DatastreamByChunk!(typeof(chunks))(chunkSize, chunks); 1387 } 1388 1389 // FIXME: no longer compiles 1390 version(none) 1391 auto byRgbaScanline() { 1392 static struct ByRgbaScanline { 1393 ReturnType!(rawDatastreamByChunk) datastream; 1394 RgbaScanline current; 1395 PngHeader header; 1396 int bpp; 1397 Color[] palette; 1398 1399 bool isEmpty = false; 1400 1401 bool empty() { 1402 return isEmpty; 1403 } 1404 1405 @property int length() { 1406 return header.height; 1407 } 1408 1409 // This is needed for the filter algorithms 1410 immutable(ubyte)[] previousLine; 1411 1412 // FIXME: I think my range logic got screwed somewhere 1413 // in the stack... this is messed up. 1414 void popFront() { 1415 assert(!empty()); 1416 if(datastream.empty()) { 1417 isEmpty = true; 1418 return; 1419 } 1420 current.pixels.length = header.width; 1421 1422 // ensure it is primed 1423 if(datastream.front.length == 0) 1424 datastream.popFront; 1425 1426 auto rawData = datastream.front(); 1427 auto filter = rawData[0]; 1428 auto data = unfilter(filter, rawData[1 .. $], previousLine, bpp); 1429 1430 if(data.length == 0) { 1431 isEmpty = true; 1432 return; 1433 } 1434 1435 assert(data.length); 1436 1437 previousLine = data; 1438 1439 // FIXME: if it's rgba, this could probably be faster 1440 assert(header.depth == 8, 1441 "Sorry, depths other than 8 aren't implemented yet."); 1442 1443 auto offset = 0; 1444 foreach(i; 0 .. header.width) { 1445 switch(header.type) { 1446 case 0: // greyscale 1447 case 4: // grey with alpha 1448 auto value = data[offset++]; 1449 current.pixels[i] = Color( 1450 value, 1451 value, 1452 value, 1453 (header.type == 4) 1454 ? data[offset++] : 255 1455 ); 1456 break; 1457 case 3: // indexed 1458 current.pixels[i] = palette[data[offset++]]; 1459 break; 1460 case 2: // truecolor 1461 case 6: // true with alpha 1462 current.pixels[i] = Color( 1463 data[offset++], 1464 data[offset++], 1465 data[offset++], 1466 (header.type == 6) 1467 ? data[offset++] : 255 1468 ); 1469 break; 1470 default: 1471 throw new Exception("invalid png file"); 1472 } 1473 } 1474 1475 assert(offset == data.length); 1476 if(!datastream.empty()) 1477 datastream.popFront(); 1478 } 1479 1480 RgbaScanline front() { 1481 return current; 1482 } 1483 } 1484 1485 assert(chunks.front.stype == "IDAT"); 1486 1487 ByRgbaScanline range; 1488 range.header = header; 1489 range.bpp = bytesPerPixel; 1490 range.palette = palette; 1491 range.datastream = rawDatastreamByChunk(bytesPerLine()); 1492 range.popFront(); 1493 1494 return range; 1495 } 1496 1497 int bytesPerPixel() { 1498 return .bytesPerPixel(header); 1499 } 1500 1501 int bytesPerLine() { 1502 return .bytesPerLineOfPng(header.depth, header.type, header.width); 1503 } 1504 1505 PngHeader header; 1506 Color[] palette; 1507 } 1508 1509 // FIXME: doesn't handle interlacing... I think 1510 // note it returns the length including the filter byte!! 1511 @nogc @safe pure nothrow 1512 int bytesPerLineOfPng(ubyte depth, ubyte type, uint width) { 1513 immutable bitsPerChannel = depth; 1514 1515 int bitsPerPixel = bitsPerChannel; 1516 if(type & 2 && !(type & 1)) // in color, but no palette 1517 bitsPerPixel *= 3; 1518 if(type & 4) // has alpha channel 1519 bitsPerPixel += bitsPerChannel; 1520 1521 immutable int sizeInBits = width * bitsPerPixel; 1522 1523 // need to round up to the nearest byte 1524 int sizeInBytes = (sizeInBits + 7) / 8; 1525 1526 return sizeInBytes + 1; // the +1 is for the filter byte that precedes all lines 1527 } 1528 1529 /************************************************** 1530 * Buffered input range - generic, non-image code 1531 ***************************************************/ 1532 1533 /// Is the given range a buffered input range? That is, an input range 1534 /// that also provides consumeFromFront(int) and appendToFront() 1535 template isBufferedInputRange(R) { 1536 enum bool isBufferedInputRange = 1537 isInputRange!(R) && is(typeof( 1538 { 1539 R r; 1540 r.consumeFromFront(0); 1541 r.appendToFront(); 1542 }())); 1543 } 1544 1545 /// Allows appending to front on a regular input range, if that range is 1546 /// an array. It appends to the array rather than creating an array of 1547 /// arrays; it's meant to make the illusion of one continuous front rather 1548 /// than simply adding capability to walk backward to an existing input range. 1549 /// 1550 /// I think something like this should be standard; I find File.byChunk 1551 /// to be almost useless without this capability. 1552 1553 // FIXME: what if Range is actually an array itself? We should just use 1554 // slices right into it... I guess maybe r.front() would be the whole 1555 // thing in that case though, so we would indeed be slicing in right now. 1556 // Gotta check it though. 1557 struct BufferedInputRange(Range) 1558 if(isInputRange!(Range) && isArray!(ElementType!(Range))) 1559 { 1560 private Range underlyingRange; 1561 private ElementType!(Range) buffer; 1562 1563 /// Creates a buffer for the given range. You probably shouldn't 1564 /// keep using the underlying range directly. 1565 /// 1566 /// It assumes the underlying range has already been primed. 1567 this(Range r) { 1568 underlyingRange = r; 1569 // Is this really correct? Want to make sure r.front 1570 // is valid but it doesn't necessarily need to have 1571 // more elements... 1572 enforce(!r.empty()); 1573 1574 buffer = r.front(); 1575 usingUnderlyingBuffer = true; 1576 } 1577 1578 /// Forwards to the underlying range's empty function 1579 bool empty() { 1580 return underlyingRange.empty(); 1581 } 1582 1583 /// Returns the current buffer 1584 ElementType!(Range) front() { 1585 return buffer; 1586 } 1587 1588 // actually, not terribly useful IMO. appendToFront calls it 1589 // implicitly when necessary 1590 1591 /// Discard the current buffer and get the next item off the 1592 /// underlying range. Be sure to call at least once to prime 1593 /// the range (after checking if it is empty, of course) 1594 void popFront() { 1595 enforce(!empty()); 1596 underlyingRange.popFront(); 1597 buffer = underlyingRange.front(); 1598 usingUnderlyingBuffer = true; 1599 } 1600 1601 bool usingUnderlyingBuffer = false; 1602 1603 /// Remove the first count items from the buffer 1604 void consumeFromFront(int count) { 1605 buffer = buffer[count .. $]; 1606 } 1607 1608 /// Append the next item available on the underlying range to 1609 /// our buffer. 1610 void appendToFront() { 1611 if(buffer.length == 0) { 1612 // may let us reuse the underlying range's buffer, 1613 // hopefully avoiding an extra allocation 1614 popFront(); 1615 } else { 1616 enforce(!underlyingRange.empty()); 1617 1618 // need to make sure underlyingRange.popFront doesn't overwrite any 1619 // of our buffer... 1620 if(usingUnderlyingBuffer) { 1621 buffer = buffer.dup; 1622 usingUnderlyingBuffer = false; 1623 } 1624 1625 underlyingRange.popFront(); 1626 1627 buffer ~= underlyingRange.front(); 1628 } 1629 } 1630 } 1631 1632 /************************************************** 1633 * Lower level implementations of image formats. 1634 * and associated helper functions. 1635 * 1636 * Related to the module, but not particularly 1637 * interesting, so it's at the bottom. 1638 ***************************************************/ 1639 1640 1641 /* PNG file format implementation */ 1642 1643 //import std.zlib; 1644 import std.math; 1645 1646 /// All PNG files are supposed to open with these bytes according to the spec 1647 enum immutable(ubyte[]) PNG_MAGIC_NUMBER = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 1648 1649 /// A PNG file consists of the magic number then a stream of chunks. This 1650 /// struct represents those chunks. 1651 struct Chunk { 1652 uint size; 1653 ubyte[4] type; 1654 ubyte[] payload; 1655 uint checksum; 1656 1657 /// returns the type as a string for easier comparison with literals 1658 const(char)[] stype() return const { 1659 return cast(const(char)[]) type; 1660 } 1661 1662 static Chunk* create(string type, ubyte[] payload) 1663 in { 1664 assert(type.length == 4); 1665 } 1666 body { 1667 Chunk* c = new Chunk; 1668 c.size = cast(int) payload.length; 1669 c.type[] = (cast(ubyte[]) type)[]; 1670 c.payload = payload; 1671 1672 c.checksum = crcPng(type, payload); 1673 1674 return c; 1675 } 1676 1677 /// Puts it into the format for outputting to a file 1678 ubyte[] toArray() { 1679 ubyte[] a; 1680 a.length = size + 12; 1681 1682 int pos = 0; 1683 1684 a[pos++] = (size & 0xff000000) >> 24; 1685 a[pos++] = (size & 0x00ff0000) >> 16; 1686 a[pos++] = (size & 0x0000ff00) >> 8; 1687 a[pos++] = (size & 0x000000ff) >> 0; 1688 1689 a[pos .. pos + 4] = type[0 .. 4]; 1690 pos += 4; 1691 1692 a[pos .. pos + size] = payload[0 .. size]; 1693 1694 pos += size; 1695 1696 assert(checksum); 1697 1698 a[pos++] = (checksum & 0xff000000) >> 24; 1699 a[pos++] = (checksum & 0x00ff0000) >> 16; 1700 a[pos++] = (checksum & 0x0000ff00) >> 8; 1701 a[pos++] = (checksum & 0x000000ff) >> 0; 1702 1703 return a; 1704 } 1705 } 1706 1707 /// The first chunk in a PNG file is a header that contains this info 1708 struct PngHeader { 1709 /// Width of the image, in pixels. 1710 uint width; 1711 1712 /// Height of the image, in pixels. 1713 uint height; 1714 1715 /** 1716 This is bits per channel - per color for truecolor or grey 1717 and per pixel for palette. 1718 1719 Indexed ones can have depth of 1,2,4, or 8, 1720 1721 Greyscale can be 1,2,4,8,16 1722 1723 Everything else must be 8 or 16. 1724 */ 1725 ubyte depth = 8; 1726 1727 /** Types from the PNG spec: 1728 0 - greyscale 1729 2 - truecolor 1730 3 - indexed color 1731 4 - grey with alpha 1732 6 - true with alpha 1733 1734 1, 5, and 7 are invalid. 1735 1736 There's a kind of bitmask going on here: 1737 If type&1, it has a palette. 1738 If type&2, it is in color. 1739 If type&4, it has an alpha channel in the datastream. 1740 */ 1741 ubyte type = 6; 1742 1743 ubyte compressionMethod = 0; /// should be zero 1744 ubyte filterMethod = 0; /// should be zero 1745 /// 0 is non interlaced, 1 if Adam7. No more are defined in the spec 1746 ubyte interlaceMethod = 0; 1747 1748 static PngHeader fromChunk(in Chunk c) { 1749 enforce(c.stype == "IHDR", 1750 "The chunk is not an image header"); 1751 1752 PngHeader h; 1753 auto data = c.payload; 1754 int pos = 0; 1755 1756 enforce(data.length == 13, 1757 "Malformed PNG file - the IHDR is the wrong size"); 1758 1759 h.width |= data[pos++] << 24; 1760 h.width |= data[pos++] << 16; 1761 h.width |= data[pos++] << 8; 1762 h.width |= data[pos++] << 0; 1763 1764 h.height |= data[pos++] << 24; 1765 h.height |= data[pos++] << 16; 1766 h.height |= data[pos++] << 8; 1767 h.height |= data[pos++] << 0; 1768 1769 h.depth = data[pos++]; 1770 h.type = data[pos++]; 1771 h.compressionMethod = data[pos++]; 1772 h.filterMethod = data[pos++]; 1773 h.interlaceMethod = data[pos++]; 1774 1775 return h; 1776 } 1777 1778 Chunk* toChunk() { 1779 ubyte[] data; 1780 data.length = 13; 1781 int pos = 0; 1782 1783 data[pos++] = width >> 24; 1784 data[pos++] = (width >> 16) & 0xff; 1785 data[pos++] = (width >> 8) & 0xff; 1786 data[pos++] = width & 0xff; 1787 1788 data[pos++] = height >> 24; 1789 data[pos++] = (height >> 16) & 0xff; 1790 data[pos++] = (height >> 8) & 0xff; 1791 data[pos++] = height & 0xff; 1792 1793 data[pos++] = depth; 1794 data[pos++] = type; 1795 data[pos++] = compressionMethod; 1796 data[pos++] = filterMethod; 1797 data[pos++] = interlaceMethod; 1798 1799 assert(pos == 13); 1800 1801 return Chunk.create("IHDR", data); 1802 } 1803 } 1804 1805 void writePngLazy(OutputRange, InputRange)(ref OutputRange where, InputRange image) 1806 if( 1807 isOutputRange!(OutputRange, ubyte[]) && 1808 isInputRange!(InputRange) && 1809 is(ElementType!InputRange == RgbaScanline)) 1810 { 1811 import std.zlib; 1812 where.put(PNG_MAGIC_NUMBER); 1813 PngHeader header; 1814 1815 assert(!image.empty()); 1816 1817 // using the default values for header here... FIXME not super clear 1818 1819 header.width = image.front.pixels.length; 1820 header.height = image.length; 1821 1822 enforce(header.width > 0, "Image width <= 0"); 1823 enforce(header.height > 0, "Image height <= 0"); 1824 1825 where.put(header.toChunk().toArray()); 1826 1827 auto compressor = new std.zlib.Compress(); 1828 const(void)[] compressedData; 1829 int cnt; 1830 foreach(line; image) { 1831 // YOU'VE GOT TO BE FUCKING KIDDING ME! 1832 // I have to /cast/ to void[]!??!? 1833 1834 ubyte[] data; 1835 data.length = 1 + header.width * 4; 1836 data[0] = 0; // filter type 1837 int offset = 1; 1838 foreach(pixel; line.pixels) { 1839 data[offset++] = pixel.r; 1840 data[offset++] = pixel.g; 1841 data[offset++] = pixel.b; 1842 data[offset++] = pixel.a; 1843 } 1844 1845 compressedData ~= compressor.compress(cast(void[]) 1846 data); 1847 if(compressedData.length > 2_000) { 1848 where.put(Chunk.create("IDAT", cast(ubyte[]) 1849 compressedData).toArray()); 1850 compressedData = null; 1851 } 1852 1853 cnt++; 1854 } 1855 1856 assert(cnt == header.height, format("Got %d lines instead of %d", cnt, header.height)); 1857 1858 compressedData ~= compressor.flush(); 1859 if(compressedData.length) 1860 where.put(Chunk.create("IDAT", cast(ubyte[]) 1861 compressedData).toArray()); 1862 1863 where.put(Chunk.create("IEND", null).toArray()); 1864 } 1865 1866 // bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey 1867 1868 uint crcPng(in string chunkName, in ubyte[] buf){ 1869 uint c = update_crc(0xffffffffL, cast(ubyte[]) chunkName); 1870 return update_crc(c, buf) ^ 0xffffffffL; 1871 } 1872 1873 immutable(ubyte)[] unfilter(ubyte filterType, in ubyte[] data, in ubyte[] previousLine, int bpp) { 1874 // Note: the overflow arithmetic on the ubytes in here is intentional 1875 switch(filterType) { 1876 case 0: 1877 return data.idup; // FIXME is copying really necessary? 1878 case 1: 1879 auto arr = data.dup; 1880 // first byte gets zero added to it so nothing special 1881 foreach(i; bpp .. arr.length) { 1882 arr[i] += arr[i - bpp]; 1883 } 1884 1885 return assumeUnique(arr); 1886 case 2: 1887 auto arr = data.dup; 1888 if(previousLine.length) 1889 foreach(i; 0 .. arr.length) { 1890 arr[i] += previousLine[i]; 1891 } 1892 1893 return assumeUnique(arr); 1894 case 3: 1895 auto arr = data.dup; 1896 if(previousLine.length) 1897 foreach(i; 0 .. arr.length) { 1898 auto prev = i < bpp ? 0 : arr[i - bpp]; 1899 arr[i] += cast(ubyte) 1900 /*std.math.floor*/( cast(int) (prev + previousLine[i]) / 2); 1901 } 1902 1903 return assumeUnique(arr); 1904 case 4: 1905 auto arr = data.dup; 1906 foreach(i; 0 .. arr.length) { 1907 ubyte prev = i < bpp ? 0 : arr[i - bpp]; 1908 ubyte prevLL = i < bpp ? 0 : (i < previousLine.length ? previousLine[i - bpp] : 0); 1909 1910 arr[i] += PaethPredictor(prev, (i < previousLine.length ? previousLine[i] : 0), prevLL); 1911 } 1912 1913 return assumeUnique(arr); 1914 default: 1915 throw new Exception("invalid PNG file, bad filter type"); 1916 } 1917 } 1918 1919 ubyte PaethPredictor(ubyte a, ubyte b, ubyte c) { 1920 int p = cast(int) a + b - c; 1921 auto pa = abs(p - a); 1922 auto pb = abs(p - b); 1923 auto pc = abs(p - c); 1924 1925 if(pa <= pb && pa <= pc) 1926 return a; 1927 if(pb <= pc) 1928 return b; 1929 return c; 1930 } 1931 1932 int bytesPerPixel(PngHeader header) { 1933 immutable bitsPerChannel = header.depth; 1934 1935 int bitsPerPixel = bitsPerChannel; 1936 if(header.type & 2 && !(header.type & 1)) // in color, but no palette 1937 bitsPerPixel *= 3; 1938 if(header.type & 4) // has alpha channel 1939 bitsPerPixel += bitsPerChannel; 1940 1941 return (bitsPerPixel + 7) / 8; 1942 }