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