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.math : abs;
20 import std.range;
21 import std.string : format;
22 
23 public import ae.utils.graphics.view;
24 
25 /// Represents a reference to COLOR data
26 /// already existing elsewhere in memory.
27 /// Assumes that pixels are stored row-by-row,
28 /// with a known distance between each row.
29 struct ImageRef(COLOR, StorageType = PlainStorageUnit!COLOR)
30 {
31 	xy_t w, h;
32 	size_t pitch; /// In bytes, not COLORs
33 	StorageType* pixels;
34 
35 	/// Returns an array for the pixels at row y.
36 	inout(StorageType)[] scanline(xy_t y) inout
37 	{
38 		assert(y>=0 && y<h, "Scanline out-of-bounds");
39 		assert(pitch, "Pitch not set");
40 		auto row = cast(StorageType*)(cast(ubyte*)pixels + y*pitch);
41 		return row[0..w];
42 	}
43 
44 	mixin DirectView;
45 }
46 
47 unittest
48 {
49 	static assert(isDirectView!(ImageRef!ubyte));
50 }
51 
52 /// Convert a direct view to an ImageRef.
53 /// Assumes that the rows are evenly spaced.
54 ImageRef!(ViewColor!SRC) toRef(SRC)(auto ref SRC src)
55 	if (isDirectView!SRC)
56 {
57 	return ImageRef!(ViewColor!SRC)(src.w, src.h,
58 		src.h > 1 ? cast(ubyte*)src.scanline(1) - cast(ubyte*)src.scanline(0) : src.w,
59 		src.scanline(0).ptr);
60 }
61 
62 unittest
63 {
64 	auto i = Image!ubyte(1, 1);
65 	auto r = i.toRef();
66 	assert(r.scanline(0).ptr is i.scanline(0).ptr);
67 }
68 
69 // ***************************************************************************
70 
71 /// An in-memory image.
72 /// Pixels are stored in a flat array.
73 struct Image(COLOR, StorageType = PlainStorageUnit!COLOR)
74 {
75 	xy_t w, h;
76 	StorageType[] pixels;
77 
78 	/// Returns an array for the pixels at row y.
79 	inout(StorageType)[] scanline(xy_t y) inout
80 	{
81 		assert(y>=0 && y<h, "Scanline out-of-bounds");
82 		auto start = w*y;
83 		return pixels[start..start+w];
84 	}
85 
86 	mixin DirectView;
87 
88 	this(xy_t w, xy_t h)
89 	{
90 		size(w, h);
91 	}
92 
93 	/// Does not scale image
94 	void size(xy_t w, xy_t h)
95 	{
96 		this.w = w;
97 		this.h = h;
98 		if (pixels.length < w*h)
99 			pixels.length = w*h;
100 	}
101 }
102 
103 unittest
104 {
105 	static assert(isDirectView!(Image!ubyte));
106 }
107 
108 // ***************************************************************************
109 
110 // Functions which need a target image to operate on are currenty declared
111 // as two overloads. The code might be simplified if some of these get fixed:
112 // https://d.puremagic.com/issues/show_bug.cgi?id=8074
113 // https://d.puremagic.com/issues/show_bug.cgi?id=12386
114 // https://d.puremagic.com/issues/show_bug.cgi?id=12425
115 // https://d.puremagic.com/issues/show_bug.cgi?id=12426
116 // https://d.puremagic.com/issues/show_bug.cgi?id=12433
117 
118 alias ViewImage(V) = Image!(ViewColor!V);
119 
120 /// Copy the given view into the specified target.
121 auto copy(SRC, TARGET)(auto ref SRC src, auto ref TARGET target)
122 	if (isView!SRC && isWritableView!TARGET)
123 {
124 	target.size(src.w, src.h);
125 	src.blitTo(target);
126 	return target;
127 }
128 
129 /// Copy the given view into a newly-allocated image.
130 auto copy(SRC)(auto ref SRC src)
131 	if (isView!SRC)
132 {
133 	ViewImage!SRC target;
134 	return src.copy(target);
135 }
136 
137 unittest
138 {
139 	auto v = onePixel(0);
140 	auto i = v.copy();
141 	v.copy(i);
142 
143 	auto c = i.crop(0, 0, 1, 1);
144 	v.copy(c);
145 }
146 
147 alias ElementViewImage(R) = ViewImage!(ElementType!R);
148 
149 /// Splice multiple images horizontally.
150 auto hjoin(R, TARGET)(R images, auto ref TARGET target)
151 	if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET)
152 {
153 	xy_t w, h;
154 	foreach (ref image; images)
155 		w += image.w,
156 		h = max(h, image.h);
157 	target.size(w, h);
158 	xy_t x;
159 	foreach (ref image; images)
160 		image.blitTo(target, x, 0),
161 		x += image.w;
162 	return target;
163 }
164 /// ditto
165 auto hjoin(R)(R images)
166 	if (isInputRange!R && isView!(ElementType!R))
167 {
168 	ElementViewImage!R target;
169 	return images.hjoin(target);
170 }
171 
172 /// Splice multiple images vertically.
173 auto vjoin(R, TARGET)(R images, auto ref TARGET target)
174 	if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET)
175 {
176 	xy_t w, h;
177 	foreach (ref image; images)
178 		w = max(w, image.w),
179 		h += image.h;
180 	target.size(w, h);
181 	xy_t y;
182 	foreach (ref image; images)
183 		image.blitTo(target, 0, y),
184 		y += image.h;
185 	return target;
186 }
187 /// ditto
188 auto vjoin(R)(R images)
189 	if (isInputRange!R && isView!(ElementType!R))
190 {
191 	ElementViewImage!R target;
192 	return images.vjoin(target);
193 }
194 
195 unittest
196 {
197 	auto h = 10
198 		.iota
199 		.retro
200 		.map!onePixel
201 		.retro
202 		.hjoin();
203 
204 	foreach (i; 0..10)
205 		assert(h[i, 0] == i);
206 
207 	auto v = 10.iota.map!onePixel.vjoin();
208 	foreach (i; 0..10)
209 		assert(v[0, i] == i);
210 }
211 
212 // ***************************************************************************
213 
214 /// Performs linear downscale by a constant factor
215 template downscale(int HRX, int HRY=HRX)
216 {
217 	auto downscale(SRC, TARGET)(auto ref SRC src, auto ref TARGET target)
218 		if (isDirectView!SRC && isWritableView!TARGET)
219 	{
220 		alias lr = target;
221 		alias hr = src;
222 		alias COLOR = ViewColor!SRC;
223 
224 		assert(hr.w % HRX == 0 && hr.h % HRY == 0, "Size mismatch");
225 
226 		lr.size(hr.w / HRX, hr.h / HRY);
227 
228 		foreach (y; 0..lr.h)
229 			foreach (x; 0..lr.w)
230 			{
231 				static if (HRX*HRY <= 0x100)
232 					enum EXPAND_BYTES = 1;
233 				else
234 				static if (HRX*HRY <= 0x10000)
235 					enum EXPAND_BYTES = 2;
236 				else
237 					static assert(0);
238 				static if (is(typeof(COLOR.init.a))) // downscale with alpha
239 				{
240 					version (none) // TODO: broken
241 					{
242 						ExpandChannelType!(COLOR, EXPAND_BYTES+COLOR.init.a.sizeof) sum;
243 						ExpandChannelType!(typeof(COLOR.init.a), EXPAND_BYTES) alphaSum;
244 						auto start = y*HRY*hr.stride + x*HRX;
245 						foreach (j; 0..HRY)
246 						{
247 							foreach (p; hr.pixels[start..start+HRX])
248 							{
249 								foreach (i, f; p.tupleof)
250 									static if (p.tupleof[i].stringof != "p.a")
251 									{
252 										enum FIELD = p.tupleof[i].stringof[2..$];
253 										mixin("sum."~FIELD~" += cast(typeof(sum."~FIELD~"))p."~FIELD~" * p.a;");
254 									}
255 								alphaSum += p.a;
256 							}
257 							start += hr.stride;
258 						}
259 						if (alphaSum)
260 						{
261 							auto result = cast(COLOR)(sum / alphaSum);
262 							result.a = cast(typeof(result.a))(alphaSum / (HRX*HRY));
263 							lr[x, y] = result;
264 						}
265 						else
266 						{
267 							static assert(COLOR.init.a == 0);
268 							lr[x, y] = COLOR.init;
269 						}
270 					}
271 					else
272 						static assert(false, "Downscaling with alpha is not implemented");
273 				}
274 				else
275 				{
276 					ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum;
277 					auto x0 = x*HRX;
278 					auto x1 = x0+HRX;
279 					foreach (j; y*HRY..(y+1)*HRY)
280 						foreach (s; hr.scanline(j)[x0..x1])
281 							foreach (p; s)
282 								sum += p;
283 					lr[x, y] = cast(ViewColor!SRC)(sum / (HRX*HRY));
284 				}
285 			}
286 
287 		return target;
288 	}
289 
290 	auto downscale(SRC)(auto ref SRC src)
291 		if (isView!SRC)
292 	{
293 		ViewImage!SRC target;
294 		return src.downscale(target);
295 	}
296 }
297 
298 unittest
299 {
300 	onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)();
301 //	onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)();
302 
303 	Image!ubyte i;
304 	i.size(4, 1);
305 	i.pixels[] = [[1], [3], [5], [7]];
306 	auto d = i.downscale!(2, 1);
307 	assert(d.pixels == [[2], [6]]);
308 }
309 
310 // ***************************************************************************
311 
312 /// Downscaling copy (averages colors in source per one pixel in target).
313 auto downscaleTo(SRC, TARGET)(auto ref SRC src, auto ref TARGET target)
314 if (isDirectView!SRC && isWritableView!TARGET)
315 {
316 	alias lr = target;
317 	alias hr = src;
318 	alias COLOR = ViewColor!SRC;
319 
320 	void impl(uint EXPAND_BYTES)()
321 	{
322 		foreach (y; 0..lr.h)
323 			foreach (x; 0..lr.w)
324 			{
325 				static if (is(typeof(COLOR.init.a))) // downscale with alpha
326 					static assert(false, "Downscaling with alpha is not implemented");
327 				else
328 				{
329 					ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum;
330 					auto x0 =  x    * hr.w / lr.w;
331 					auto x1 = (x+1) * hr.w / lr.w;
332 					auto y0 =  y    * hr.h / lr.h;
333 					auto y1 = (y+1) * hr.h / lr.h;
334 
335 					// When upscaling (across one or two axes),
336 					// fall back to nearest neighbor
337 					if (x0 == x1) x1++;
338 					if (y0 == y1) y1++;
339 
340 					foreach (j; y0 .. y1)
341 						foreach (s; hr.scanline(j)[x0 .. x1])
342 							foreach (p; s)
343 								sum += p;
344 					auto area = (x1 - x0) * (y1 - y0);
345 					auto avg = sum / cast(uint)area;
346 					lr[x, y] = cast(ViewColor!SRC)(avg);
347 				}
348 			}
349 	}
350 
351 	auto perPixelArea = (hr.w / lr.w + 1) * (hr.h / lr.h + 1);
352 
353 	if (perPixelArea <= 0x100)
354 		impl!1();
355 	else
356 	if (perPixelArea <= 0x10000)
357 		impl!2();
358 	else
359 	if (perPixelArea <= 0x1000000)
360 		impl!3();
361 	else
362 		assert(false, "Downscaling too much");
363 
364 	return target;
365 }
366 
367 /// Downscales an image to a certain size.
368 auto downscaleTo(SRC)(auto ref SRC src, xy_t w, xy_t h)
369 if (isView!SRC)
370 {
371 	ViewImage!SRC target;
372 	target.size(w, h);
373 	return src.downscaleTo(target);
374 }
375 
376 unittest
377 {
378 	onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2);
379 //	onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2);
380 
381 	Image!ubyte i;
382 	i.size(6, 1);
383 	i.pixels[] = [[1], [2], [3], [4], [5], [6]];
384 	assert(i.downscaleTo(6, 1).pixels == [[1], [2], [3], [4], [5], [6]]);
385 	assert(i.downscaleTo(3, 1).pixels == [[1], [3], [5]]);
386 	assert(i.downscaleTo(2, 1).pixels == [[2], [5]]);
387 	assert(i.downscaleTo(1, 1).pixels == [[3]]);
388 
389 	i.size(3, 3);
390 	i.pixels[] = [
391 		[1], [2], [3],
392 		[4], [5], [6],
393 		[7], [8], [9]];
394 	assert(i.downscaleTo(2, 2).pixels == [[1], [2], [5], [7]]);
395 
396 	i.size(1, 1);
397 	i.pixels = [[1]];
398 	assert(i.downscaleTo(2, 2).pixels == [[1], [1], [1], [1]]);
399 }
400 
401 // ***************************************************************************
402 
403 /// Copy the indicated row of src to a StorageType buffer.
404 void copyScanline(SRC, StorageType)(auto ref SRC src, xy_t y, StorageType[] dst)
405 if (isView!SRC && is(StorageColor!StorageType == ViewColor!SRC))
406 {
407 	static if (isDirectView!SRC && is(ViewStorageType!SRC == StorageType))
408 		dst[] = src.scanline(y)[];
409 	else
410 	{
411 		auto storageUnitsPerRow = (src.w + StorageType.length - 1) / StorageType.length;
412 		assert(storageUnitsPerRow == dst.length);
413 		foreach (x; 0..src.w)
414 			dst[x / StorageType.length][x % StorageType.length] = src[x, y];
415 	}
416 }
417 
418 /// Copy a view's pixels (top-to-bottom) to a StorageType buffer.
419 /// Rows are assumed to be StorageType.sizeof-aligned.
420 void copyPixels(SRC, StorageType)(auto ref SRC src, StorageType[] dst)
421 if (isView!SRC && is(StorageColor!StorageType == ViewColor!SRC))
422 {
423 	auto storageUnitsPerRow = src.w + (StorageType.length - 1) / StorageType.length;
424 	assert(dst.length == storageUnitsPerRow * src.h);
425 	foreach (y; 0..src.h)
426 		src.copyScanline(y, dst[y*storageUnitsPerRow..(y+1)*storageUnitsPerRow]);
427 }
428 
429 // ***************************************************************************
430 
431 import std.traits;
432 
433 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433
434 
435 struct InputColor {}
436 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR);
437 
438 struct TargetColor {}
439 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET);
440 
441 // ***************************************************************************
442 
443 import ae.utils.graphics.color;
444 import ae.utils.meta : structFields;
445 
446 private string[] readPBMHeader(ref const(ubyte)[] data)
447 {
448 	import std.ascii;
449 
450 	string[] fields;
451 	uint wordStart = 0;
452 	uint p;
453 	for (p=1; p<data.length && fields.length<4; p++)
454 		if (!isWhite(data[p-1]) && isWhite(data[p]))
455 			fields ~= cast(string)data[wordStart..p];
456 		else
457 		if (isWhite(data[p-1]) && !isWhite(data[p]))
458 			wordStart = p;
459 	data = data[p..$];
460 	enforce(fields.length==4, "Header too short");
461 	enforce(fields[0].length==2 && fields[0][0]=='P', "Invalid signature");
462 	return fields;
463 }
464 
465 private template PBMSignature(COLOR)
466 {
467 	static if (structFields!COLOR == ["l"])
468 		enum PBMSignature = "P5";
469 	else
470 	static if (structFields!COLOR == ["r", "g", "b"])
471 		enum PBMSignature = "P6";
472 	else
473 		static assert(false, "Unsupported PBM color: " ~
474 			__traits(allMembers, COLOR.Fields).stringof);
475 }
476 
477 /// Parses a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
478 auto parsePBM(C = TargetColor, TARGET)(const(void)[] vdata, auto ref TARGET target)
479 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
480 {
481 	alias COLOR = ViewColor!TARGET;
482 
483 	auto data = cast(const(ubyte)[])vdata;
484 	string[] fields = readPBMHeader(data);
485 	enforce(fields[0]==PBMSignature!COLOR, "Invalid signature");
486 	enforce(to!uint(fields[3]) == COLOR.tupleof[0].max, "Channel depth mismatch");
487 
488 	target.size(to!uint(fields[1]), to!uint(fields[2]));
489 	enforce(data.length / COLOR.sizeof == target.w * target.h,
490 		"Dimension / filesize mismatch");
491 	target.pixels[] = cast(PlainStorageUnit!COLOR[])data;
492 
493 	static if (COLOR.tupleof[0].sizeof > 1)
494 		foreach (ref pixel; pixels)
495 			pixel = COLOR.op!q{swapBytes(a)}(pixel); // TODO: proper endianness support
496 
497 	return target;
498 }
499 /// ditto
500 auto parsePBM(COLOR)(const(void)[] vdata)
501 {
502 	Image!COLOR target;
503 	return vdata.parsePBM(target);
504 }
505 
506 unittest
507 {
508 	import std.conv : hexString;
509 	auto data = "P6\n2\n2\n255\n" ~
510 		hexString!"000000 FFF000" ~
511 		hexString!"000FFF FFFFFF";
512 	auto i = data.parsePBM!RGB();
513 	assert(i[0, 0] == RGB.fromHex("000000"));
514 	assert(i[0, 1] == RGB.fromHex("000FFF"));
515 }
516 
517 unittest
518 {
519 	import std.conv : hexString;
520 	auto data = "P5\n2\n2\n255\n" ~
521 		hexString!"00 55" ~
522 		hexString!"AA FF";
523 	auto i = data.parsePBM!L8();
524 	assert(i[0, 0] == L8(0x00));
525 	assert(i[0, 1] == L8(0xAA));
526 }
527 
528 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file.
529 ubyte[] toPBM(SRC)(auto ref SRC src)
530 	if (isView!SRC)
531 {
532 	alias COLOR = ViewColor!SRC;
533 	alias StorageType = PlainStorageUnit!COLOR;
534 
535 	auto length = src.w * src.h;
536 	ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n"
537 		.format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max);
538 	ubyte[] data = new ubyte[header.length + length * COLOR.sizeof];
539 
540 	data[0..header.length] = header;
541 	src.copyPixels(cast(StorageType[])data[header.length..$]);
542 
543 	static if (ChannelType!COLOR.sizeof > 1)
544 		foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$])
545 			p = swapBytes(p); // TODO: proper endianness support
546 
547 	return data;
548 }
549 
550 unittest
551 {
552 	import std.conv : hexString;
553 	assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ hexString!"01 02 03");
554 	assert(onePixel(L8 (1)    ).toPBM == "P5\n1 1 255\n" ~ hexString!"01"      );
555 }
556 
557 // ***************************************************************************
558 
559 /// Loads a raw COLOR[] into an image of the indicated size.
560 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h,
561 		auto ref TARGET target)
562 	if (isWritableView!TARGET
563 	 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET))
564 {
565 	alias COLOR = ViewColor!TARGET;
566 
567 	auto pixels = cast(PlainStorageUnit!COLOR[])input;
568 	enforce(pixels.length == w*h, "Dimension / filesize mismatch");
569 	target.size(w, h);
570 	target.pixels[] = pixels;
571 	return target;
572 }
573 
574 /// ditto
575 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h)
576 {
577 	alias COLOR = GetInputColor!(C, INPUT);
578 	Image!COLOR target;
579 	return fromPixels!COLOR(input, w, h, target);
580 }
581 
582 unittest
583 {
584 	import std.conv : hexString;
585 	Image!L8 i;
586 	i = hexString!"42".fromPixels!L8(1, 1);
587 	i = hexString!"42".fromPixels!L8(1, 1, i);
588 	assert(i[0, 0].l == 0x42);
589 	i = (cast(L8[])hexString!"42").fromPixels(1, 1);
590 	i = (cast(L8[])hexString!"42").fromPixels(1, 1, i);
591 }
592 
593 // ***************************************************************************
594 
595 static import ae.utils.graphics.bitmap;
596 
597 // Different software have different standards regarding alpha without a V4 header.
598 // ImageMagick will write BMPs with alpha without a V4 header, but not all software will read them.
599 enum bitmapNeedV4HeaderForWrite(COLOR) = is(COLOR == struct) && !is(COLOR == BGR) && !is(COLOR == BGRX);
600 enum bitmapNeedV4HeaderForRead (COLOR) = is(COLOR == struct) && !is(COLOR == BGR) && !is(COLOR == BGRX) && !is(COLOR == BGRA);
601 
602 uint[4] bitmapChannelMasks(COLOR)()
603 {
604 	uint[4] result;
605 	foreach (i, f; COLOR.init.tupleof)
606 	{
607 		enum channelName = __traits(identifier, COLOR.tupleof[i]);
608 		static if (channelName != "x")
609 			static assert((COLOR.tupleof[i].offsetof + COLOR.tupleof[i].sizeof) * 8 <= 32,
610 				"Color " ~ COLOR.stringof ~ " (channel " ~ channelName ~ ") is too large for BMP");
611 
612 		enum MASK = (cast(uint)typeof(COLOR.tupleof[i]).max) << (COLOR.tupleof[i].offsetof*8);
613 		static if (channelName == "r")
614 			result[0] |= MASK;
615 		else
616 		static if (channelName == "g")
617 			result[1] |= MASK;
618 		else
619 		static if (channelName == "b")
620 			result[2] |= MASK;
621 		else
622 		static if (channelName == "a")
623 			result[3] |= MASK;
624 		else
625 		static if (channelName == "l")
626 		{
627 			result[0] |= MASK;
628 			result[1] |= MASK;
629 			result[2] |= MASK;
630 		}
631 		else
632 		static if (channelName == "x")
633 		{
634 		}
635 		else
636 			static assert(false, "Don't know how to encode channelNamenel " ~ channelName);
637 	}
638 	return result;
639 }
640 
641 @property size_t bitmapPixelStride(StorageType)(xy_t w)
642 {
643 	auto rowBits = w * storageColorBits!StorageType;
644 	rowBits = (rowBits + 0x1f) & ~0x1f;
645 	return rowBits / 8;
646 }
647 
648 template BMPStorageType(COLOR)
649 {
650 	static if (is(COLOR == bool))
651 		alias BMPStorageType = OneBitStorageBE;
652 	else
653 		alias BMPStorageType = PlainStorageUnit!COLOR;
654 }
655 
656 /// Returns a view representing a BMP file.
657 /// Does not copy pixel data.
658 auto viewBMP(COLOR, V)(V data)
659 if (is(V : const(void)[]))
660 {
661 	import ae.utils.graphics.bitmap;
662 	alias BitmapHeader!3 Header;
663 	enforce(data.length > Header.sizeof, "Not enough data for header");
664 	Header* header = cast(Header*) data.ptr;
665 	enforce(header.bfType == "BM", "Invalid signature");
666 	enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)"
667 		.format(header.bfSize, data.length));
668 	enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof);
669 
670 	alias StorageType = BMPStorageType!COLOR;
671 
672 	static struct BMP
673 	{
674 		xy_t w, h;
675 		typeof(data.ptr) pixelData;
676 		sizediff_t pixelStride;
677 
678 		inout(StorageType)[] scanline(xy_t y) inout
679 		{
680 			assert(y >= 0 && y < h, "BMP scanline out of bounds");
681 			auto row = cast(void*)pixelData + y * pixelStride;
682 			auto storageUnitsPerRow = (w + StorageType.length - 1) / StorageType.length;
683 			return (cast(inout(StorageType)*)row)[0 .. storageUnitsPerRow];
684 		}
685 
686 		mixin DirectView;
687 	}
688 	BMP bmp;
689 
690 	bmp.w = header.bcWidth;
691 	bmp.h = header.bcHeight;
692 	enforce(header.bcPlanes==1, "Multiplane BMPs not supported");
693 
694 	enum storageBits = StorageType.sizeof * 8 / StorageType.length;
695 	enforce(header.bcBitCount == storageBits,
696 		"Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image"
697 		.format(header.bcBitCount, storageBits));
698 
699 	static if (bitmapNeedV4HeaderForRead!COLOR)
700 		enforce(header.VERSION >= 4, "Need a V4+ header to load a %s image".format(COLOR.stringof));
701 	if (header.VERSION >= 4)
702 	{
703 		enforce(data.length > BitmapHeader!4.sizeof, "Not enough data for header");
704 		auto header4 = cast(BitmapHeader!4*) data.ptr;
705 		static if (is(COLOR == struct))
706 		{
707 			uint[4] fileMasks = [
708 				header4.bV4RedMask,
709 				header4.bV4GreenMask,
710 				header4.bV4BlueMask,
711 				header4.bV4AlphaMask];
712 			static immutable expectedMasks = bitmapChannelMasks!COLOR();
713 			enforce(fileMasks == expectedMasks,
714 				"Channel format mask mismatch.\nExpected: [%(%32b, %)]\nIn file : [%(%32b, %)]"
715 				.format(expectedMasks, fileMasks));
716 		}
717 		else
718 			throw new Exception("Unexpected V4 header with basic COLOR type " ~ COLOR.stringof);
719 	}
720 
721 	auto pixelData = data[header.bfOffBits..$];
722 	bmp.pixelData = pixelData.ptr;
723 	bmp.pixelStride = bitmapPixelStride!StorageType(bmp.w);
724 	enforce(bmp.pixelStride * abs(bmp.h) <= pixelData.length, "Insufficient data for pixels");
725 
726 	if (bmp.h < 0)
727 		bmp.h = -bmp.h;
728 	else
729 	{
730 		bmp.pixelData += bmp.pixelStride * (bmp.h - 1);
731 		bmp.pixelStride = -bmp.pixelStride;
732 	}
733 
734 	return bmp;
735 }
736 
737 /// Parses a Windows bitmap (.bmp) file.
738 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target)
739 	if (isWritableView!TARGET && isTargetColor!(C, TARGET))
740 {
741 	alias COLOR = ViewColor!TARGET;
742 	viewBMP!COLOR(data).copy(target);
743 	return target;
744 }
745 /// ditto
746 auto parseBMP(COLOR)(const(void)[] data)
747 {
748 	Image!(COLOR, BMPStorageType!COLOR) target;
749 	return data.parseBMP(target);
750 }
751 
752 unittest
753 {
754 	alias parseBMP!BGR parseBMP24;
755 	if (false)
756 	{
757 		auto b = viewBMP!BGRA((void[]).init);
758 		BGRA c = b[1, 2];
759 	}
760 	alias parseBMP!bool parseBMP1;
761 }
762 
763 /// Creates a Windows bitmap (.bmp) file.
764 ubyte[] toBMP(SRC)(auto ref SRC src)
765 	if (isView!SRC)
766 {
767 	alias COLOR = ViewColor!SRC;
768 	alias StorageType = BMPStorageType!COLOR;
769 
770 	import ae.utils.graphics.bitmap;
771 	static if (bitmapNeedV4HeaderForWrite!COLOR)
772 		alias BitmapHeader!4 Header;
773 	else
774 		alias BitmapHeader!3 Header;
775 
776 	auto pixelStride = bitmapPixelStride!StorageType(src.w);
777 	auto bitmapDataSize = src.h * pixelStride;
778 	ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize];
779 	auto header = cast(Header*)data.ptr;
780 	*header = Header.init;
781 	header.bfSize = data.length.to!uint;
782 	header.bfOffBits = Header.sizeof;
783 	header.bcWidth = src.w.to!int;
784 	header.bcHeight = -src.h.to!int;
785 	header.bcPlanes = 1;
786 	header.biSizeImage = bitmapDataSize.to!uint;
787 	enum storageBits = StorageType.sizeof * 8 / StorageType.length;
788 	header.bcBitCount = storageBits;
789 
790 	static if (header.VERSION >= 4)
791 	{
792 		header.biCompression = BI_BITFIELDS;
793 		static immutable masks = bitmapChannelMasks!COLOR();
794 		header.bV4RedMask   = masks[0];
795 		header.bV4GreenMask = masks[1];
796 		header.bV4BlueMask  = masks[2];
797 		header.bV4AlphaMask = masks[3];
798 	}
799 
800 	auto pixelData = data[header.bfOffBits..$];
801 	auto ptr = pixelData.ptr;
802 	size_t pos = 0;
803 	auto storageUnitsPerRow = (src.w + StorageType.length - 1) / StorageType.length;
804 
805 	foreach (y; 0..src.h)
806 	{
807 		src.copyScanline(y, (cast(StorageType*)ptr)[0..storageUnitsPerRow]);
808 		ptr += pixelStride;
809 	}
810 
811 	return data;
812 }
813 
814 unittest
815 {
816 	Image!BGR output;
817 	onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output);
818 }
819 
820 // ***************************************************************************
821 
822 static immutable ubyte[8] pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; // \211   P   N   G  \r  \n \032 \n
823 
824 struct PNGChunk
825 {
826 	char[4] type;
827 	const(void)[] data;
828 
829 	uint crc32()
830 	{
831 		import std.digest.crc;
832 		CRC32 crc;
833 		crc.put(cast(ubyte[])(type[]));
834 		crc.put(cast(ubyte[])data);
835 		ubyte[4] hash = crc.finish();
836 		return *cast(uint*)hash.ptr;
837 	}
838 
839 	this(string type, const(void)[] data)
840 	{
841 		this.type[] = type[];
842 		this.data = data;
843 	}
844 }
845 
846 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 }
847 enum PNGCompressionMethod : ubyte { DEFLATE }
848 enum PNGFilterMethod : ubyte { ADAPTIVE }
849 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 }
850 
851 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH }
852 
853 align(1)
854 struct PNGHeader
855 {
856 align(1):
857 	ubyte[4] width, height;
858 	ubyte colourDepth;
859 	PNGColourType colourType;
860 	PNGCompressionMethod compressionMethod;
861 	PNGFilterMethod filterMethod;
862 	PNGInterlaceMethod interlaceMethod;
863 	static assert(PNGHeader.sizeof == 13);
864 }
865 
866 struct PNGChunkHeader { ubyte[4] length; char[4] type; }
867 struct PNGChunkFooter { ubyte[4] crc32; }
868 
869 /// Creates a PNG file.
870 /// Only basic PNG features are supported
871 /// (no filters, interlacing, palettes etc.)
872 ubyte[] toPNG(SRC)(auto ref SRC src, int compressionLevel = 5)
873 	if (isView!SRC)
874 {
875 	import std.zlib : compress;
876 	import std.bitmanip : nativeToBigEndian, swapEndian;
877 
878 	alias COLOR = ViewColor!SRC;
879 	static if (!is(COLOR == struct))
880 		enum COLOUR_TYPE = PNGColourType.G;
881 	else
882 	static if (structFields!COLOR == ["l"])
883 		enum COLOUR_TYPE = PNGColourType.G;
884 	else
885 	static if (structFields!COLOR == ["r","g","b"])
886 		enum COLOUR_TYPE = PNGColourType.RGB;
887 	else
888 	static if (structFields!COLOR == ["l","a"])
889 		enum COLOUR_TYPE = PNGColourType.GA;
890 	else
891 	static if (structFields!COLOR == ["r","g","b","a"])
892 		enum COLOUR_TYPE = PNGColourType.RGBA;
893 	else
894 		static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof);
895 
896 	static if (is(COLOR == bool))
897 		alias StorageType = OneBitStorageBE;
898 	else
899 		alias StorageType = PlainStorageUnit!COLOR;
900 
901 	static if (is(COLOR == struct))
902 		enum numChannels = structFields!COLOR.length;
903 	else
904 		enum numChannels = 1;
905 
906 	PNGChunk[] chunks;
907 	PNGHeader header = {
908 		width : nativeToBigEndian(src.w.to!uint),
909 		height : nativeToBigEndian(src.h.to!uint),
910 		colourDepth : StorageType.sizeof * 8 / StorageType.length / numChannels,
911 		colourType : COLOUR_TYPE,
912 		compressionMethod : PNGCompressionMethod.DEFLATE,
913 		filterMethod : PNGFilterMethod.ADAPTIVE,
914 		interlaceMethod : PNGInterlaceMethod.NONE,
915 	};
916 	chunks ~= PNGChunk("IHDR", cast(void[])[header]);
917 	auto storageUnitsPerRow = (src.w + StorageType.length - 1) / StorageType.length;
918 	size_t idatStride = 1 + (storageUnitsPerRow * StorageType.sizeof);
919 	ubyte[] idatData = new ubyte[src.h * idatStride];
920 	for (uint y=0; y<src.h; y++)
921 	{
922 		idatData[y * idatStride] = PNGFilterAdaptive.NONE;
923 		auto rowPixels = cast(StorageType[])idatData[1 + (y * idatStride) .. (y + 1) * idatStride];
924 		src.copyScanline(y, rowPixels);
925 
926 		version (LittleEndian)
927 			static if (ChannelType!COLOR.sizeof > 1)
928 				foreach (ref p; cast(ChannelType!COLOR[])rowPixels)
929 					p = swapEndian(p);
930 	}
931 	chunks ~= PNGChunk("IDAT", compress(idatData, compressionLevel));
932 	chunks ~= PNGChunk("IEND", null);
933 
934 	return makePNG(chunks);
935 }
936 
937 ubyte[] makePNG(PNGChunk[] chunks)
938 {
939 	import std.bitmanip : nativeToBigEndian;
940 
941 	size_t totalSize = pngSignature.length;
942 	foreach (chunk; chunks)
943 		totalSize += PNGChunkHeader.sizeof + chunk.data.length + PNGChunkFooter.sizeof;
944 	ubyte[] data = new ubyte[totalSize];
945 
946 	data[0 .. pngSignature.length] = pngSignature;
947 	size_t pos = pngSignature.length;
948 	foreach (chunk; chunks)
949 	{
950 		auto header = cast(PNGChunkHeader*)data.ptr;
951 		header.length = chunk.data.length.to!uint.nativeToBigEndian;
952 		header.type = chunk.type;
953 		pos += PNGChunkHeader.sizeof;
954 
955 		data[pos .. pos + chunk.data.length] = cast(ubyte[])chunk.data;
956 		pos += chunk.data.length;
957 
958 		auto footer = cast(PNGChunkFooter*)data.ptr;
959 		footer.crc32 = chunk.crc32.nativeToBigEndian;
960 		pos += PNGChunkFooter.sizeof;
961 	}
962 
963 	return data;
964 }
965 
966 unittest
967 {
968 	onePixel(RGB(1,2,3)).toPNG();
969 	onePixel(5).toPNG();
970 	onePixel(true).toPNG();
971 }