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