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