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 // Different software have different standards regarding alpha without a V4 header.
591 // ImageMagick will write BMPs with alpha without a V4 header, but not all software will read them.
592 enum bitmapNeedV4HeaderForWrite(COLOR) = !is(COLOR == BGR) && !is(COLOR == BGRX);
593 enum bitmapNeedV4HeaderForRead (COLOR) = !is(COLOR == BGR) && !is(COLOR == BGRX) && !is(COLOR == BGRA);
594 
595 uint[4] bitmapChannelMasks(COLOR)()
596 {
597 	uint[4] result;
598 	foreach (i, f; COLOR.init.tupleof)
599 	{
600 		enum channelName = __traits(identifier, COLOR.tupleof[i]);
601 		static if (channelName != "x")
602 			static assert((COLOR.tupleof[i].offsetof + COLOR.tupleof[i].sizeof) * 8 <= 32,
603 				"Color " ~ COLOR.stringof ~ " (channel " ~ channelName ~ ") is too large for BMP");
604 
605 		enum MASK = (cast(uint)typeof(COLOR.tupleof[i]).max) << (COLOR.tupleof[i].offsetof*8);
606 		static if (channelName == "r")
607 			result[0] |= MASK;
608 		else
609 		static if (channelName == "g")
610 			result[1] |= MASK;
611 		else
612 		static if (channelName == "b")
613 			result[2] |= MASK;
614 		else
615 		static if (channelName == "a")
616 			result[3] |= MASK;
617 		else
618 		static if (channelName == "l")
619 		{
620 			result[0] |= MASK;
621 			result[1] |= MASK;
622 			result[2] |= MASK;
623 		}
624 		else
625 		static if (channelName == "x")
626 		{
627 		}
628 		else
629 			static assert(false, "Don't know how to encode channelNamenel " ~ channelName);
630 	}
631 	return result;
632 }
633 
634 @property int bitmapPixelStride(COLOR)(int w)
635 {
636 	int pixelStride = w * cast(uint)COLOR.sizeof;
637 	pixelStride = (pixelStride+3) & ~3;
638 	return pixelStride;
639 }
640 
641 /// Returns a view representing a BMP file.
642 /// Does not copy pixel data.
643 auto viewBMP(COLOR, V)(V data)
644 if (is(V : const(void)[]))
645 {
646 	import ae.utils.graphics.bitmap;
647 	alias BitmapHeader!3 Header;
648 	enforce(data.length > Header.sizeof, "Not enough data for header");
649 	Header* header = cast(Header*) data.ptr;
650 	enforce(header.bfType == "BM", "Invalid signature");
651 	enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)"
652 		.format(header.bfSize, data.length));
653 	enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof);
654 
655 	static struct BMP
656 	{
657 		int w, h;
658 		typeof(data.ptr) pixelData;
659 		int pixelStride;
660 
661 		inout(COLOR)[] scanline(int y) inout // TODO constness
662 		{
663 			assert(y >= 0 && y < h, "BMP scanline out of bounds");
664 			return (cast(COLOR*)(pixelData + y * pixelStride))[0..w];
665 		}
666 
667 		mixin DirectView;
668 	}
669 	BMP bmp;
670 
671 	bmp.w = header.bcWidth;
672 	bmp.h = header.bcHeight;
673 	enforce(header.bcPlanes==1, "Multiplane BMPs not supported");
674 
675 	enforce(header.bcBitCount == COLOR.sizeof * 8,
676 		"Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image"
677 		.format(header.bcBitCount, COLOR.sizeof * 8));
678 
679 	static if (bitmapNeedV4HeaderForRead!COLOR)
680 		enforce(header.VERSION >= 4, "Need a V4+ header to load a %s image".format(COLOR.stringof));
681 	if (header.VERSION >= 4)
682 	{
683 		enforce(data.length > BitmapHeader!4.sizeof, "Not enough data for header");
684 		auto header4 = cast(BitmapHeader!4*) data.ptr;
685 		uint[4] fileMasks = [
686 			header4.bV4RedMask,
687 			header4.bV4GreenMask,
688 			header4.bV4BlueMask,
689 			header4.bV4AlphaMask];
690 		static immutable expectedMasks = bitmapChannelMasks!COLOR();
691 		enforce(fileMasks == expectedMasks,
692 			"Channel format mask mismatch.\nExpected: [%(%32b, %)]\nIn file : [%(%32b, %)]"
693 			.format(expectedMasks, fileMasks));
694 	}
695 
696 	bmp.pixelData = data[header.bfOffBits..$].ptr;
697 	bmp.pixelStride = bitmapPixelStride!COLOR(bmp.w);
698 
699 	if (bmp.h < 0)
700 		bmp.h = -bmp.h;
701 	else
702 	{
703 		bmp.pixelData += bmp.pixelStride * (bmp.h - 1);
704 		bmp.pixelStride = -bmp.pixelStride;
705 	}
706 
707 	return bmp;
708 }
709 
710 /// Parses a Windows bitmap (.bmp) file.
711 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target)
712 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
713 {
714 	alias COLOR = ViewColor!TARGET;
715 	viewBMP!COLOR(data).copy(target);
716 	return target;
717 }
718 /// ditto
719 auto parseBMP(COLOR)(const(void)[] data)
720 {
721 	Image!COLOR target;
722 	return data.parseBMP(target);
723 }
724 
725 unittest
726 {
727 	alias parseBMP!BGR parseBMP24;
728 }
729 
730 /// Creates a Windows bitmap (.bmp) file.
731 ubyte[] toBMP(SRC)(auto ref SRC src)
732 	if (isView!SRC)
733 {
734 	alias COLOR = ViewColor!SRC;
735 
736 	import ae.utils.graphics.bitmap;
737 	static if (bitmapNeedV4HeaderForWrite!COLOR)
738 		alias BitmapHeader!4 Header;
739 	else
740 		alias BitmapHeader!3 Header;
741 
742 	auto pixelStride = bitmapPixelStride!COLOR(src.w);
743 	auto bitmapDataSize = src.h * pixelStride;
744 	ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize];
745 	auto header = cast(Header*)data.ptr;
746 	*header = Header.init;
747 	header.bfSize = to!uint(data.length);
748 	header.bfOffBits = Header.sizeof;
749 	header.bcWidth = src.w;
750 	header.bcHeight = -src.h;
751 	header.bcPlanes = 1;
752 	header.biSizeImage = bitmapDataSize;
753 	header.bcBitCount = COLOR.sizeof * 8;
754 
755 	static if (header.VERSION >= 4)
756 	{
757 		header.biCompression = BI_BITFIELDS;
758 		static immutable masks = bitmapChannelMasks!COLOR();
759 		header.bV4RedMask   = masks[0];
760 		header.bV4GreenMask = masks[1];
761 		header.bV4BlueMask  = masks[2];
762 		header.bV4AlphaMask = masks[3];
763 	}
764 
765 	auto pixelData = data[header.bfOffBits..$];
766 	auto ptr = pixelData.ptr;
767 	size_t pos = 0;
768 
769 	foreach (y; 0..src.h)
770 	{
771 		src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]);
772 		ptr += pixelStride;
773 	}
774 
775 	return data;
776 }
777 
778 unittest
779 {
780 	Image!BGR output;
781 	onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output);
782 }
783 
784 // ***************************************************************************
785 
786 enum ulong PNGSignature = 0x0a1a0a0d474e5089;
787 
788 struct PNGChunk
789 {
790 	char[4] type;
791 	const(void)[] data;
792 
793 	uint crc32()
794 	{
795 		import std.digest.crc;
796 		CRC32 crc;
797 		crc.put(cast(ubyte[])(type[]));
798 		crc.put(cast(ubyte[])data);
799 		ubyte[4] hash = crc.finish();
800 		return *cast(uint*)hash.ptr;
801 	}
802 
803 	this(string type, const(void)[] data)
804 	{
805 		this.type[] = type[];
806 		this.data = data;
807 	}
808 }
809 
810 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 }
811 enum PNGCompressionMethod : ubyte { DEFLATE }
812 enum PNGFilterMethod : ubyte { ADAPTIVE }
813 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 }
814 
815 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH }
816 
817 align(1)
818 struct PNGHeader
819 {
820 align(1):
821 	ubyte[4] width, height;
822 	ubyte colourDepth;
823 	PNGColourType colourType;
824 	PNGCompressionMethod compressionMethod;
825 	PNGFilterMethod filterMethod;
826 	PNGInterlaceMethod interlaceMethod;
827 	static assert(PNGHeader.sizeof == 13);
828 }
829 
830 /// Creates a PNG file.
831 /// Only basic PNG features are supported
832 /// (no filters, interlacing, palettes etc.)
833 ubyte[] toPNG(SRC)(auto ref SRC src, int compressionLevel = 5)
834 	if (isView!SRC)
835 {
836 	import std.zlib : compress;
837 	import std.bitmanip : nativeToBigEndian, swapEndian;
838 
839 	alias COLOR = ViewColor!SRC;
840 	static if (!is(COLOR == struct))
841 		enum COLOUR_TYPE = PNGColourType.G;
842 	else
843 	static if (structFields!COLOR == ["l"])
844 		enum COLOUR_TYPE = PNGColourType.G;
845 	else
846 	static if (structFields!COLOR == ["r","g","b"])
847 		enum COLOUR_TYPE = PNGColourType.RGB;
848 	else
849 	static if (structFields!COLOR == ["l","a"])
850 		enum COLOUR_TYPE = PNGColourType.GA;
851 	else
852 	static if (structFields!COLOR == ["r","g","b","a"])
853 		enum COLOUR_TYPE = PNGColourType.RGBA;
854 	else
855 		static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof);
856 
857 	PNGChunk[] chunks;
858 	PNGHeader header = {
859 		width : nativeToBigEndian(src.w),
860 		height : nativeToBigEndian(src.h),
861 		colourDepth : ChannelType!COLOR.sizeof * 8,
862 		colourType : COLOUR_TYPE,
863 		compressionMethod : PNGCompressionMethod.DEFLATE,
864 		filterMethod : PNGFilterMethod.ADAPTIVE,
865 		interlaceMethod : PNGInterlaceMethod.NONE,
866 	};
867 	chunks ~= PNGChunk("IHDR", cast(void[])[header]);
868 	uint idatStride = to!uint(src.w * COLOR.sizeof+1);
869 	ubyte[] idatData = new ubyte[src.h * idatStride];
870 	for (uint y=0; y<src.h; y++)
871 	{
872 		idatData[y*idatStride] = PNGFilterAdaptive.NONE;
873 		auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride];
874 		src.copyScanline(y, rowPixels);
875 
876 		version (LittleEndian)
877 			static if (ChannelType!COLOR.sizeof > 1)
878 				foreach (ref p; cast(ChannelType!COLOR[])rowPixels)
879 					p = swapEndian(p);
880 	}
881 	chunks ~= PNGChunk("IDAT", compress(idatData, compressionLevel));
882 	chunks ~= PNGChunk("IEND", null);
883 
884 	return makePNG(chunks);
885 }
886 
887 ubyte[] makePNG(PNGChunk[] chunks)
888 {
889 	import std.bitmanip : nativeToBigEndian;
890 
891 	uint totalSize = 8;
892 	foreach (chunk; chunks)
893 		totalSize += 8 + chunk.data.length + 4;
894 	ubyte[] data = new ubyte[totalSize];
895 
896 	*cast(ulong*)data.ptr = PNGSignature;
897 	uint pos = 8;
898 	foreach(chunk;chunks)
899 	{
900 		uint i = pos;
901 		uint chunkLength = to!uint(chunk.data.length);
902 		pos += 12 + chunkLength;
903 		*cast(ubyte[4]*)&data[i] = nativeToBigEndian(chunkLength);
904 		(cast(char[])data[i+4 .. i+8])[] = chunk.type[];
905 		data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[];
906 		*cast(ubyte[4]*)&data[i+8+chunk.data.length] = nativeToBigEndian(chunk.crc32());
907 		assert(pos == i+12+chunk.data.length);
908 	}
909 
910 	return data;
911 }
912 
913 unittest
914 {
915 	onePixel(RGB(1,2,3)).toPNG();
916 	onePixel(5).toPNG();
917 }