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