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 	auto data = "P6\n2\n2\n255\n" ~
400 		x"000000 FFF000" ~
401 		x"000FFF FFFFFF";
402 	auto i = data.parsePBM!RGB();
403 	assert(i[0, 0] == RGB.fromHex("000000"));
404 	assert(i[0, 1] == RGB.fromHex("000FFF"));
405 }
406 
407 unittest
408 {
409 	auto data = "P5\n2\n2\n255\n" ~
410 		x"00 55" ~
411 		x"AA FF";
412 	auto i = data.parsePBM!L8();
413 	assert(i[0, 0] == L8(0x00));
414 	assert(i[0, 1] == L8(0xAA));
415 }
416 
417 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
418 ubyte[] toPBM(SRC)(auto ref SRC src)
419 	if (isView!SRC)
420 {
421 	alias COLOR = ViewColor!SRC;
422 
423 	auto length = src.w * src.h;
424 	ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n"
425 		.format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max);
426 	ubyte[] data = new ubyte[header.length + length * COLOR.sizeof];
427 
428 	data[0..header.length] = header;
429 	src.copyPixels(cast(COLOR[])data[header.length..$]);
430 
431 	static if (ChannelType!COLOR.sizeof > 1)
432 		foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$])
433 			p = swapBytes(p); // TODO: proper endianness support
434 
435 	return data;
436 }
437 
438 unittest
439 {
440 	assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ x"01 02 03");
441 	assert(onePixel(L8 (1)    ).toPBM == "P5\n1 1 255\n" ~ x"01"      );
442 }
443 
444 // ***************************************************************************
445 
446 /// Loads a raw COLOR[] into an image of the indicated size.
447 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h,
448 		auto ref TARGET target)
449 	if (isWritableView!TARGET
450 	 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET))
451 {
452 	alias COLOR = ViewColor!TARGET;
453 
454 	auto pixels = cast(COLOR[])input;
455 	enforce(pixels.length == w*h, "Dimension / filesize mismatch");
456 	target.size(w, h);
457 	target.pixels[] = pixels;
458 	return target;
459 }
460 
461 /// ditto
462 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h)
463 {
464 	alias COLOR = GetInputColor!(C, INPUT);
465 	Image!COLOR target;
466 	return fromPixels!COLOR(input, w, h, target);
467 }
468 
469 unittest
470 {
471 	Image!L8 i;
472 	i = x"42".fromPixels!L8(1, 1);
473 	i = x"42".fromPixels!L8(1, 1, i);
474 	assert(i[0, 0].l == 0x42);
475 	i = (cast(L8[])x"42").fromPixels(1, 1);
476 	i = (cast(L8[])x"42").fromPixels(1, 1, i);
477 }
478 
479 // ***************************************************************************
480 
481 static import ae.utils.graphics.bitmap;
482 
483 template bitmapBitCount(COLOR)
484 {
485 	static if (is(COLOR == BGR))
486 		enum bitmapBitCount = 24;
487 	else
488 	static if (is(COLOR == BGRX) || is(COLOR == BGRA))
489 		enum bitmapBitCount = 32;
490 	else
491 	static if (is(COLOR == L8))
492 		enum bitmapBitCount = 8;
493 	else
494 		static assert(false, "Unsupported BMP color type: " ~ COLOR.stringof);
495 }
496 
497 @property int bitmapPixelStride(COLOR)(int w)
498 {
499 	int pixelStride = w * cast(uint)COLOR.sizeof;
500 	pixelStride = (pixelStride+3) & ~3;
501 	return pixelStride;
502 }
503 
504 /// Parses a Windows bitmap (.bmp) file.
505 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target)
506 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
507 {
508 	alias COLOR = ViewColor!TARGET;
509 
510 	import ae.utils.graphics.bitmap;
511 	alias BitmapHeader!3 Header;
512 	enforce(data.length > Header.sizeof);
513 	Header* header = cast(Header*) data.ptr;
514 	enforce(header.bfType == "BM", "Invalid signature");
515 	enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)"
516 		.format(header.bfSize, data.length));
517 	enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof);
518 
519 	auto w = header.bcWidth;
520 	auto h = header.bcHeight;
521 	enforce(header.bcPlanes==1, "Multiplane BMPs not supported");
522 
523 	enforce(header.bcBitCount == bitmapBitCount!COLOR,
524 		"Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image"
525 		.format(header.bcBitCount, bitmapBitCount!COLOR));
526 
527 	auto pixelData = data[header.bfOffBits..$];
528 	auto pixelStride = bitmapPixelStride!COLOR(w);
529 	size_t pos = 0;
530 
531 	if (h < 0)
532 		h = -h;
533 	else
534 	{
535 		pos = pixelStride*(h-1);
536 		pixelStride = -pixelStride;
537 	}
538 
539 	target.size(w, h);
540 	foreach (y; 0..h)
541 	{
542 		target.scanline(y)[] = (cast(COLOR*)(pixelData.ptr+pos))[0..w];
543 		pos += pixelStride;
544 	}
545 
546 	return target;
547 }
548 /// ditto
549 auto parseBMP(COLOR)(const(void)[] data)
550 {
551 	Image!COLOR target;
552 	return data.parseBMP(target);
553 }
554 
555 unittest
556 {
557 	alias parseBMP!BGR parseBMP24;
558 }
559 
560 /// Creates a Windows bitmap (.bmp) file.
561 ubyte[] toBMP(SRC)(auto ref SRC src)
562 	if (isView!SRC)
563 {
564 	alias COLOR = ViewColor!SRC;
565 
566 	import ae.utils.graphics.bitmap;
567 	static if (COLOR.sizeof > 3)
568 		alias BitmapHeader!4 Header;
569 	else
570 		alias BitmapHeader!3 Header;
571 
572 	auto pixelStride = bitmapPixelStride!COLOR(src.w);
573 	auto bitmapDataSize = src.h * pixelStride;
574 	ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize];
575 	auto header = cast(Header*)data.ptr;
576 	*header = Header.init;
577 	header.bfSize = to!uint(data.length);
578 	header.bfOffBits = Header.sizeof;
579 	header.bcWidth = src.w;
580 	header.bcHeight = -src.h;
581 	header.bcPlanes = 1;
582 	header.biSizeImage = bitmapDataSize;
583 	header.bcBitCount = bitmapBitCount!COLOR;
584 
585 	static if (header.VERSION >= 4)
586 	{
587 		header.biCompression = BI_BITFIELDS;
588 
589 		COLOR c;
590 		foreach (i, f; c.tupleof)
591 		{
592 			enum CHAN = c.tupleof[i].stringof[2..$];
593 			enum MASK = (cast(uint)typeof(c.tupleof[i]).max) << (c.tupleof[i].offsetof*8);
594 			static if (CHAN=="r")
595 				header.bV4RedMask   |= MASK;
596 			else
597 			static if (CHAN=="g")
598 				header.bV4GreenMask |= MASK;
599 			else
600 			static if (CHAN=="b")
601 				header.bV4BlueMask  |= MASK;
602 			else
603 			static if (CHAN=="a")
604 				header.bV4AlphaMask |= MASK;
605 		}
606 	}
607 
608 	auto pixelData = data[header.bfOffBits..$];
609 	auto ptr = pixelData.ptr;
610 	size_t pos = 0;
611 
612 	foreach (y; 0..src.h)
613 	{
614 		src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]);
615 		ptr += pixelStride;
616 	}
617 
618 	return data;
619 }
620 
621 unittest
622 {
623 	Image!BGR output;
624 	onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output);
625 }
626 
627 // ***************************************************************************
628 
629 private // https://issues.dlang.org/show_bug.cgi?id=16563
630 {
631 	struct PNGChunk
632 	{
633 		char[4] type;
634 		const(void)[] data;
635 
636 		uint crc32()
637 		{
638 			import std.digest.crc;
639 			CRC32 crc;
640 			crc.put(cast(ubyte[])(type[]));
641 			crc.put(cast(ubyte[])data);
642 			ubyte[4] hash = crc.finish();
643 			return *cast(uint*)hash.ptr;
644 		}
645 
646 		this(string type, const(void)[] data)
647 		{
648 			this.type[] = type[];
649 			this.data = data;
650 		}
651 	}
652 
653 	enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 }
654 	enum PNGCompressionMethod : ubyte { DEFLATE }
655 	enum PNGFilterMethod : ubyte { ADAPTIVE }
656 	enum PNGInterlaceMethod : ubyte { NONE, ADAM7 }
657 
658 	enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH }
659 
660 	align(1)
661 	struct PNGHeader
662 	{
663 	align(1):
664 		uint width, height;
665 		ubyte colourDepth;
666 		PNGColourType colourType;
667 		PNGCompressionMethod compressionMethod;
668 		PNGFilterMethod filterMethod;
669 		PNGInterlaceMethod interlaceMethod;
670 		static assert(PNGHeader.sizeof == 13);
671 	}
672 }
673 
674 /// Creates a PNG file.
675 /// Only basic PNG features are supported
676 /// (no filters, interlacing, palettes etc.)
677 ubyte[] toPNG(SRC)(auto ref SRC src)
678 	if (isView!SRC)
679 {
680 	import std.zlib : compress;
681 	import ae.utils.math : swapBytes; // TODO: proper endianness support
682 
683 	enum : ulong { SIGNATURE = 0x0a1a0a0d474e5089 }
684 
685 	alias COLOR = ViewColor!SRC;
686 	static if (!is(COLOR == struct))
687 		enum COLOUR_TYPE = PNGColourType.G;
688 	else
689 	static if (structFields!COLOR == ["l"])
690 		enum COLOUR_TYPE = PNGColourType.G;
691 	else
692 	static if (structFields!COLOR == ["r","g","b"])
693 		enum COLOUR_TYPE = PNGColourType.RGB;
694 	else
695 	static if (structFields!COLOR == ["l","a"])
696 		enum COLOUR_TYPE = PNGColourType.GA;
697 	else
698 	static if (structFields!COLOR == ["r","g","b","a"])
699 		enum COLOUR_TYPE = PNGColourType.RGBA;
700 	else
701 		static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof);
702 
703 	PNGChunk[] chunks;
704 	PNGHeader header = {
705 		width : swapBytes(src.w),
706 		height : swapBytes(src.h),
707 		colourDepth : ChannelType!COLOR.sizeof * 8,
708 		colourType : COLOUR_TYPE,
709 		compressionMethod : PNGCompressionMethod.DEFLATE,
710 		filterMethod : PNGFilterMethod.ADAPTIVE,
711 		interlaceMethod : PNGInterlaceMethod.NONE,
712 	};
713 	chunks ~= PNGChunk("IHDR", cast(void[])[header]);
714 	uint idatStride = to!uint(src.w * COLOR.sizeof+1);
715 	ubyte[] idatData = new ubyte[src.h * idatStride];
716 	for (uint y=0; y<src.h; y++)
717 	{
718 		idatData[y*idatStride] = PNGFilterAdaptive.NONE;
719 		auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride];
720 		src.copyScanline(y, rowPixels);
721 
722 		static if (ChannelType!COLOR.sizeof > 1)
723 			foreach (ref p; cast(ChannelType!COLOR[])rowPixels)
724 				p = swapBytes(p);
725 	}
726 	chunks ~= PNGChunk("IDAT", compress(idatData, 5));
727 	chunks ~= PNGChunk("IEND", null);
728 
729 	uint totalSize = 8;
730 	foreach (chunk; chunks)
731 		totalSize += 8 + chunk.data.length + 4;
732 	ubyte[] data = new ubyte[totalSize];
733 
734 	*cast(ulong*)data.ptr = SIGNATURE;
735 	uint pos = 8;
736 	foreach(chunk;chunks)
737 	{
738 		uint i = pos;
739 		uint chunkLength = to!uint(chunk.data.length);
740 		pos += 12 + chunkLength;
741 		*cast(uint*)&data[i] = swapBytes(chunkLength);
742 		(cast(char[])data[i+4 .. i+8])[] = chunk.type[];
743 		data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[];
744 		*cast(uint*)&data[i+8+chunk.data.length] = swapBytes(chunk.crc32());
745 		assert(pos == i+12+chunk.data.length);
746 	}
747 
748 	return data;
749 }
750 
751 unittest
752 {
753 	onePixel(RGB(1,2,3)).toPNG();
754 	onePixel(5).toPNG();
755 }