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