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