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