1 /**
2  * In-memory images and various image formats.
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.image;
15 
16 import std.algorithm;
17 import std.conv : to;
18 import std.exception;
19 import std.range;
20 import std.string : format;
21 
22 public import ae.utils.graphics.view;
23 
24 /// Represents a reference to COLOR data
25 /// already existing elsewhere in memory.
26 /// Assumes that pixels are stored row-by-row,
27 /// with a known distance between each row.
28 struct ImageRef(COLOR)
29 {
30 	int w, h;
31 	size_t pitch; /// In bytes, not COLORs
32 	COLOR* pixels;
33 
34 	/// Returns an array for the pixels at row y.
35 	COLOR[] scanline(int y)
36 	{
37 		assert(y>=0 && y<h, "Scanline out-of-bounds");
38 		assert(pitch, "Pitch not set");
39 		auto row = cast(COLOR*)(cast(ubyte*)pixels + y*pitch);
40 		return row[0..w];
41 	}
42 
43 	mixin DirectView;
44 }
45 
46 unittest
47 {
48 	static assert(isDirectView!(ImageRef!ubyte));
49 }
50 
51 /// Convert a direct view to an ImageRef.
52 /// Assumes that the rows are evenly spaced.
53 ImageRef!(ViewColor!SRC) toRef(SRC)(auto ref SRC src)
54 	if (isDirectView!SRC)
55 {
56 	return ImageRef!(ViewColor!SRC)(src.w, src.h,
57 		src.h > 1 ? cast(ubyte*)src.scanline(1) - cast(ubyte*)src.scanline(0) : src.w,
58 		src.scanline(0).ptr);
59 }
60 
61 unittest
62 {
63 	auto i = Image!ubyte(1, 1);
64 	auto r = i.toRef();
65 	assert(r.scanline(0).ptr is i.scanline(0).ptr);
66 }
67 
68 // ***************************************************************************
69 
70 /// An in-memory image.
71 /// Pixels are stored in a flat array.
72 struct Image(COLOR)
73 {
74 	int w, h;
75 	COLOR[] pixels;
76 
77 	/// Returns an array for the pixels at row y.
78 	COLOR[] scanline(int y)
79 	{
80 		assert(y>=0 && y<h, "Scanline out-of-bounds");
81 		auto start = w*y;
82 		return pixels[start..start+w];
83 	}
84 
85 	mixin DirectView;
86 
87 	this(int w, int h)
88 	{
89 		size(w, h);
90 	}
91 
92 	/// Does not scale image
93 	void size(int w, int h)
94 	{
95 		this.w = w;
96 		this.h = h;
97 		if (pixels.length < w*h)
98 			pixels.length = w*h;
99 	}
100 }
101 
102 unittest
103 {
104 	static assert(isDirectView!(Image!ubyte));
105 }
106 
107 // ***************************************************************************
108 
109 // Functions which need a target image to operate on are currenty declared
110 // as two overloads. The code might be simplified if some of these get fixed:
111 // https://d.puremagic.com/issues/show_bug.cgi?id=8074
112 // https://d.puremagic.com/issues/show_bug.cgi?id=12386
113 // https://d.puremagic.com/issues/show_bug.cgi?id=12425
114 // https://d.puremagic.com/issues/show_bug.cgi?id=12426
115 // https://d.puremagic.com/issues/show_bug.cgi?id=12433
116 
117 alias ViewImage(V) = Image!(ViewColor!V);
118 
119 /// Copy the given view into the specified target.
120 auto copy(SRC, TARGET)(auto ref SRC src, auto ref TARGET target)
121 	if (isView!SRC && isWritableView!TARGET)
122 {
123 	target.size(src.w, src.h);
124 	src.blitTo(target);
125 	return target;
126 }
127 
128 /// Copy the given view into a newly-allocated image.
129 auto copy(SRC)(auto ref SRC src)
130 	if (isView!SRC)
131 {
132 	ViewImage!SRC target;
133 	return src.copy(target);
134 }
135 
136 unittest
137 {
138 	auto v = onePixel(0);
139 	auto i = v.copy();
140 	v.copy(i);
141 
142 	auto c = i.crop(0, 0, 1, 1);
143 	v.copy(c);
144 }
145 
146 alias ElementViewImage(R) = ViewImage!(ElementType!R);
147 
148 /// Splice multiple images horizontally.
149 auto hjoin(R, TARGET)(R images, auto ref TARGET target)
150 	if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET)
151 {
152 	int w, h;
153 	foreach (ref image; images)
154 		w += image.w,
155 		h = max(h, image.h);
156 	target.size(w, h);
157 	int x;
158 	foreach (ref image; images)
159 		image.blitTo(target, x, 0),
160 		x += image.w;
161 	return target;
162 }
163 /// ditto
164 auto hjoin(R)(R images)
165 	if (isInputRange!R && isView!(ElementType!R))
166 {
167 	ElementViewImage!R target;
168 	return images.hjoin(target);
169 }
170 
171 /// Splice multiple images vertically.
172 auto vjoin(R, TARGET)(R images, auto ref TARGET target)
173 	if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET)
174 {
175 	int w, h;
176 	foreach (ref image; images)
177 		w = max(w, image.w),
178 		h += image.h;
179 	target.size(w, h);
180 	int y;
181 	foreach (ref image; images)
182 		image.blitTo(target, 0, y),
183 		y += image.h;
184 	return target;
185 }
186 /// ditto
187 auto vjoin(R)(R images)
188 	if (isInputRange!R && isView!(ElementType!R))
189 {
190 	ElementViewImage!R target;
191 	return images.vjoin(target);
192 }
193 
194 unittest
195 {
196 	auto h = 10
197 		.iota
198 		.retro
199 		.map!onePixel
200 		.retro
201 		.hjoin();
202 
203 	foreach (i; 0..10)
204 		assert(h[i, 0] == i);
205 
206 	auto v = 10.iota.map!onePixel.vjoin();
207 	foreach (i; 0..10)
208 		assert(v[0, i] == i);
209 }
210 
211 // ***************************************************************************
212 
213 /// Performs linear downscale by a constant factor
214 template downscale(int HRX, int HRY=HRX)
215 {
216 	auto downscale(SRC, TARGET)(auto ref SRC src, auto ref TARGET target)
217 		if (isDirectView!SRC && isWritableView!TARGET)
218 	{
219 		alias lr = target;
220 		alias hr = src;
221 
222 		assert(hr.w % HRX == 0 && hr.h % HRY == 0, "Size mismatch");
223 
224 		lr.size(hr.w / HRX, hr.h / HRY);
225 
226 		foreach (y; 0..lr.h)
227 			foreach (x; 0..lr.w)
228 			{
229 				static if (HRX*HRY <= 0x100)
230 					enum EXPAND_BYTES = 1;
231 				else
232 				static if (HRX*HRY <= 0x10000)
233 					enum EXPAND_BYTES = 2;
234 				else
235 					static assert(0);
236 				static if (is(typeof(COLOR.init.a))) // downscale with alpha
237 				{
238 					ExpandType!(COLOR, EXPAND_BYTES+COLOR.init.a.sizeof) sum;
239 					ExpandType!(typeof(COLOR.init.a), EXPAND_BYTES) alphaSum;
240 					auto start = y*HRY*hr.stride + x*HRX;
241 					foreach (j; 0..HRY)
242 					{
243 						foreach (p; hr.pixels[start..start+HRX])
244 						{
245 							foreach (i, f; p.tupleof)
246 								static if (p.tupleof[i].stringof != "p.a")
247 								{
248 									enum FIELD = p.tupleof[i].stringof[2..$];
249 									mixin("sum."~FIELD~" += cast(typeof(sum."~FIELD~"))p."~FIELD~" * p.a;");
250 								}
251 							alphaSum += p.a;
252 						}
253 						start += hr.stride;
254 					}
255 					if (alphaSum)
256 					{
257 						auto result = cast(COLOR)(sum / alphaSum);
258 						result.a = cast(typeof(result.a))(alphaSum / (HRX*HRY));
259 						lr[x, y] = result;
260 					}
261 					else
262 					{
263 						static assert(COLOR.init.a == 0);
264 						lr[x, y] = COLOR.init;
265 					}
266 				}
267 				else
268 				{
269 					ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum;
270 					auto x0 = x*HRX;
271 					auto x1 = x0+HRX;
272 					foreach (j; y*HRY..(y+1)*HRY)
273 						foreach (p; hr.scanline(j)[x0..x1])
274 							sum += p;
275 					lr[x, y] = cast(ViewColor!SRC)(sum / (HRX*HRY));
276 				}
277 			}
278 
279 		return target;
280 	}
281 
282 	auto downscale(SRC)(auto ref SRC src)
283 		if (isView!SRC)
284 	{
285 		ViewImage!SRC target;
286 		return src.downscale(target);
287 	}
288 }
289 
290 unittest
291 {
292 	onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)();
293 }
294 
295 // ***************************************************************************
296 
297 /// Copy the indicated row of src to a COLOR buffer.
298 void copyScanline(SRC, COLOR)(auto ref SRC src, int y, COLOR[] dst)
299 	if (isView!SRC && is(COLOR == ViewColor!SRC))
300 {
301 	static if (isDirectView!SRC)
302 		dst[] = src.scanline(y)[];
303 	else
304 	{
305 		assert(src.w == dst.length);
306 		foreach (x; 0..src.w)
307 			dst[x] = src[x, y];
308 	}
309 }
310 
311 /// Copy a view's pixels (top-to-bottom) to a COLOR buffer.
312 void copyPixels(SRC, COLOR)(auto ref SRC src, COLOR[] dst)
313 	if (isView!SRC && is(COLOR == ViewColor!SRC))
314 {
315 	assert(dst.length == src.w * src.h);
316 	foreach (y; 0..src.h)
317 		src.copyScanline(y, dst[y*src.w..(y+1)*src.w]);
318 }
319 
320 // ***************************************************************************
321 
322 import std.traits;
323 
324 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433
325 
326 struct InputColor {}
327 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR);
328 
329 struct TargetColor {}
330 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET);
331 
332 // ***************************************************************************
333 
334 import ae.utils.graphics.color;
335 import ae.utils.meta : structFields;
336 
337 private string[] readPBMHeader(ref const(ubyte)[] data)
338 {
339 	import std.ascii;
340 
341 	string[] fields;
342 	uint wordStart = 0;
343 	uint p;
344 	for (p=1; p<data.length && fields.length<4; p++)
345 		if (!isWhite(data[p-1]) && isWhite(data[p]))
346 			fields ~= cast(string)data[wordStart..p];
347 		else
348 		if (isWhite(data[p-1]) && !isWhite(data[p]))
349 			wordStart = p;
350 	data = data[p..$];
351 	enforce(fields.length==4, "Header too short");
352 	enforce(fields[0].length==2 && fields[0][0]=='P', "Invalid signature");
353 	return fields;
354 }
355 
356 private template PBMSignature(COLOR)
357 {
358 	static if (structFields!COLOR == ["l"])
359 		enum PBMSignature = "P5";
360 	else
361 	static if (structFields!COLOR == ["r", "g", "b"])
362 		enum PBMSignature = "P6";
363 	else
364 		static assert(false, "Unsupported PBM color: " ~
365 			__traits(allMembers, COLOR.Fields).stringof);
366 }
367 
368 /// Parses a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
369 auto parsePBM(C = TargetColor, TARGET)(const(void)[] vdata, auto ref TARGET target)
370 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
371 {
372 	alias COLOR = ViewColor!TARGET;
373 
374 	auto data = cast(const(ubyte)[])vdata;
375 	string[] fields = readPBMHeader(data);
376 	enforce(fields[0]==PBMSignature!COLOR, "Invalid signature");
377 	enforce(to!uint(fields[3]) == COLOR.tupleof[0].max, "Channel depth mismatch");
378 
379 	target.size(to!uint(fields[1]), to!uint(fields[2]));
380 	enforce(data.length / COLOR.sizeof == target.w * target.h,
381 		"Dimension / filesize mismatch");
382 	target.pixels[] = cast(COLOR[])data;
383 
384 	static if (COLOR.tupleof[0].sizeof > 1)
385 		foreach (ref pixel; pixels)
386 			pixel = COLOR.op!q{swapBytes(a)}(pixel); // TODO: proper endianness support
387 
388 	return target;
389 }
390 /// ditto
391 auto parsePBM(COLOR)(const(void)[] vdata)
392 {
393 	Image!COLOR target;
394 	return vdata.parsePBM(target);
395 }
396 
397 unittest
398 {
399 	import std.conv : hexString;
400 	auto data = "P6\n2\n2\n255\n" ~
401 		hexString!"000000 FFF000" ~
402 		hexString!"000FFF FFFFFF";
403 	auto i = data.parsePBM!RGB();
404 	assert(i[0, 0] == RGB.fromHex("000000"));
405 	assert(i[0, 1] == RGB.fromHex("000FFF"));
406 }
407 
408 unittest
409 {
410 	import std.conv : hexString;
411 	auto data = "P5\n2\n2\n255\n" ~
412 		hexString!"00 55" ~
413 		hexString!"AA FF";
414 	auto i = data.parsePBM!L8();
415 	assert(i[0, 0] == L8(0x00));
416 	assert(i[0, 1] == L8(0xAA));
417 }
418 
419 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
420 ubyte[] toPBM(SRC)(auto ref SRC src)
421 	if (isView!SRC)
422 {
423 	alias COLOR = ViewColor!SRC;
424 
425 	auto length = src.w * src.h;
426 	ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n"
427 		.format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max);
428 	ubyte[] data = new ubyte[header.length + length * COLOR.sizeof];
429 
430 	data[0..header.length] = header;
431 	src.copyPixels(cast(COLOR[])data[header.length..$]);
432 
433 	static if (ChannelType!COLOR.sizeof > 1)
434 		foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$])
435 			p = swapBytes(p); // TODO: proper endianness support
436 
437 	return data;
438 }
439 
440 unittest
441 {
442 	import std.conv : hexString;
443 	assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ hexString!"01 02 03");
444 	assert(onePixel(L8 (1)    ).toPBM == "P5\n1 1 255\n" ~ hexString!"01"      );
445 }
446 
447 // ***************************************************************************
448 
449 /// Loads a raw COLOR[] into an image of the indicated size.
450 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h,
451 		auto ref TARGET target)
452 	if (isWritableView!TARGET
453 	 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET))
454 {
455 	alias COLOR = ViewColor!TARGET;
456 
457 	auto pixels = cast(COLOR[])input;
458 	enforce(pixels.length == w*h, "Dimension / filesize mismatch");
459 	target.size(w, h);
460 	target.pixels[] = pixels;
461 	return target;
462 }
463 
464 /// ditto
465 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h)
466 {
467 	alias COLOR = GetInputColor!(C, INPUT);
468 	Image!COLOR target;
469 	return fromPixels!COLOR(input, w, h, target);
470 }
471 
472 unittest
473 {
474 	import std.conv : hexString;
475 	Image!L8 i;
476 	i = hexString!"42".fromPixels!L8(1, 1);
477 	i = hexString!"42".fromPixels!L8(1, 1, i);
478 	assert(i[0, 0].l == 0x42);
479 	i = (cast(L8[])hexString!"42").fromPixels(1, 1);
480 	i = (cast(L8[])hexString!"42").fromPixels(1, 1, i);
481 }
482 
483 // ***************************************************************************
484 
485 static import ae.utils.graphics.bitmap;
486 
487 template bitmapBitCount(COLOR)
488 {
489 	static if (is(COLOR == BGR))
490 		enum bitmapBitCount = 24;
491 	else
492 	static if (is(COLOR == BGRX) || is(COLOR == BGRA))
493 		enum bitmapBitCount = 32;
494 	else
495 	static if (is(COLOR == L8))
496 		enum bitmapBitCount = 8;
497 	else
498 		static assert(false, "Unsupported BMP color type: " ~ COLOR.stringof);
499 }
500 
501 @property int bitmapPixelStride(COLOR)(int w)
502 {
503 	int pixelStride = w * cast(uint)COLOR.sizeof;
504 	pixelStride = (pixelStride+3) & ~3;
505 	return pixelStride;
506 }
507 
508 /// Parses a Windows bitmap (.bmp) file.
509 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target)
510 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
511 {
512 	alias COLOR = ViewColor!TARGET;
513 
514 	import ae.utils.graphics.bitmap;
515 	alias BitmapHeader!3 Header;
516 	enforce(data.length > Header.sizeof);
517 	Header* header = cast(Header*) data.ptr;
518 	enforce(header.bfType == "BM", "Invalid signature");
519 	enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)"
520 		.format(header.bfSize, data.length));
521 	enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof);
522 
523 	auto w = header.bcWidth;
524 	auto h = header.bcHeight;
525 	enforce(header.bcPlanes==1, "Multiplane BMPs not supported");
526 
527 	enforce(header.bcBitCount == bitmapBitCount!COLOR,
528 		"Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image"
529 		.format(header.bcBitCount, bitmapBitCount!COLOR));
530 
531 	auto pixelData = data[header.bfOffBits..$];
532 	auto pixelStride = bitmapPixelStride!COLOR(w);
533 	size_t pos = 0;
534 
535 	if (h < 0)
536 		h = -h;
537 	else
538 	{
539 		pos = pixelStride*(h-1);
540 		pixelStride = -pixelStride;
541 	}
542 
543 	target.size(w, h);
544 	foreach (y; 0..h)
545 	{
546 		target.scanline(y)[] = (cast(COLOR*)(pixelData.ptr+pos))[0..w];
547 		pos += pixelStride;
548 	}
549 
550 	return target;
551 }
552 /// ditto
553 auto parseBMP(COLOR)(const(void)[] data)
554 {
555 	Image!COLOR target;
556 	return data.parseBMP(target);
557 }
558 
559 unittest
560 {
561 	alias parseBMP!BGR parseBMP24;
562 }
563 
564 /// Creates a Windows bitmap (.bmp) file.
565 ubyte[] toBMP(SRC)(auto ref SRC src)
566 	if (isView!SRC)
567 {
568 	alias COLOR = ViewColor!SRC;
569 
570 	import ae.utils.graphics.bitmap;
571 	static if (COLOR.sizeof > 3)
572 		alias BitmapHeader!4 Header;
573 	else
574 		alias BitmapHeader!3 Header;
575 
576 	auto pixelStride = bitmapPixelStride!COLOR(src.w);
577 	auto bitmapDataSize = src.h * pixelStride;
578 	ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize];
579 	auto header = cast(Header*)data.ptr;
580 	*header = Header.init;
581 	header.bfSize = to!uint(data.length);
582 	header.bfOffBits = Header.sizeof;
583 	header.bcWidth = src.w;
584 	header.bcHeight = -src.h;
585 	header.bcPlanes = 1;
586 	header.biSizeImage = bitmapDataSize;
587 	header.bcBitCount = bitmapBitCount!COLOR;
588 
589 	static if (header.VERSION >= 4)
590 	{
591 		header.biCompression = BI_BITFIELDS;
592 
593 		COLOR c;
594 		foreach (i, f; c.tupleof)
595 		{
596 			enum CHAN = c.tupleof[i].stringof[2..$];
597 			enum MASK = (cast(uint)typeof(c.tupleof[i]).max) << (c.tupleof[i].offsetof*8);
598 			static if (CHAN=="r")
599 				header.bV4RedMask   |= MASK;
600 			else
601 			static if (CHAN=="g")
602 				header.bV4GreenMask |= MASK;
603 			else
604 			static if (CHAN=="b")
605 				header.bV4BlueMask  |= MASK;
606 			else
607 			static if (CHAN=="a")
608 				header.bV4AlphaMask |= MASK;
609 		}
610 	}
611 
612 	auto pixelData = data[header.bfOffBits..$];
613 	auto ptr = pixelData.ptr;
614 	size_t pos = 0;
615 
616 	foreach (y; 0..src.h)
617 	{
618 		src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]);
619 		ptr += pixelStride;
620 	}
621 
622 	return data;
623 }
624 
625 unittest
626 {
627 	Image!BGR output;
628 	onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output);
629 }
630 
631 // ***************************************************************************
632 
633 private // https://issues.dlang.org/show_bug.cgi?id=16563
634 {
635 	struct PNGChunk
636 	{
637 		char[4] type;
638 		const(void)[] data;
639 
640 		uint crc32()
641 		{
642 			import std.digest.crc;
643 			CRC32 crc;
644 			crc.put(cast(ubyte[])(type[]));
645 			crc.put(cast(ubyte[])data);
646 			ubyte[4] hash = crc.finish();
647 			return *cast(uint*)hash.ptr;
648 		}
649 
650 		this(string type, const(void)[] data)
651 		{
652 			this.type[] = type[];
653 			this.data = data;
654 		}
655 	}
656 
657 	enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 }
658 	enum PNGCompressionMethod : ubyte { DEFLATE }
659 	enum PNGFilterMethod : ubyte { ADAPTIVE }
660 	enum PNGInterlaceMethod : ubyte { NONE, ADAM7 }
661 
662 	enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH }
663 
664 	align(1)
665 	struct PNGHeader
666 	{
667 	align(1):
668 		uint width, height;
669 		ubyte colourDepth;
670 		PNGColourType colourType;
671 		PNGCompressionMethod compressionMethod;
672 		PNGFilterMethod filterMethod;
673 		PNGInterlaceMethod interlaceMethod;
674 		static assert(PNGHeader.sizeof == 13);
675 	}
676 }
677 
678 /// Creates a PNG file.
679 /// Only basic PNG features are supported
680 /// (no filters, interlacing, palettes etc.)
681 ubyte[] toPNG(SRC)(auto ref SRC src, int compressionLevel = 5)
682 	if (isView!SRC)
683 {
684 	import std.zlib : compress;
685 	import ae.utils.math : swapBytes; // TODO: proper endianness support
686 
687 	enum : ulong { SIGNATURE = 0x0a1a0a0d474e5089 }
688 
689 	alias COLOR = ViewColor!SRC;
690 	static if (!is(COLOR == struct))
691 		enum COLOUR_TYPE = PNGColourType.G;
692 	else
693 	static if (structFields!COLOR == ["l"])
694 		enum COLOUR_TYPE = PNGColourType.G;
695 	else
696 	static if (structFields!COLOR == ["r","g","b"])
697 		enum COLOUR_TYPE = PNGColourType.RGB;
698 	else
699 	static if (structFields!COLOR == ["l","a"])
700 		enum COLOUR_TYPE = PNGColourType.GA;
701 	else
702 	static if (structFields!COLOR == ["r","g","b","a"])
703 		enum COLOUR_TYPE = PNGColourType.RGBA;
704 	else
705 		static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof);
706 
707 	PNGChunk[] chunks;
708 	PNGHeader header = {
709 		width : swapBytes(src.w),
710 		height : swapBytes(src.h),
711 		colourDepth : ChannelType!COLOR.sizeof * 8,
712 		colourType : COLOUR_TYPE,
713 		compressionMethod : PNGCompressionMethod.DEFLATE,
714 		filterMethod : PNGFilterMethod.ADAPTIVE,
715 		interlaceMethod : PNGInterlaceMethod.NONE,
716 	};
717 	chunks ~= PNGChunk("IHDR", cast(void[])[header]);
718 	uint idatStride = to!uint(src.w * COLOR.sizeof+1);
719 	ubyte[] idatData = new ubyte[src.h * idatStride];
720 	for (uint y=0; y<src.h; y++)
721 	{
722 		idatData[y*idatStride] = PNGFilterAdaptive.NONE;
723 		auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride];
724 		src.copyScanline(y, rowPixels);
725 
726 		static if (ChannelType!COLOR.sizeof > 1)
727 			foreach (ref p; cast(ChannelType!COLOR[])rowPixels)
728 				p = swapBytes(p);
729 	}
730 	chunks ~= PNGChunk("IDAT", compress(idatData, compressionLevel));
731 	chunks ~= PNGChunk("IEND", null);
732 
733 	uint totalSize = 8;
734 	foreach (chunk; chunks)
735 		totalSize += 8 + chunk.data.length + 4;
736 	ubyte[] data = new ubyte[totalSize];
737 
738 	*cast(ulong*)data.ptr = SIGNATURE;
739 	uint pos = 8;
740 	foreach(chunk;chunks)
741 	{
742 		uint i = pos;
743 		uint chunkLength = to!uint(chunk.data.length);
744 		pos += 12 + chunkLength;
745 		*cast(uint*)&data[i] = swapBytes(chunkLength);
746 		(cast(char[])data[i+4 .. i+8])[] = chunk.type[];
747 		data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[];
748 		*cast(uint*)&data[i+8+chunk.data.length] = swapBytes(chunk.crc32());
749 		assert(pos == i+12+chunk.data.length);
750 	}
751 
752 	return data;
753 }
754 
755 unittest
756 {
757 	onePixel(RGB(1,2,3)).toPNG();
758 	onePixel(5).toPNG();
759 }