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