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