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 }