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 	inout(COLOR)[] scanline(int y) inout
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 	inout(COLOR)[] scanline(int y) inout
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 		alias COLOR = ViewColor!SRC;
222 
223 		assert(hr.w % HRX == 0 && hr.h % HRY == 0, "Size mismatch");
224 
225 		lr.size(hr.w / HRX, hr.h / HRY);
226 
227 		foreach (y; 0..lr.h)
228 			foreach (x; 0..lr.w)
229 			{
230 				static if (HRX*HRY <= 0x100)
231 					enum EXPAND_BYTES = 1;
232 				else
233 				static if (HRX*HRY <= 0x10000)
234 					enum EXPAND_BYTES = 2;
235 				else
236 					static assert(0);
237 				static if (is(typeof(COLOR.init.a))) // downscale with alpha
238 				{
239 					version (none) // TODO: broken
240 					{
241 						ExpandChannelType!(COLOR, EXPAND_BYTES+COLOR.init.a.sizeof) sum;
242 						ExpandChannelType!(typeof(COLOR.init.a), EXPAND_BYTES) alphaSum;
243 						auto start = y*HRY*hr.stride + x*HRX;
244 						foreach (j; 0..HRY)
245 						{
246 							foreach (p; hr.pixels[start..start+HRX])
247 							{
248 								foreach (i, f; p.tupleof)
249 									static if (p.tupleof[i].stringof != "p.a")
250 									{
251 										enum FIELD = p.tupleof[i].stringof[2..$];
252 										mixin("sum."~FIELD~" += cast(typeof(sum."~FIELD~"))p."~FIELD~" * p.a;");
253 									}
254 								alphaSum += p.a;
255 							}
256 							start += hr.stride;
257 						}
258 						if (alphaSum)
259 						{
260 							auto result = cast(COLOR)(sum / alphaSum);
261 							result.a = cast(typeof(result.a))(alphaSum / (HRX*HRY));
262 							lr[x, y] = result;
263 						}
264 						else
265 						{
266 							static assert(COLOR.init.a == 0);
267 							lr[x, y] = COLOR.init;
268 						}
269 					}
270 					else
271 						static assert(false, "Downscaling with alpha is not implemented");
272 				}
273 				else
274 				{
275 					ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum;
276 					auto x0 = x*HRX;
277 					auto x1 = x0+HRX;
278 					foreach (j; y*HRY..(y+1)*HRY)
279 						foreach (p; hr.scanline(j)[x0..x1])
280 							sum += p;
281 					lr[x, y] = cast(ViewColor!SRC)(sum / (HRX*HRY));
282 				}
283 			}
284 
285 		return target;
286 	}
287 
288 	auto downscale(SRC)(auto ref SRC src)
289 		if (isView!SRC)
290 	{
291 		ViewImage!SRC target;
292 		return src.downscale(target);
293 	}
294 }
295 
296 unittest
297 {
298 	onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)();
299 //	onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)();
300 
301 	Image!ubyte i;
302 	i.size(4, 1);
303 	i.pixels[] = [1, 3, 5, 7];
304 	auto d = i.downscale!(2, 1);
305 	assert(d.pixels == [2, 6]);
306 }
307 
308 // ***************************************************************************
309 
310 /// Downscaling copy (averages colors in source per one pixel in target).
311 auto downscaleTo(SRC, TARGET)(auto ref SRC src, auto ref TARGET target)
312 if (isDirectView!SRC && isWritableView!TARGET)
313 {
314 	alias lr = target;
315 	alias hr = src;
316 	alias COLOR = ViewColor!SRC;
317 
318 	void impl(uint EXPAND_BYTES)()
319 	{
320 		foreach (y; 0..lr.h)
321 			foreach (x; 0..lr.w)
322 			{
323 				static if (is(typeof(COLOR.init.a))) // downscale with alpha
324 					static assert(false, "Downscaling with alpha is not implemented");
325 				else
326 				{
327 					ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum;
328 					auto x0 =  x    * hr.w / lr.w;
329 					auto x1 = (x+1) * hr.w / lr.w;
330 					auto y0 =  y    * hr.h / lr.h;
331 					auto y1 = (y+1) * hr.h / lr.h;
332 
333 					// When upscaling (across one or two axes),
334 					// fall back to nearest neighbor
335 					if (x0 == x1) x1++;
336 					if (y0 == y1) y1++;
337 
338 					foreach (j; y0 .. y1)
339 						foreach (p; hr.scanline(j)[x0 .. x1])
340 							sum += p;
341 					auto area = (x1 - x0) * (y1 - y0);
342 					auto avg = sum / area;
343 					lr[x, y] = cast(ViewColor!SRC)(avg);
344 				}
345 			}
346 	}
347 
348 	auto perPixelArea = (hr.w / lr.w + 1) * (hr.h / lr.h + 1);
349 
350 	if (perPixelArea <= 0x100)
351 		impl!1();
352 	else
353 	if (perPixelArea <= 0x10000)
354 		impl!2();
355 	else
356 	if (perPixelArea <= 0x1000000)
357 		impl!3();
358 	else
359 		assert(false, "Downscaling too much");
360 
361 	return target;
362 }
363 
364 /// Downscales an image to a certain size.
365 auto downscaleTo(SRC)(auto ref SRC src, int w, int h)
366 if (isView!SRC)
367 {
368 	ViewImage!SRC target;
369 	target.size(w, h);
370 	return src.downscaleTo(target);
371 }
372 
373 unittest
374 {
375 	onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2);
376 //	onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2);
377 
378 	Image!ubyte i;
379 	i.size(6, 1);
380 	i.pixels[] = [1, 2, 3, 4, 5, 6];
381 	assert(i.downscaleTo(6, 1).pixels == [1, 2, 3, 4, 5, 6]);
382 	assert(i.downscaleTo(3, 1).pixels == [1, 3, 5]);
383 	assert(i.downscaleTo(2, 1).pixels == [2, 5]);
384 	assert(i.downscaleTo(1, 1).pixels == [3]);
385 
386 	i.size(3, 3);
387 	i.pixels[] = [
388 		1, 2, 3,
389 		4, 5, 6,
390 		7, 8, 9];
391 	assert(i.downscaleTo(2, 2).pixels == [1, 2, 5, 7]);
392 
393 	i.size(1, 1);
394 	i.pixels = [1];
395 	assert(i.downscaleTo(2, 2).pixels == [1, 1, 1, 1]);
396 }
397 
398 // ***************************************************************************
399 
400 /// Copy the indicated row of src to a COLOR buffer.
401 void copyScanline(SRC, COLOR)(auto ref SRC src, int y, COLOR[] dst)
402 	if (isView!SRC && is(COLOR == ViewColor!SRC))
403 {
404 	static if (isDirectView!SRC)
405 		dst[] = src.scanline(y)[];
406 	else
407 	{
408 		assert(src.w == dst.length);
409 		foreach (x; 0..src.w)
410 			dst[x] = src[x, y];
411 	}
412 }
413 
414 /// Copy a view's pixels (top-to-bottom) to a COLOR buffer.
415 void copyPixels(SRC, COLOR)(auto ref SRC src, COLOR[] dst)
416 	if (isView!SRC && is(COLOR == ViewColor!SRC))
417 {
418 	assert(dst.length == src.w * src.h);
419 	foreach (y; 0..src.h)
420 		src.copyScanline(y, dst[y*src.w..(y+1)*src.w]);
421 }
422 
423 // ***************************************************************************
424 
425 import std.traits;
426 
427 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433
428 
429 struct InputColor {}
430 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR);
431 
432 struct TargetColor {}
433 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET);
434 
435 // ***************************************************************************
436 
437 import ae.utils.graphics.color;
438 import ae.utils.meta : structFields;
439 
440 private string[] readPBMHeader(ref const(ubyte)[] data)
441 {
442 	import std.ascii;
443 
444 	string[] fields;
445 	uint wordStart = 0;
446 	uint p;
447 	for (p=1; p<data.length && fields.length<4; p++)
448 		if (!isWhite(data[p-1]) && isWhite(data[p]))
449 			fields ~= cast(string)data[wordStart..p];
450 		else
451 		if (isWhite(data[p-1]) && !isWhite(data[p]))
452 			wordStart = p;
453 	data = data[p..$];
454 	enforce(fields.length==4, "Header too short");
455 	enforce(fields[0].length==2 && fields[0][0]=='P', "Invalid signature");
456 	return fields;
457 }
458 
459 private template PBMSignature(COLOR)
460 {
461 	static if (structFields!COLOR == ["l"])
462 		enum PBMSignature = "P5";
463 	else
464 	static if (structFields!COLOR == ["r", "g", "b"])
465 		enum PBMSignature = "P6";
466 	else
467 		static assert(false, "Unsupported PBM color: " ~
468 			__traits(allMembers, COLOR.Fields).stringof);
469 }
470 
471 /// Parses a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
472 auto parsePBM(C = TargetColor, TARGET)(const(void)[] vdata, auto ref TARGET target)
473 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
474 {
475 	alias COLOR = ViewColor!TARGET;
476 
477 	auto data = cast(const(ubyte)[])vdata;
478 	string[] fields = readPBMHeader(data);
479 	enforce(fields[0]==PBMSignature!COLOR, "Invalid signature");
480 	enforce(to!uint(fields[3]) == COLOR.tupleof[0].max, "Channel depth mismatch");
481 
482 	target.size(to!uint(fields[1]), to!uint(fields[2]));
483 	enforce(data.length / COLOR.sizeof == target.w * target.h,
484 		"Dimension / filesize mismatch");
485 	target.pixels[] = cast(COLOR[])data;
486 
487 	static if (COLOR.tupleof[0].sizeof > 1)
488 		foreach (ref pixel; pixels)
489 			pixel = COLOR.op!q{swapBytes(a)}(pixel); // TODO: proper endianness support
490 
491 	return target;
492 }
493 /// ditto
494 auto parsePBM(COLOR)(const(void)[] vdata)
495 {
496 	Image!COLOR target;
497 	return vdata.parsePBM(target);
498 }
499 
500 unittest
501 {
502 	import std.conv : hexString;
503 	auto data = "P6\n2\n2\n255\n" ~
504 		hexString!"000000 FFF000" ~
505 		hexString!"000FFF FFFFFF";
506 	auto i = data.parsePBM!RGB();
507 	assert(i[0, 0] == RGB.fromHex("000000"));
508 	assert(i[0, 1] == RGB.fromHex("000FFF"));
509 }
510 
511 unittest
512 {
513 	import std.conv : hexString;
514 	auto data = "P5\n2\n2\n255\n" ~
515 		hexString!"00 55" ~
516 		hexString!"AA FF";
517 	auto i = data.parsePBM!L8();
518 	assert(i[0, 0] == L8(0x00));
519 	assert(i[0, 1] == L8(0xAA));
520 }
521 
522 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
523 ubyte[] toPBM(SRC)(auto ref SRC src)
524 	if (isView!SRC)
525 {
526 	alias COLOR = ViewColor!SRC;
527 
528 	auto length = src.w * src.h;
529 	ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n"
530 		.format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max);
531 	ubyte[] data = new ubyte[header.length + length * COLOR.sizeof];
532 
533 	data[0..header.length] = header;
534 	src.copyPixels(cast(COLOR[])data[header.length..$]);
535 
536 	static if (ChannelType!COLOR.sizeof > 1)
537 		foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$])
538 			p = swapBytes(p); // TODO: proper endianness support
539 
540 	return data;
541 }
542 
543 unittest
544 {
545 	import std.conv : hexString;
546 	assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ hexString!"01 02 03");
547 	assert(onePixel(L8 (1)    ).toPBM == "P5\n1 1 255\n" ~ hexString!"01"      );
548 }
549 
550 // ***************************************************************************
551 
552 /// Loads a raw COLOR[] into an image of the indicated size.
553 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h,
554 		auto ref TARGET target)
555 	if (isWritableView!TARGET
556 	 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET))
557 {
558 	alias COLOR = ViewColor!TARGET;
559 
560 	auto pixels = cast(COLOR[])input;
561 	enforce(pixels.length == w*h, "Dimension / filesize mismatch");
562 	target.size(w, h);
563 	target.pixels[] = pixels;
564 	return target;
565 }
566 
567 /// ditto
568 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h)
569 {
570 	alias COLOR = GetInputColor!(C, INPUT);
571 	Image!COLOR target;
572 	return fromPixels!COLOR(input, w, h, target);
573 }
574 
575 unittest
576 {
577 	import std.conv : hexString;
578 	Image!L8 i;
579 	i = hexString!"42".fromPixels!L8(1, 1);
580 	i = hexString!"42".fromPixels!L8(1, 1, i);
581 	assert(i[0, 0].l == 0x42);
582 	i = (cast(L8[])hexString!"42").fromPixels(1, 1);
583 	i = (cast(L8[])hexString!"42").fromPixels(1, 1, i);
584 }
585 
586 // ***************************************************************************
587 
588 static import ae.utils.graphics.bitmap;
589 
590 enum bitmapNeedV4Header(COLOR) = !is(COLOR == BGR) && !is(COLOR == BGRX);
591 
592 uint[4] bitmapChannelMasks(COLOR)()
593 {
594 	uint[4] result;
595 	foreach (i, f; COLOR.init.tupleof)
596 	{
597 		enum channelName = __traits(identifier, COLOR.tupleof[i]);
598 		static if (channelName != "x")
599 			static assert((COLOR.tupleof[i].offsetof + COLOR.tupleof[i].sizeof) * 8 <= 32,
600 				"Color " ~ COLOR.stringof ~ " (channel " ~ channelName ~ ") is too large for BMP");
601 
602 		enum MASK = (cast(uint)typeof(COLOR.tupleof[i]).max) << (COLOR.tupleof[i].offsetof*8);
603 		static if (channelName == "r")
604 			result[0] |= MASK;
605 		else
606 		static if (channelName == "g")
607 			result[1] |= MASK;
608 		else
609 		static if (channelName == "b")
610 			result[2] |= MASK;
611 		else
612 		static if (channelName == "a")
613 			result[3] |= MASK;
614 		else
615 		static if (channelName == "l")
616 		{
617 			result[0] |= MASK;
618 			result[1] |= MASK;
619 			result[2] |= MASK;
620 		}
621 		else
622 		static if (channelName == "x")
623 		{
624 		}
625 		else
626 			static assert(false, "Don't know how to encode channelNamenel " ~ channelName);
627 	}
628 	return result;
629 }
630 
631 @property int bitmapPixelStride(COLOR)(int w)
632 {
633 	int pixelStride = w * cast(uint)COLOR.sizeof;
634 	pixelStride = (pixelStride+3) & ~3;
635 	return pixelStride;
636 }
637 
638 /// Returns a view representing a BMP file.
639 /// Does not copy pixel data.
640 auto viewBMP(COLOR, V)(V data)
641 if (is(V : const(void)[]))
642 {
643 	import ae.utils.graphics.bitmap;
644 	alias BitmapHeader!3 Header;
645 	enforce(data.length > Header.sizeof, "Not enough data for header");
646 	Header* header = cast(Header*) data.ptr;
647 	enforce(header.bfType == "BM", "Invalid signature");
648 	enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)"
649 		.format(header.bfSize, data.length));
650 	enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof);
651 
652 	static struct BMP
653 	{
654 		int w, h;
655 		typeof(data.ptr) pixelData;
656 		int pixelStride;
657 
658 		inout(COLOR)[] scanline(int y) inout // TODO constness
659 		{
660 			assert(y >= 0 && y < h, "BMP scanline out of bounds");
661 			return (cast(COLOR*)(pixelData + y * pixelStride))[0..w];
662 		}
663 
664 		mixin DirectView;
665 	}
666 	BMP bmp;
667 
668 	bmp.w = header.bcWidth;
669 	bmp.h = header.bcHeight;
670 	enforce(header.bcPlanes==1, "Multiplane BMPs not supported");
671 
672 	enforce(header.bcBitCount == COLOR.sizeof * 8,
673 		"Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image"
674 		.format(header.bcBitCount, COLOR.sizeof * 8));
675 
676 	static if (bitmapNeedV4Header!COLOR)
677 		enforce(header.VERSION >= 4, "Need a V4+ header to load a %s image".format(COLOR.stringof));
678 	if (header.VERSION >= 4)
679 	{
680 		enforce(data.length > BitmapHeader!4.sizeof, "Not enough data for header");
681 		auto header4 = cast(BitmapHeader!4*) data.ptr;
682 		uint[4] fileMasks = [
683 			header4.bV4RedMask,
684 			header4.bV4GreenMask,
685 			header4.bV4BlueMask,
686 			header4.bV4AlphaMask];
687 		static immutable expectedMasks = bitmapChannelMasks!COLOR();
688 		enforce(fileMasks == expectedMasks,
689 			"Channel format mask mismatch.\nExpected: [%(%32b, %)]\nIn file : [%(%32b, %)]"
690 			.format(expectedMasks, fileMasks));
691 	}
692 
693 	bmp.pixelData = data[header.bfOffBits..$].ptr;
694 	bmp.pixelStride = bitmapPixelStride!COLOR(bmp.w);
695 
696 	if (bmp.h < 0)
697 		bmp.h = -bmp.h;
698 	else
699 	{
700 		bmp.pixelData += bmp.pixelStride * (bmp.h - 1);
701 		bmp.pixelStride = -bmp.pixelStride;
702 	}
703 
704 	return bmp;
705 }
706 
707 /// Parses a Windows bitmap (.bmp) file.
708 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target)
709 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
710 {
711 	alias COLOR = ViewColor!TARGET;
712 	viewBMP!COLOR(data).copy(target);
713 	return target;
714 }
715 /// ditto
716 auto parseBMP(COLOR)(const(void)[] data)
717 {
718 	Image!COLOR target;
719 	return data.parseBMP(target);
720 }
721 
722 unittest
723 {
724 	alias parseBMP!BGR parseBMP24;
725 }
726 
727 /// Creates a Windows bitmap (.bmp) file.
728 ubyte[] toBMP(SRC)(auto ref SRC src)
729 	if (isView!SRC)
730 {
731 	alias COLOR = ViewColor!SRC;
732 
733 	import ae.utils.graphics.bitmap;
734 	static if (bitmapNeedV4Header!COLOR)
735 		alias BitmapHeader!4 Header;
736 	else
737 		alias BitmapHeader!3 Header;
738 
739 	auto pixelStride = bitmapPixelStride!COLOR(src.w);
740 	auto bitmapDataSize = src.h * pixelStride;
741 	ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize];
742 	auto header = cast(Header*)data.ptr;
743 	*header = Header.init;
744 	header.bfSize = to!uint(data.length);
745 	header.bfOffBits = Header.sizeof;
746 	header.bcWidth = src.w;
747 	header.bcHeight = -src.h;
748 	header.bcPlanes = 1;
749 	header.biSizeImage = bitmapDataSize;
750 	header.bcBitCount = COLOR.sizeof * 8;
751 
752 	static if (header.VERSION >= 4)
753 	{
754 		header.biCompression = BI_BITFIELDS;
755 		static immutable masks = bitmapChannelMasks!COLOR();
756 		header.bV4RedMask   = masks[0];
757 		header.bV4GreenMask = masks[1];
758 		header.bV4BlueMask  = masks[2];
759 		header.bV4AlphaMask = masks[3];
760 	}
761 
762 	auto pixelData = data[header.bfOffBits..$];
763 	auto ptr = pixelData.ptr;
764 	size_t pos = 0;
765 
766 	foreach (y; 0..src.h)
767 	{
768 		src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]);
769 		ptr += pixelStride;
770 	}
771 
772 	return data;
773 }
774 
775 unittest
776 {
777 	Image!BGR output;
778 	onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output);
779 }
780 
781 // ***************************************************************************
782 
783 enum ulong PNGSignature = 0x0a1a0a0d474e5089;
784 
785 struct PNGChunk
786 {
787 	char[4] type;
788 	const(void)[] data;
789 
790 	uint crc32()
791 	{
792 		import std.digest.crc;
793 		CRC32 crc;
794 		crc.put(cast(ubyte[])(type[]));
795 		crc.put(cast(ubyte[])data);
796 		ubyte[4] hash = crc.finish();
797 		return *cast(uint*)hash.ptr;
798 	}
799 
800 	this(string type, const(void)[] data)
801 	{
802 		this.type[] = type[];
803 		this.data = data;
804 	}
805 }
806 
807 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 }
808 enum PNGCompressionMethod : ubyte { DEFLATE }
809 enum PNGFilterMethod : ubyte { ADAPTIVE }
810 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 }
811 
812 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH }
813 
814 align(1)
815 struct PNGHeader
816 {
817 align(1):
818 	ubyte[4] width, height;
819 	ubyte colourDepth;
820 	PNGColourType colourType;
821 	PNGCompressionMethod compressionMethod;
822 	PNGFilterMethod filterMethod;
823 	PNGInterlaceMethod interlaceMethod;
824 	static assert(PNGHeader.sizeof == 13);
825 }
826 
827 /// Creates a PNG file.
828 /// Only basic PNG features are supported
829 /// (no filters, interlacing, palettes etc.)
830 ubyte[] toPNG(SRC)(auto ref SRC src, int compressionLevel = 5)
831 	if (isView!SRC)
832 {
833 	import std.zlib : compress;
834 	import std.bitmanip : nativeToBigEndian, swapEndian;
835 
836 	alias COLOR = ViewColor!SRC;
837 	static if (!is(COLOR == struct))
838 		enum COLOUR_TYPE = PNGColourType.G;
839 	else
840 	static if (structFields!COLOR == ["l"])
841 		enum COLOUR_TYPE = PNGColourType.G;
842 	else
843 	static if (structFields!COLOR == ["r","g","b"])
844 		enum COLOUR_TYPE = PNGColourType.RGB;
845 	else
846 	static if (structFields!COLOR == ["l","a"])
847 		enum COLOUR_TYPE = PNGColourType.GA;
848 	else
849 	static if (structFields!COLOR == ["r","g","b","a"])
850 		enum COLOUR_TYPE = PNGColourType.RGBA;
851 	else
852 		static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof);
853 
854 	PNGChunk[] chunks;
855 	PNGHeader header = {
856 		width : nativeToBigEndian(src.w),
857 		height : nativeToBigEndian(src.h),
858 		colourDepth : ChannelType!COLOR.sizeof * 8,
859 		colourType : COLOUR_TYPE,
860 		compressionMethod : PNGCompressionMethod.DEFLATE,
861 		filterMethod : PNGFilterMethod.ADAPTIVE,
862 		interlaceMethod : PNGInterlaceMethod.NONE,
863 	};
864 	chunks ~= PNGChunk("IHDR", cast(void[])[header]);
865 	uint idatStride = to!uint(src.w * COLOR.sizeof+1);
866 	ubyte[] idatData = new ubyte[src.h * idatStride];
867 	for (uint y=0; y<src.h; y++)
868 	{
869 		idatData[y*idatStride] = PNGFilterAdaptive.NONE;
870 		auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride];
871 		src.copyScanline(y, rowPixels);
872 
873 		version (LittleEndian)
874 			static if (ChannelType!COLOR.sizeof > 1)
875 				foreach (ref p; cast(ChannelType!COLOR[])rowPixels)
876 					p = swapEndian(p);
877 	}
878 	chunks ~= PNGChunk("IDAT", compress(idatData, compressionLevel));
879 	chunks ~= PNGChunk("IEND", null);
880 
881 	return makePNG(chunks);
882 }
883 
884 ubyte[] makePNG(PNGChunk[] chunks)
885 {
886 	import std.bitmanip : nativeToBigEndian;
887 
888 	uint totalSize = 8;
889 	foreach (chunk; chunks)
890 		totalSize += 8 + chunk.data.length + 4;
891 	ubyte[] data = new ubyte[totalSize];
892 
893 	*cast(ulong*)data.ptr = PNGSignature;
894 	uint pos = 8;
895 	foreach(chunk;chunks)
896 	{
897 		uint i = pos;
898 		uint chunkLength = to!uint(chunk.data.length);
899 		pos += 12 + chunkLength;
900 		*cast(ubyte[4]*)&data[i] = nativeToBigEndian(chunkLength);
901 		(cast(char[])data[i+4 .. i+8])[] = chunk.type[];
902 		data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[];
903 		*cast(ubyte[4]*)&data[i+8+chunk.data.length] = nativeToBigEndian(chunk.crc32());
904 		assert(pos == i+12+chunk.data.length);
905 	}
906 
907 	return data;
908 }
909 
910 unittest
911 {
912 	onePixel(RGB(1,2,3)).toPNG();
913 	onePixel(5).toPNG();
914 }