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 }