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