1 /** 2 * libpng support. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.graphics.libpng; 15 16 import std.exception; 17 import std.string : fromStringz; 18 19 debug(LIBPNG) import std.stdio : stderr; 20 21 import ae.utils.graphics.color; 22 import ae.utils.graphics.image; 23 24 import libpng.png; 25 import libpng.pnglibconf; 26 27 pragma(lib, "png"); 28 29 struct PNGReader 30 { 31 // Settings 32 33 bool strict = true; // Throw on corrupt / invalid data vs. ignore errors as much as possible 34 enum Depth { d8, d16 } Depth depth; 35 enum Channels { gray, rgb, bgr } Channels channels; 36 enum Alpha { none, alpha, filler } Alpha alpha; 37 enum AlphaLocation { before, after } AlphaLocation alphaLocation; 38 ubyte[] defaultColor; 39 40 // Callbacks 41 42 void delegate(int width, int height) infoHandler; 43 ubyte[] delegate(uint rowNum) rowGetter; 44 void delegate(uint rowNum, int pass) rowHandler; 45 void delegate() endHandler; 46 47 // Data 48 49 size_t rowbytes; 50 uint passes; 51 52 // Public interface 53 54 void init() 55 { 56 png_ptr = png_create_read_struct( 57 png_get_libpng_ver(null), 58 &this, 59 &libpngErrorHandler, 60 &libpngWarningHandler 61 ).enforce("png_create_read_struct"); 62 scope(failure) png_destroy_read_struct(&png_ptr, null, null); 63 64 info_ptr = png_create_info_struct(png_ptr) 65 .enforce("png_create_info_struct"); 66 67 if (!strict) 68 png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); 69 70 png_set_progressive_read_fn(png_ptr, 71 &this, 72 &libpngInfoCallback, 73 &libpngRowCallback, 74 &libpngEndCallback, 75 ); 76 } 77 78 void put(ubyte[] data) 79 { 80 png_process_data(png_ptr, info_ptr, data.ptr, data.length); 81 } 82 83 private: 84 png_structp png_ptr; 85 png_infop info_ptr; 86 87 static extern(C) void libpngInfoCallback(png_structp png_ptr, png_infop info_ptr) 88 { 89 int color_type, bit_depth; 90 png_uint_32 width, height; 91 92 auto self = cast(PNGReader*)png_get_progressive_ptr(png_ptr); 93 assert(self); 94 95 png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 96 null, null, null); 97 98 png_set_expand(png_ptr); 99 100 version (LittleEndian) 101 png_set_swap(png_ptr); 102 103 final switch (self.depth) 104 { 105 case Depth.d8: 106 png_set_scale_16(png_ptr); 107 break; 108 case Depth.d16: 109 png_set_expand_16(png_ptr); 110 break; 111 } 112 113 final switch (self.channels) 114 { 115 case Channels.gray: 116 png_set_rgb_to_gray(png_ptr, 117 PNG_ERROR_ACTION_NONE, 118 PNG_RGB_TO_GRAY_DEFAULT, 119 PNG_RGB_TO_GRAY_DEFAULT 120 ); 121 break; 122 case Channels.rgb: 123 png_set_gray_to_rgb(png_ptr); 124 break; 125 case Channels.bgr: 126 png_set_gray_to_rgb(png_ptr); 127 png_set_bgr(png_ptr); 128 break; 129 } 130 131 if (self.alpha != Alpha.alpha) 132 { 133 png_set_strip_alpha(png_ptr); 134 135 png_color_16p image_background; 136 if (png_get_bKGD(png_ptr, info_ptr, &image_background)) 137 { 138 if (image_background.gray == 0 && 139 ( 140 image_background.red != 0 || 141 image_background.green != 0 || 142 image_background.blue != 0 143 )) 144 { 145 // Work around libpng bug. 146 // Note: this conversion uses a different algorithm than libpng... 147 debug(LIBPNG) stderr.writeln("Manually adding gray image background."); 148 image_background.gray = (image_background.red + image_background.green + image_background.blue) / 3; 149 } 150 151 png_set_background(png_ptr, image_background, 152 PNG_BACKGROUND_GAMMA_FILE, 1/*needs to be expanded*/, 1); 153 } 154 else 155 if (self.defaultColor) 156 png_set_background(png_ptr, 157 cast(png_const_color_16p)self.defaultColor.ptr, 158 PNG_BACKGROUND_GAMMA_SCREEN, 0/*do not expand*/, 1); 159 } 160 161 if (self.alpha != Alpha.none) 162 { 163 int location; 164 final switch (self.alphaLocation) 165 { 166 case AlphaLocation.before: 167 location = PNG_FILLER_BEFORE; 168 png_set_swap_alpha(png_ptr); 169 break; 170 case AlphaLocation.after: 171 location = PNG_FILLER_AFTER; 172 break; 173 } 174 final switch (self.alpha) 175 { 176 case Alpha.none: 177 assert(false); 178 case Alpha.alpha: 179 png_set_add_alpha(png_ptr, 0xFFFFFFFF, location); 180 break; 181 case Alpha.filler: 182 png_set_filler(png_ptr, 0, location); 183 break; 184 } 185 } 186 187 self.passes = png_set_interlace_handling(png_ptr); 188 189 png_read_update_info(png_ptr, info_ptr); 190 191 self.rowbytes = cast(int)png_get_rowbytes(png_ptr, info_ptr); 192 193 if (self.infoHandler) 194 self.infoHandler(width, height); 195 } 196 197 extern(C) static void libpngRowCallback(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num, int pass) 198 { 199 auto self = cast(PNGReader*)png_get_progressive_ptr(png_ptr); 200 assert(self); 201 202 auto row = self.rowGetter(row_num); 203 if (row.length != self.rowbytes) 204 assert(false, "Row size mismatch"); 205 206 png_progressive_combine_row(png_ptr, row.ptr, new_row); 207 208 if (self.rowHandler) 209 self.rowHandler(row_num, pass); 210 } 211 212 extern(C) static void libpngEndCallback(png_structp png_ptr, png_infop info_ptr) 213 { 214 auto self = cast(PNGReader*)png_get_progressive_ptr(png_ptr); 215 assert(self); 216 217 if (self.endHandler) 218 self.endHandler(); 219 } 220 221 extern(C) static void libpngWarningHandler(png_structp png_ptr, png_const_charp msg) 222 { 223 debug(LIBPNG) stderr.writeln("PNG warning: ", fromStringz(msg)); 224 225 auto self = cast(PNGReader*)png_get_progressive_ptr(png_ptr); 226 assert(self); 227 228 if (self.strict) 229 throw new Exception("PNG warning: " ~ fromStringz(msg).assumeUnique); 230 } 231 232 extern(C) static void libpngErrorHandler(png_structp png_ptr, png_const_charp msg) 233 { 234 debug(LIBPNG) stderr.writeln("PNG error: ", fromStringz(msg)); 235 236 auto self = cast(PNGReader*)png_get_progressive_ptr(png_ptr); 237 assert(self); 238 239 // We must stop execution here, otherwise libpng abort()s 240 throw new Exception("PNG error: " ~ fromStringz(msg).assumeUnique); 241 } 242 243 @disable this(this); 244 245 ~this() 246 { 247 if (png_ptr && info_ptr) 248 png_destroy_read_struct(&png_ptr, &info_ptr, null); 249 png_ptr = null; 250 info_ptr = null; 251 } 252 } 253 254 Image!COLOR decodePNG(COLOR)(ubyte[] data, bool strict = true) 255 { 256 Image!COLOR img; 257 258 PNGReader reader; 259 reader.strict = strict; 260 reader.init(); 261 262 // Depth 263 264 static if (is(ChannelType!COLOR == ubyte)) 265 reader.depth = PNGReader.Depth.d8; 266 else 267 static if (is(ChannelType!COLOR == ushort)) 268 reader.depth = PNGReader.Depth.d16; 269 else 270 static assert(false, "Can't read PNG into " ~ ChannelType!COLOR.stringof ~ " channels"); 271 272 // Channels 273 274 static if (!is(COLOR == struct)) 275 enum channels = ["l"]; 276 else 277 { 278 import ae.utils.meta : structFields; 279 enum channels = structFields!COLOR; 280 } 281 282 // Alpha location 283 284 static if (channels[0] == "a" || channels[0] == "x") 285 { 286 reader.alphaLocation = PNGReader.AlphaLocation.before; 287 enum alphaChannel = channels[0]; 288 enum colorChannels = channels[1 .. $]; 289 } 290 else 291 static if (channels[$-1] == "a" || channels[$-1] == "x") 292 { 293 reader.alphaLocation = PNGReader.AlphaLocation.after; 294 enum alphaChannel = channels[$-1]; 295 enum colorChannels = channels[0 .. $-1]; 296 } 297 else 298 { 299 enum alphaChannel = null; 300 enum colorChannels = channels; 301 } 302 303 // Alpha kind 304 305 static if (alphaChannel is null) 306 reader.alpha = PNGReader.Alpha.none; 307 else 308 static if (alphaChannel == "a") 309 reader.alpha = PNGReader.Alpha.alpha; 310 else 311 static if (alphaChannel == "x") 312 reader.alpha = PNGReader.Alpha.filler; 313 else 314 static assert(false); 315 316 // Channel order 317 318 static if (colorChannels == ["l"]) 319 reader.channels = PNGReader.Channels.gray; 320 else 321 static if (colorChannels == ["r", "g", "b"]) 322 reader.channels = PNGReader.Channels.rgb; 323 else 324 static if (colorChannels == ["b", "g", "r"]) 325 reader.channels = PNGReader.Channels.bgr; 326 else 327 static assert(false, "Can't read PNG into channel order " ~ channels.stringof); 328 329 // Delegates 330 331 reader.infoHandler = (int width, int height) 332 { 333 img.size(width, height); 334 }; 335 336 reader.rowGetter = (uint rowNum) 337 { 338 return cast(ubyte[])img.scanline(rowNum); 339 }; 340 341 reader.put(data); 342 343 return img; 344 } 345 346 unittest 347 { 348 static struct BitWriter 349 { 350 ubyte[] buf; 351 size_t off; ubyte bit; 352 353 void write(T)(T value, ubyte size) 354 { 355 foreach_reverse (vBit; 0..size) 356 { 357 ubyte b = cast(ubyte)(ulong(value) >> vBit) & 1; 358 auto bBit = 7 - this.bit; 359 buf[this.off] |= b << bBit; 360 if (++this.bit == 8) 361 { 362 this.bit = 0; 363 this.off++; 364 } 365 } 366 } 367 } 368 369 static void testColor(PNGReader.Depth depth, PNGReader.Channels channels, PNGReader.Alpha alpha, PNGReader.AlphaLocation alphaLocation)() 370 { 371 debug(LIBPNG) stderr.writefln(">>> COLOR depth=%-3s channels=%-4s alpha=%-6s alphaloc=%-6s", 372 depth, channels, alpha, alphaLocation); 373 374 static if (depth == PNGReader.Depth.d8) 375 alias ChannelType = ubyte; 376 else 377 static if (depth == PNGReader.Depth.d16) 378 alias ChannelType = ushort; 379 else 380 static assert(false); 381 382 static if (alpha == PNGReader.Alpha.none) 383 enum string[] alphaField = []; 384 else 385 static if (alpha == PNGReader.Alpha.alpha) 386 enum alphaField = ["a"]; 387 else 388 static if (alpha == PNGReader.Alpha.filler) 389 enum alphaField = ["x"]; 390 else 391 static assert(false); 392 393 static if (channels == PNGReader.Channels.gray) 394 enum channelFields = ["l"]; 395 else 396 static if (channels == PNGReader.Channels.rgb) 397 enum channelFields = ["r", "g", "b"]; 398 else 399 static if (channels == PNGReader.Channels.bgr) 400 enum channelFields = ["b", "g", "r"]; 401 else 402 static assert(false); 403 404 static if (alphaLocation == PNGReader.AlphaLocation.before) 405 enum fields = alphaField ~ channelFields; 406 else 407 static if (alphaLocation == PNGReader.AlphaLocation.after) 408 enum fields = channelFields ~ alphaField; 409 else 410 static assert(false); 411 412 import ae.utils.meta : ArrayToTuple; 413 alias COLOR = Color!(ChannelType, ArrayToTuple!fields); 414 415 enum Bkgd { none, black, white } 416 417 static void testPNG(ubyte pngDepth, bool pngPaletted, bool pngColor, bool pngAlpha, bool pngTrns, Bkgd pngBkgd) 418 { 419 debug(LIBPNG) stderr.writefln(" > PNG depth=%2d palette=%d color=%d alpha=%d trns=%d bkgd=%-5s", 420 pngDepth, pngPaletted, pngColor, pngAlpha, pngTrns, pngBkgd); 421 422 void skip(string msg) { debug(LIBPNG) stderr.writefln(" >> Skipped: %s", msg); } 423 424 enum numPixels = 7; 425 426 if (pngPaletted && !pngColor) 427 return skip("Palette without color rejected by libpng ('Invalid color type in IHDR')"); 428 if (pngPaletted && pngAlpha) 429 return skip("Palette with alpha rejected by libpng ('Invalid color type in IHDR')"); 430 if (pngPaletted && pngDepth > 8) 431 return skip("Large palette rejected by libpng ('Invalid color type/bit depth combination in IHDR')"); 432 if (pngAlpha && pngDepth < 8) 433 return skip("Alpha with low bit depth rejected by libpng ('Invalid color type/bit depth combination in IHDR')"); 434 if (pngColor && !pngPaletted && pngDepth < 8) 435 return skip("Non-palette RGB with low bit depth rejected by libpng ('Invalid color type/bit depth combination in IHDR')"); 436 if (pngTrns && pngAlpha) 437 return skip("tRNS with alpha is redundant, libpng complains ('invalid with alpha channel')"); 438 if (pngTrns && !pngPaletted && pngDepth < 2) 439 return skip("Not enough bits to represent tRNS color"); 440 if (pngPaletted && (1 << pngDepth) < numPixels) 441 return skip("Not enough bits to represent all palette color indices"); 442 443 import std.bitmanip : nativeToBigEndian; 444 import std.conv : to; 445 import std.algorithm.iteration : sum; 446 447 ubyte pngChannelSize; 448 if (pngPaletted) 449 pngChannelSize = 8; // PLTE is always 8-bit 450 else 451 pngChannelSize = pngDepth; 452 453 ulong pngChannelMax = (1 << pngChannelSize) - 1; 454 ulong pngChannelMed = pngChannelMax / 2; 455 ulong bkgdColor = [pngChannelMed, 0, pngChannelMax][pngBkgd]; 456 ulong[4][numPixels] pixels = [ 457 [ 0, 0, 0, pngChannelMax], // black 458 [pngChannelMax, pngChannelMax, pngChannelMax, pngChannelMax], // white 459 [pngChannelMax, pngChannelMed, 0, pngChannelMax], // red 460 [ 0, pngChannelMed, pngChannelMax, pngChannelMax], // blue 461 [ ulong(0), 0, 0, 0], // transparent (zero alpha) 462 [ 1, 2, 3, pngChannelMax], // transparent (tRNS color) 463 [bkgdColor , bkgdColor , bkgdColor , pngChannelMax], // bKGD color (for palette index) 464 ]; 465 enum pixelIndexTRNS = 5; 466 enum pixelIndexBKGD = 6; 467 468 ubyte colourType; 469 if (pngPaletted) 470 colourType |= PNG_COLOR_MASK_PALETTE; 471 if (pngColor) 472 colourType |= PNG_COLOR_MASK_COLOR; 473 if (pngAlpha) 474 colourType |= PNG_COLOR_MASK_ALPHA; 475 476 PNGChunk[] chunks; 477 PNGHeader header = { 478 width : nativeToBigEndian(int(pixels.length)), 479 height : nativeToBigEndian(1), 480 colourDepth : pngDepth, 481 colourType : cast(PNGColourType)colourType, 482 compressionMethod : PNGCompressionMethod.DEFLATE, 483 filterMethod : PNGFilterMethod.ADAPTIVE, 484 interlaceMethod : PNGInterlaceMethod.NONE, 485 }; 486 chunks ~= PNGChunk("IHDR", cast(void[])[header]); 487 488 if (pngPaletted) 489 { 490 auto palette = BitWriter(new ubyte[3 * pixels.length]); 491 foreach (pixel; pixels) 492 foreach (channel; pixel[0..3]) 493 palette.write(channel, 8); 494 chunks ~= PNGChunk("PLTE", palette.buf); 495 } 496 497 if (pngTrns) 498 { 499 BitWriter trns; 500 if (pngPaletted) 501 { 502 trns = BitWriter(new ubyte[pixels.length]); 503 foreach (pixel; pixels) 504 trns.write(pixel[3] * 255 / pngChannelMax, 8); 505 } 506 else 507 if (pngColor) 508 { 509 trns = BitWriter(new ubyte[3 * ushort.sizeof]); 510 foreach (channel; pixels[pixelIndexTRNS][0..3]) 511 trns.write(channel, 16); 512 } 513 else 514 { 515 trns = BitWriter(new ubyte[ushort.sizeof]); 516 trns.write(pixels[pixelIndexTRNS][0..3].sum / 3, 16); 517 } 518 debug(LIBPNG) stderr.writefln(" tRNS=%s", trns.buf); 519 chunks ~= PNGChunk("tRNS", trns.buf); 520 } 521 522 if (pngBkgd != Bkgd.none) 523 { 524 BitWriter bkgd; 525 if (pngPaletted) 526 { 527 bkgd = BitWriter(new ubyte[1]); 528 bkgd.write(pixelIndexBKGD, 8); 529 } 530 else 531 if (pngColor) 532 { 533 bkgd = BitWriter(new ubyte[3 * ushort.sizeof]); 534 foreach (channel; 0..3) 535 bkgd.write(bkgdColor, 16); 536 } 537 else 538 { 539 bkgd = BitWriter(new ubyte[ushort.sizeof]); 540 bkgd.write(bkgdColor, 16); 541 } 542 chunks ~= PNGChunk("bKGD", bkgd.buf); 543 } 544 545 auto channelBits = pixels.length * pngDepth; 546 547 uint pngChannels; 548 if (pngPaletted) 549 pngChannels = 1; 550 else 551 if (pngColor) 552 pngChannels = 3; 553 else 554 pngChannels = 1; 555 if (pngAlpha) 556 pngChannels++; 557 auto pixelBits = channelBits * pngChannels; 558 559 auto pixelBytes = (pixelBits + 7) / 8; 560 uint idatStride = to!uint(1 + pixelBytes); 561 auto idat = BitWriter(new ubyte[idatStride]); 562 idat.write(PNGFilterAdaptive.NONE, 8); 563 564 foreach (x; 0 .. pixels.length) 565 { 566 if (pngPaletted) 567 idat.write(x, pngDepth); 568 else 569 if (pngColor) 570 foreach (channel; pixels[x][0..3]) 571 idat.write(channel, pngDepth); 572 else 573 idat.write(pixels[x][0..3].sum / 3, pngDepth); 574 575 if (pngAlpha) 576 idat.write(pixels[x][3], pngDepth); 577 } 578 579 import std.zlib : compress; 580 chunks ~= PNGChunk("IDAT", compress(idat.buf, 0)); 581 582 chunks ~= PNGChunk("IEND", null); 583 584 auto bytes = makePNG(chunks); 585 auto img = decodePNG!COLOR(bytes); 586 587 assert(img.w == pixels.length); 588 assert(img.h == 1); 589 590 import std.conv : text; 591 592 // Solids 593 594 void checkSolid(bool nontrans=false)(int x, ulong[3] spec) 595 { 596 ChannelType r = cast(ChannelType)(spec[0] * ChannelType.max / pngChannelMax); 597 ChannelType g = cast(ChannelType)(spec[1] * ChannelType.max / pngChannelMax); 598 ChannelType b = cast(ChannelType)(spec[2] * ChannelType.max / pngChannelMax); 599 600 immutable c = img[x, 0]; 601 602 scope(failure) debug(LIBPNG) stderr.writeln("x:", x, " def:", spec, " / expected:", [r,g,b], " / got:", c); 603 604 static if (nontrans) 605 { /* Already checked alpha / filler in checkTransparent */ } 606 else 607 static if (alpha == PNGReader.Alpha.filler) 608 { 609 assert(c.x == 0); 610 } 611 else 612 static if (alpha == PNGReader.Alpha.alpha) 613 assert(c.a == ChannelType.max); 614 615 ChannelType norm(ChannelType v) 616 { 617 uint pngMax; 618 if (pngPaletted) 619 pngMax = 255; 620 else 621 pngMax = (1 << pngDepth) - 1; 622 return cast(ChannelType)(v * pngMax / ChannelType.max * ChannelType.max / pngMax); 623 } 624 625 if (!pngColor) 626 r = g = b = (r + g + b) / 3; 627 628 static if (channels == PNGReader.Channels.gray) 629 { 630 if (spec == [1,2,3]) 631 assert(c.l <= norm(b)); 632 else 633 if (pngColor && spec[0..3].sum / 3 == pngChannelMax / 2) 634 { 635 // libpng's RGB to grayscale conversion is not straight-forward, 636 // do a range check 637 assert(c.l > 0 && c.l < ChannelType.max); 638 } 639 else 640 assert(c.l == norm((r + g + b) / 3), text(c.l, " != ", norm((r + g + b) / 3))); 641 } 642 else 643 { 644 assert(c.r == norm(r)); 645 assert(c.g == norm(g)); 646 assert(c.b == norm(b)); 647 } 648 } 649 650 foreach (x; 0..4) 651 checkSolid(x, pixels[x][0..3]); 652 653 // Transparency 654 655 void checkTransparent(int x, ulong[3] bgColor) 656 { 657 auto c = img[x, 0]; 658 659 scope(failure) debug(LIBPNG) stderr.writeln("x:", x, " def:", pixels[x], " / got:", c); 660 661 static if (alpha == PNGReader.Alpha.alpha) 662 assert(c.a == 0); 663 else 664 { 665 static if (alpha == PNGReader.Alpha.filler) 666 assert(c.x == 0); 667 668 ulong[3] bg = pngBkgd != Bkgd.none ? [bkgdColor, bkgdColor, bkgdColor] : bgColor; 669 ChannelType[3] cbg; 670 foreach (i; 0..3) 671 cbg[i] = cast(ChannelType)(bg[i] * ChannelType.max / pngChannelMax); 672 673 checkSolid!true(x, bg); 674 } 675 } 676 677 if (pngAlpha || (pngTrns && pngPaletted)) 678 checkTransparent(4, [0,0,0]); 679 else 680 checkSolid(4, [0,0,0]); 681 682 if (pngTrns && !pngPaletted) 683 { 684 if (pngBkgd != Bkgd.none) 685 {} // libpng bug! 686 else 687 checkTransparent(5, [1,2,3]); 688 } 689 else 690 checkSolid(5, [1,2,3]); 691 } 692 693 foreach (ubyte pngDepth; [1, 2, 4, 8, 16]) 694 foreach (pngPaletted; [false, true]) 695 foreach (pngColor; [false, true]) 696 foreach (pngAlpha; [false, true]) 697 foreach (pngTrns; [false, true]) 698 foreach (pngBkgd; [EnumMembers!Bkgd]) // absent, black, white 699 testPNG(pngDepth, pngPaletted, pngColor, pngAlpha, pngTrns, pngBkgd); 700 } 701 702 import std.traits : EnumMembers; 703 foreach (depth; EnumMembers!(PNGReader.Depth)) 704 foreach (channels; EnumMembers!(PNGReader.Channels)) 705 foreach (alpha; EnumMembers!(PNGReader.Alpha)) 706 foreach (alphaLocation; EnumMembers!(PNGReader.AlphaLocation)) 707 testColor!(depth, channels, alpha, alphaLocation); 708 }