The OpenD Programming Language

1 /++
2 	PNG file read and write. Leverages [arsd.color|color.d]'s [MemoryImage] interfaces for interop.
3 
4 	The main high-level functions you want are [readPng], [readPngFromBytes], [writePng], and maybe [writeImageToPngFile] or [writePngLazy] for some circumstances.
5 
6 	The other functions are low-level implementations and helpers for dissecting the png file format.
7 
8 	History:
9 		Originally written in 2009. This is why some of it is still written in a C-like style!
10 
11 	See_Also:
12 	$(LIST
13 		* [arsd.image] has generic load interfaces that can handle multiple file formats, including png.
14 		* [arsd.apng] handles the animated png extensions.
15 	)
16 +/
17 module arsd.png;
18 
19 import core.memory;
20 
21 /++
22 	Easily reads a png file into a [MemoryImage]
23 
24 	Returns:
25 		Please note this function doesn't return null right now, but you should still check for null anyway as that might change.
26 
27 		The returned [MemoryImage] is either a [IndexedImage] or a [TrueColorImage], depending on the file's color mode. You can cast it to one or the other, or just call [MemoryImage.getAsTrueColorImage] which will cast and return or convert as needed automatically.
28 
29 		Greyscale pngs and bit depths other than 8 are converted for the ease of the MemoryImage interface. If you need more detail, try [PNG] and [getDatastream] etc.
30 +/
31 MemoryImage readPng(string filename) {
32 	import std.file;
33 	return imageFromPng(readPng(cast(ubyte[]) read(filename)));
34 }
35 
36 /++
37 	Easily reads a png from a data array into a MemoryImage.
38 
39 	History:
40 		Added December 29, 2021 (dub v10.5)
41 +/
42 MemoryImage readPngFromBytes(const(ubyte)[] bytes) {
43 	return imageFromPng(readPng(bytes));
44 }
45 
46 /++
47 	Saves a MemoryImage to a png file. See also: [writeImageToPngFile] which uses memory a little more efficiently
48 
49 	See_Also:
50 		[writePngToArray]
51 +/
52 void writePng(string filename, MemoryImage mi) {
53 	// FIXME: it would be nice to write the file lazily so we don't have so many intermediate buffers here
54 	import std.file;
55 	std.file.write(filename, writePngToArray(mi));
56 }
57 
58 /++
59 	Creates an in-memory png file from the given memory image, returning it.
60 
61 	History:
62 		Added April 21, 2023 (dub v11.0)
63 	See_Also:
64 		[writePng]
65 +/
66 ubyte[] writePngToArray(MemoryImage mi) {
67 	PNG* png;
68 	if(auto p = cast(IndexedImage) mi)
69 		png = pngFromImage(p);
70 	else if(auto p = cast(TrueColorImage) mi)
71 		png = pngFromImage(p);
72 	else assert(0);
73 	return writePng(png);
74 }
75 
76 /++
77 	Represents the different types of png files, with numbers matching what the spec gives for filevalues.
78 +/
79 enum PngType {
80 	greyscale = 0, /// The data must be `depth` bits per pixel
81 	truecolor = 2, /// The data will be RGB triples, so `depth * 3` bits per pixel. Depth must be 8 or 16.
82 	indexed = 3, /// The data must be `depth` bits per pixel, with a palette attached. Use [writePng] with [IndexedImage] for this mode. Depth must be <= 8.
83 	greyscale_with_alpha = 4, /// The data must be (grey, alpha) byte pairs for each pixel. Thus `depth * 2` bits per pixel. Depth must be 8 or 16.
84 	truecolor_with_alpha = 6 /// The data must be RGBA quads for each pixel. Thus, `depth * 4` bits per pixel. Depth must be 8 or 16.
85 }
86 
87 /++
88 	Saves an image from an existing array of pixel data. Note that depth other than 8 may not be implemented yet. Also note depth of 16 must be stored big endian
89 +/
90 void writePng(string filename, const ubyte[] data, int width, int height, PngType type, ubyte depth = 8) {
91 	PngHeader h;
92 	h.width = width;
93 	h.height = height;
94 	h.type = cast(ubyte) type;
95 	h.depth = depth;
96 
97 	auto png = blankPNG(h);
98 	addImageDatastreamToPng(data, png);
99 
100 	import std.file;
101 	std.file.write(filename, writePng(png));
102 }
103 
104 
105 /*
106 //Here's a simple test program that shows how to write a quick image viewer with simpledisplay:
107 
108 import arsd.png;
109 import arsd.simpledisplay;
110 
111 import std.file;
112 void main(string[] args) {
113 	// older api, the individual functions give you more control if you need it
114 	//auto img = imageFromPng(readPng(cast(ubyte[]) read(args[1])));
115 
116 	// newer api, simpler but less control
117 	auto img = readPng(args[1]);
118 
119 	// displayImage is from simpledisplay and just pops up a window to show the image
120 	// simpledisplay's Images are a little different than MemoryImages that this loads,
121 	// but conversion is easy
122 	displayImage(Image.fromMemoryImage(img));
123 }
124 */
125 
126 // By Adam D. Ruppe, 2009-2010, released into the public domain
127 //import std.file;
128 
129 //import std.zlib;
130 
131 public import arsd.color;
132 
133 /**
134 	The return value should be casted to indexed or truecolor depending on what the file is. You can
135 	also use getAsTrueColorImage to forcibly convert it if needed.
136 
137 	To get an image from a png file, do something like this:
138 
139 	auto i = cast(TrueColorImage) imageFromPng(readPng(cast(ubyte)[]) std.file.read("file.png")));
140 */
141 MemoryImage imageFromPng(PNG* png) {
142 	PngHeader h = getHeader(png);
143 
144 	/** Types from the PNG spec:
145 		0 - greyscale
146 		2 - truecolor
147 		3 - indexed color
148 		4 - grey with alpha
149 		6 - true with alpha
150 
151 		1, 5, and 7 are invalid.
152 
153 		There's a kind of bitmask going on here:
154 			If type&1, it has a palette.
155 			If type&2, it is in color.
156 			If type&4, it has an alpha channel in the datastream.
157 	*/
158 
159 	MemoryImage i;
160 	ubyte[] idata;
161 	// FIXME: some duplication with the lazy reader below in the module
162 
163 	switch(h.type) {
164 		case 0: // greyscale
165 		case 4: // greyscale with alpha
166 			// this might be a different class eventually...
167 			auto a = new TrueColorImage(h.width, h.height);
168 			idata = a.imageData.bytes;
169 			i = a;
170 		break;
171 		case 2: // truecolor
172 		case 6: // truecolor with alpha
173 			auto a = new TrueColorImage(h.width, h.height);
174 			idata = a.imageData.bytes;
175 			i = a;
176 		break;
177 		case 3: // indexed
178 			auto a = new IndexedImage(h.width, h.height);
179 			a.palette = fetchPalette(png);
180 			a.hasAlpha = true; // FIXME: don't be so conservative here
181 			idata = a.data;
182 			i = a;
183 		break;
184 		default:
185 			assert(0, "invalid png");
186 	}
187 
188 	size_t idataIdx = 0;
189 
190 	auto file = LazyPngFile!(Chunk[])(png.chunks);
191 	immutable(ubyte)[] previousLine;
192 	auto bpp = bytesPerPixel(h);
193 	foreach(line; file.rawDatastreamByChunk()) {
194 		auto filter = line[0];
195 		auto data = unfilter(filter, line[1 .. $], previousLine, bpp);
196 		previousLine = data;
197 
198 		convertPngData(h.type, h.depth, data, h.width, idata, idataIdx);
199 	}
200 	assert(idataIdx == idata.length, "not all filled, wtf");
201 
202 	assert(i !is null);
203 
204 	return i;
205 }
206 
207 /+
208 	This is used by the load MemoryImage functions to convert the png'd datastream into the format MemoryImage's implementations expect.
209 
210 	idata needs to be already sized for the image! width * height if indexed, width*height*4 if not.
211 +/
212 void convertPngData(ubyte type, ubyte depth, const(ubyte)[] data, int width, ubyte[] idata, ref size_t idataIdx) {
213 	ubyte consumeOne() {
214 		ubyte ret = data[0];
215 		data = data[1 .. $];
216 		return ret;
217 	}
218 	import std.conv;
219 
220 	loop: for(int pixel = 0; pixel < width; pixel++)
221 		switch(type) {
222 			case 0: // greyscale
223 			case 4: // greyscale with alpha
224 			case 3: // indexed
225 
226 				void acceptPixel(ubyte p) {
227 					if(type == 3) {
228 						idata[idataIdx++] = p;
229 					} else {
230 						if(depth == 1) {
231 							p = p ? 0xff : 0;
232 						} else if (depth == 2) {
233 							p |= p << 2;
234 							p |= p << 4;
235 						}
236 						else if (depth == 4) {
237 							p |= p << 4;
238 						}
239 						idata[idataIdx++] = p;
240 						idata[idataIdx++] = p;
241 						idata[idataIdx++] = p;
242 
243 						if(type == 0)
244 							idata[idataIdx++] = 255;
245 						else if(type == 4)
246 							idata[idataIdx++] = consumeOne();
247 					}
248 				}
249 
250 				auto b = consumeOne();
251 				switch(depth) {
252 					case 1:
253 						acceptPixel((b >> 7) & 0x01);
254 						pixel++; if(pixel == width) break loop;
255 						acceptPixel((b >> 6) & 0x01);
256 						pixel++; if(pixel == width) break loop;
257 						acceptPixel((b >> 5) & 0x01);
258 						pixel++; if(pixel == width) break loop;
259 						acceptPixel((b >> 4) & 0x01);
260 						pixel++; if(pixel == width) break loop;
261 						acceptPixel((b >> 3) & 0x01);
262 						pixel++; if(pixel == width) break loop;
263 						acceptPixel((b >> 2) & 0x01);
264 						pixel++; if(pixel == width) break loop;
265 						acceptPixel((b >> 1) & 0x01);
266 						pixel++; if(pixel == width) break loop;
267 						acceptPixel(b & 0x01);
268 					break;
269 					case 2:
270 						acceptPixel((b >> 6) & 0x03);
271 						pixel++; if(pixel == width) break loop;
272 						acceptPixel((b >> 4) & 0x03);
273 						pixel++; if(pixel == width) break loop;
274 						acceptPixel((b >> 2) & 0x03);
275 						pixel++; if(pixel == width) break loop;
276 						acceptPixel(b & 0x03);
277 					break;
278 					case 4:
279 						acceptPixel((b >> 4) & 0x0f);
280 						pixel++; if(pixel == width) break loop;
281 						acceptPixel(b & 0x0f);
282 					break;
283 					case 8:
284 						acceptPixel(b);
285 					break;
286 					case 16:
287 						assert(type != 3); // 16 bit indexed isn't supported per png spec
288 						acceptPixel(b);
289 						consumeOne(); // discarding the least significant byte as we can't store it anyway
290 					break;
291 					default:
292 						assert(0, "bit depth not implemented");
293 				}
294 			break;
295 			case 2: // truecolor
296 			case 6: // true with alpha
297 				if(depth == 8) {
298 					idata[idataIdx++] = consumeOne();
299 					idata[idataIdx++] = consumeOne();
300 					idata[idataIdx++] = consumeOne();
301 					idata[idataIdx++] = (type == 6) ? consumeOne() : 255;
302 				} else if(depth == 16) {
303 					idata[idataIdx++] = consumeOne();
304 					consumeOne();
305 					idata[idataIdx++] = consumeOne();
306 					consumeOne();
307 					idata[idataIdx++] = consumeOne();
308 					consumeOne();
309 					idata[idataIdx++] = (type == 6) ? consumeOne() : 255;
310 					if(type == 6)
311 						consumeOne();
312 
313 				} else assert(0, "unsupported truecolor bit depth " ~ to!string(depth));
314 			break;
315 			default: assert(0);
316 		}
317 	assert(data.length == 0, "not all consumed, wtf " ~ to!string(data));
318 }
319 
320 /*
321 struct PngHeader {
322 	uint width;
323 	uint height;
324 	ubyte depth = 8;
325 	ubyte type = 6; // 0 - greyscale, 2 - truecolor, 3 - indexed color, 4 - grey with alpha, 6 - true with alpha
326 	ubyte compressionMethod = 0; // should be zero
327 	ubyte filterMethod = 0; // should be zero
328 	ubyte interlaceMethod = 0; // bool
329 }
330 */
331 
332 
333 /++
334 	Creates the [PNG] data structure out of an [IndexedImage]. This structure will have the minimum number of colors
335 	needed to represent the image faithfully in the file and will be ready for writing to a file.
336 
337 	This is called by [writePng].
338 +/
339 PNG* pngFromImage(IndexedImage i) {
340 	PngHeader h;
341 	h.width = i.width;
342 	h.height = i.height;
343 	h.type = 3;
344 	if(i.numColors() <= 2)
345 		h.depth = 1;
346 	else if(i.numColors() <= 4)
347 		h.depth = 2;
348 	else if(i.numColors() <= 16)
349 		h.depth = 4;
350 	else if(i.numColors() <= 256)
351 		h.depth = 8;
352 	else throw new Exception("can't save this as an indexed png");
353 
354 	auto png = blankPNG(h);
355 
356 	// do palette and alpha
357 	// FIXME: if there is only one transparent color, set it as the special chunk for that
358 
359 	// FIXME: we'd get a smaller file size if the transparent pixels were arranged first
360 	Chunk palette;
361 	palette.type = ['P', 'L', 'T', 'E'];
362 	palette.size = cast(int) i.palette.length * 3;
363 	palette.payload.length = palette.size;
364 
365 	Chunk alpha;
366 	if(i.hasAlpha) {
367 		alpha.type = ['t', 'R', 'N', 'S'];
368 		alpha.size = cast(uint) i.palette.length;
369 		alpha.payload.length = alpha.size;
370 	}
371 
372 	for(int a = 0; a < i.palette.length; a++) {
373 		palette.payload[a*3+0] = i.palette[a].r;
374 		palette.payload[a*3+1] = i.palette[a].g;
375 		palette.payload[a*3+2] = i.palette[a].b;
376 		if(i.hasAlpha)
377 			alpha.payload[a] = i.palette[a].a;
378 	}
379 
380 	palette.checksum = crc("PLTE", palette.payload);
381 	png.chunks ~= palette;
382 	if(i.hasAlpha) {
383 		alpha.checksum = crc("tRNS", alpha.payload);
384 		png.chunks ~= alpha;
385 	}
386 
387 	// do the datastream
388 	if(h.depth == 8) {
389 		addImageDatastreamToPng(i.data, png);
390 	} else {
391 		// gotta convert it
392 
393 		auto bitsPerLine = i.width * h.depth;
394 		if(bitsPerLine % 8 != 0)
395 			bitsPerLine = bitsPerLine / 8 + 1;
396 		else
397 			bitsPerLine = bitsPerLine / 8;
398 
399 		ubyte[] datastream = new ubyte[bitsPerLine * i.height];
400 		int shift = 0;
401 
402 		switch(h.depth) {
403 			default: assert(0);
404 			case 1: shift = 7; break;
405 			case 2: shift = 6; break;
406 			case 4: shift = 4; break;
407 			case 8: shift = 0; break;
408 		}
409 		size_t dsp = 0;
410 		size_t dpos = 0;
411 		bool justAdvanced;
412 		for(int y = 0; y < i.height; y++) {
413 		for(int x = 0; x < i.width; x++) {
414 			datastream[dsp] |= i.data[dpos++] << shift;
415 
416 			switch(h.depth) {
417 				default: assert(0);
418 				case 1: shift-= 1; break;
419 				case 2: shift-= 2; break;
420 				case 4: shift-= 4; break;
421 				case 8: shift-= 8; break;
422 			}
423 
424 			justAdvanced = shift < 0;
425 			if(shift < 0) {
426 				dsp++;
427 				switch(h.depth) {
428 					default: assert(0);
429 					case 1: shift = 7; break;
430 					case 2: shift = 6; break;
431 					case 4: shift = 4; break;
432 					case 8: shift = 0; break;
433 				}
434 			}
435 		}
436 			if(!justAdvanced)
437 				dsp++;
438 			switch(h.depth) {
439 				default: assert(0);
440 				case 1: shift = 7; break;
441 				case 2: shift = 6; break;
442 				case 4: shift = 4; break;
443 				case 8: shift = 0; break;
444 			}
445 
446 		}
447 
448 		addImageDatastreamToPng(datastream, png);
449 	}
450 
451 	return png;
452 }
453 
454 /++
455 	Creates the [PNG] data structure out of a [TrueColorImage]. This implementation currently always make
456 	the file a true color with alpha png type.
457 
458 	This is called by [writePng].
459 +/
460 
461 PNG* pngFromImage(TrueColorImage i) {
462 	PngHeader h;
463 	h.width = i.width;
464 	h.height = i.height;
465 	// FIXME: optimize it if it is greyscale or doesn't use alpha alpha
466 
467 	auto png = blankPNG(h);
468 	addImageDatastreamToPng(i.imageData.bytes, png);
469 
470 	return png;
471 }
472 
473 /*
474 void main(string[] args) {
475 	auto a = readPng(cast(ubyte[]) read(args[1]));
476 	auto f = getDatastream(a);
477 
478 	foreach(i; f) {
479 		writef("%d ", i);
480 	}
481 
482 	writefln("\n\n%d", f.length);
483 }
484 */
485 
486 /++
487 	Represents the PNG file's data. This struct is intended to be passed around by pointer.
488 +/
489 struct PNG {
490 	/++
491 		The length of the file.
492 	+/
493 	uint length;
494 	/++
495 		The PNG file magic number header. Please note the image data header is a IHDR chunk, not this (see [getHeader] for that). This just a static identifier
496 
497 		History:
498 			Prior to October 10, 2022, this was called `header`.
499 	+/
500 	ubyte[8] magic;// = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; // this is the only valid value but idk if it is worth changing here since the ctor sets it regardless.
501 	/// ditto
502 	deprecated("use `magic` instead") alias header = magic;
503 
504 	/++
505 		The array of chunks that make up the file contents. See [getChunkNullable], [getChunk], [insertChunk], and [replaceChunk] for functions to access and manipulate this array.
506 	+/
507 	Chunk[] chunks;
508 
509 	/++
510 		Gets the chunk with the given name, or throws if it cannot be found.
511 
512 		Returns:
513 			A non-null pointer to the chunk in the [chunks] array.
514 		Throws:
515 			an exception if the chunk can not be found. The type of this exception is subject to change at this time.
516 		See_Also:
517 			[getChunkNullable], which returns a null pointer instead of throwing.
518 	+/
519 	pure @trusted /* see note on getChunkNullable */
520 	Chunk* getChunk(string what) {
521 		foreach(ref c; chunks) {
522 			if(c.stype == what)
523 				return &c;
524 		}
525 		throw new Exception("no such chunk " ~ what);
526 	}
527 
528 	/++
529 		Gets the chunk with the given name, return `null` if it is not found.
530 
531 		See_Also:
532 			[getChunk], which throws if the chunk cannot be found.
533 	+/
534 	nothrow @nogc pure @trusted /* trusted because &c i know is referring to the dynamic array, not actually a local. That has lifetime at least as much of the parent PNG object. */
535 	Chunk* getChunkNullable(string what) {
536 		foreach(ref c; chunks) {
537 			if(c.stype == what)
538 				return &c;
539 		}
540 		return null;
541 	}
542 
543 	/++
544 		Insert chunk before IDAT. PNG specs allows to drop all chunks after IDAT,
545 		so we have to insert our custom chunks right before it.
546 		Use `Chunk.create()` to create new chunk, and then `insertChunk()` to add it.
547 		Return `true` if we did replacement.
548 	+/
549 	nothrow pure @trusted /* the chunks.ptr here fails safe, but it does that for performance and again I control that data so can be reasonably assured */
550 	bool insertChunk (Chunk* chk, bool replaceExisting=false) {
551 		if (chk is null) return false; // just in case
552 		// use reversed loop, as "IDAT" is usually present, and it is usually the last,
553 		// so we will somewhat amortize painter's algorithm here.
554 		foreach_reverse (immutable idx, ref cc; chunks) {
555 			if (replaceExisting && cc.type == chk.type) {
556 				// replace existing chunk, the easiest case
557 				chunks[idx] = *chk;
558 				return true;
559 			}
560 			if (cc.stype == "IDAT") {
561 				// ok, insert it; and don't use phobos
562 				chunks.length += 1;
563 				foreach_reverse (immutable c; idx+1..chunks.length) chunks.ptr[c] = chunks.ptr[c-1];
564 				chunks.ptr[idx] = *chk;
565 				return false;
566 			}
567 		}
568 		chunks ~= *chk;
569 		return false;
570 	}
571 
572 	/++
573 		Convenient wrapper for `insertChunk()`.
574 	+/
575 	nothrow pure @safe
576 	bool replaceChunk (Chunk* chk) { return insertChunk(chk, true); }
577 }
578 
579 /++
580 	this is just like writePng(filename, pngFromImage(image)), but it manages
581 	its own memory and writes straight to the file instead of using intermediate buffers that might not get gc'd right
582 +/
583 void writeImageToPngFile(in char[] filename, TrueColorImage image) {
584 	PNG* png;
585 	ubyte[] com;
586 {
587 	import std.zlib;
588 	PngHeader h;
589 	h.width = image.width;
590 	h.height = image.height;
591 	png = blankPNG(h);
592 
593 	size_t bytesPerLine = cast(size_t)h.width * 4;
594 	if(h.type == 3)
595 		bytesPerLine = cast(size_t)h.width * 8 / h.depth;
596 	Chunk dat;
597 	dat.type = ['I', 'D', 'A', 'T'];
598 	size_t pos = 0;
599 
600 	auto compressor = new Compress();
601 
602 	import core.stdc.stdlib;
603 	auto lineBuffer = (cast(ubyte*)malloc(1 + bytesPerLine))[0 .. 1+bytesPerLine];
604 	scope(exit) free(lineBuffer.ptr);
605 
606 	while(pos+bytesPerLine <= image.imageData.bytes.length) {
607 		lineBuffer[0] = 0;
608 		lineBuffer[1..1+bytesPerLine] = image.imageData.bytes[pos.. pos+bytesPerLine];
609 		com ~= cast(ubyte[]) compressor.compress(lineBuffer);
610 		pos += bytesPerLine;
611 	}
612 
613 	com ~= cast(ubyte[]) compressor.flush();
614 
615 	assert(com.length <= uint.max);
616 	dat.size = cast(uint) com.length;
617 	dat.payload = com;
618 	dat.checksum = crc("IDAT", dat.payload);
619 
620 	png.chunks ~= dat;
621 
622 	Chunk c;
623 
624 	c.size = 0;
625 	c.type = ['I', 'E', 'N', 'D'];
626 	c.checksum = crc("IEND", c.payload);
627 
628 	png.chunks ~= c;
629 }
630 	assert(png !is null);
631 
632 	import core.stdc.stdio;
633 	import std.string;
634 	FILE* fp = fopen(toStringz(filename), "wb");
635 	if(fp is null)
636 		throw new Exception("Couldn't open png file for writing.");
637 	scope(exit) fclose(fp);
638 
639 	fwrite(png.magic.ptr, 1, 8, fp);
640 	foreach(c; png.chunks) {
641 		fputc((c.size & 0xff000000) >> 24, fp);
642 		fputc((c.size & 0x00ff0000) >> 16, fp);
643 		fputc((c.size & 0x0000ff00) >> 8, fp);
644 		fputc((c.size & 0x000000ff) >> 0, fp);
645 
646 		fwrite(c.type.ptr, 1, 4, fp);
647 		fwrite(c.payload.ptr, 1, c.size, fp);
648 
649 		fputc((c.checksum & 0xff000000) >> 24, fp);
650 		fputc((c.checksum & 0x00ff0000) >> 16, fp);
651 		fputc((c.checksum & 0x0000ff00) >> 8, fp);
652 		fputc((c.checksum & 0x000000ff) >> 0, fp);
653 	}
654 
655 	{ import core.memory : GC; GC.free(com.ptr); } // there is a reference to this in the PNG struct, but it is going out of scope here too, so who cares
656 	// just wanna make sure this crap doesn't stick around
657 }
658 
659 /++
660 	Turns a [PNG] structure into an array of bytes, ready to be written to a file.
661 +/
662 ubyte[] writePng(PNG* p) {
663 	ubyte[] a;
664 	if(p.length)
665 		a.length = p.length;
666 	else {
667 		a.length = 8;
668 		foreach(c; p.chunks)
669 			a.length += c.size + 12;
670 	}
671 	size_t pos;
672 
673 	a[0..8] = p.magic[0..8];
674 	pos = 8;
675 	foreach(c; p.chunks) {
676 		a[pos++] = (c.size & 0xff000000) >> 24;
677 		a[pos++] = (c.size & 0x00ff0000) >> 16;
678 		a[pos++] = (c.size & 0x0000ff00) >> 8;
679 		a[pos++] = (c.size & 0x000000ff) >> 0;
680 
681 		a[pos..pos+4] = c.type[0..4];
682 		pos += 4;
683 		a[pos..pos+c.size] = c.payload[0..c.size];
684 		pos += c.size;
685 
686 		a[pos++] = (c.checksum & 0xff000000) >> 24;
687 		a[pos++] = (c.checksum & 0x00ff0000) >> 16;
688 		a[pos++] = (c.checksum & 0x0000ff00) >> 8;
689 		a[pos++] = (c.checksum & 0x000000ff) >> 0;
690 	}
691 
692 	return a;
693 }
694 
695 /++
696 	Opens a file and pulls the [PngHeader] out, leaving the rest of the data alone.
697 
698 	This might be useful when you're only interested in getting a file's image size or
699 	other basic metainfo without loading the whole thing.
700 +/
701 PngHeader getHeaderFromFile(string filename) {
702 	import std.stdio;
703 	auto file = File(filename, "rb");
704 	ubyte[12] initialBuffer; // file header + size of first chunk (should be IHDR)
705 	auto data = file.rawRead(initialBuffer[]);
706 	if(data.length != 12)
707 		throw new Exception("couldn't get png file header off " ~ filename);
708 
709 	if(data[0..8] != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
710 		throw new Exception("file " ~ filename ~ " is not a png");
711 
712 	size_t pos = 8;
713 	size_t size;
714 	size |= data[pos++] << 24;
715 	size |= data[pos++] << 16;
716 	size |= data[pos++] << 8;
717 	size |= data[pos++] << 0;
718 
719 	size += 4; // chunk type
720 	size += 4; // checksum
721 
722 	ubyte[] more;
723 	more.length = size;
724 
725 	auto chunk = file.rawRead(more);
726 	if(chunk.length != size)
727 		throw new Exception("couldn't get png image header off " ~ filename);
728 
729 
730 	more = data ~ chunk;
731 
732 	auto png = readPng(more);
733 	return getHeader(png);
734 }
735 
736 /++
737 	Given an in-memory array of bytes from a PNG file, returns the parsed out [PNG] object.
738 
739 	You might want the other [readPng] overload instead, which returns an even more processed [MemoryImage] object.
740 +/
741 PNG* readPng(in ubyte[] data) {
742 	auto p = new PNG;
743 
744 	p.length = cast(int) data.length;
745 	p.magic[0..8] = data[0..8];
746 
747 	if(p.magic != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
748 		throw new Exception("not a png, header wrong");
749 
750 	size_t pos = 8;
751 
752 	while(pos < data.length && data.length - pos >= 12) {
753 		Chunk n;
754 		n.size |= data[pos++] << 24;
755 		n.size |= data[pos++] << 16;
756 		n.size |= data[pos++] << 8;
757 		n.size |= data[pos++] << 0;
758 		n.type[0..4] = data[pos..pos+4];
759 		pos += 4;
760 		n.payload.length = n.size;
761 		if(pos + n.size > data.length)
762 			throw new Exception(format("malformed png, chunk '%s' %d @ %d longer than data %d", n.type, n.size, pos, data.length));
763 		if(pos + n.size < pos)
764 			throw new Exception("uint overflow: chunk too large");
765 		n.payload[0..n.size] = data[pos..pos+n.size];
766 		pos += n.size;
767 
768 		n.checksum |= data[pos++] << 24;
769 		n.checksum |= data[pos++] << 16;
770 		n.checksum |= data[pos++] << 8;
771 		n.checksum |= data[pos++] << 0;
772 
773 		p.chunks ~= n;
774 
775 		if(n.type == "IEND")
776 			break;
777 	}
778 
779 	return p;
780 }
781 
782 /++
783 	Creates a new [PNG] object from the given header parameters, ready to receive data.
784 +/
785 PNG* blankPNG(PngHeader h) {
786 	auto p = new PNG;
787 	p.magic = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
788 
789 	Chunk c;
790 
791 	c.size = 13;
792 	c.type = ['I', 'H', 'D', 'R'];
793 
794 	c.payload.length = 13;
795 	size_t pos = 0;
796 
797 	c.payload[pos++] = h.width >> 24;
798 	c.payload[pos++] = (h.width >> 16) & 0xff;
799 	c.payload[pos++] = (h.width >> 8) & 0xff;
800 	c.payload[pos++] = h.width & 0xff;
801 
802 	c.payload[pos++] = h.height >> 24;
803 	c.payload[pos++] = (h.height >> 16) & 0xff;
804 	c.payload[pos++] = (h.height >> 8) & 0xff;
805 	c.payload[pos++] = h.height & 0xff;
806 
807 	c.payload[pos++] = h.depth;
808 	c.payload[pos++] = h.type;
809 	c.payload[pos++] = h.compressionMethod;
810 	c.payload[pos++] = h.filterMethod;
811 	c.payload[pos++] = h.interlaceMethod;
812 
813 
814 	c.checksum = crc("IHDR", c.payload);
815 
816 	p.chunks ~= c;
817 
818 	return p;
819 }
820 
821 /+
822 	Implementation helper for creating png files.
823 
824 	Its API is subject to change; it would be private except it might be useful to you.
825 +/
826 // should NOT have any idata already.
827 // FIXME: doesn't handle palettes
828 void addImageDatastreamToPng(const(ubyte)[] data, PNG* png, bool addIend = true) {
829 	// we need to go through the lines and add the filter byte
830 	// then compress it into an IDAT chunk
831 	// then add the IEND chunk
832 	import std.zlib;
833 
834 	PngHeader h = getHeader(png);
835 
836 	if(h.depth == 0)
837 		throw new Exception("depth of zero makes no sense");
838 	if(h.width == 0)
839 		throw new Exception("width zero?!!?!?!");
840 
841 	int multiplier;
842 	size_t bytesPerLine;
843 	switch(h.type) {
844 		case 0:
845 			multiplier = 1;
846 		break;
847 		case 2:
848 			multiplier = 3;
849 		break;
850 		case 3:
851 			multiplier = 1;
852 		break;
853 		case 4:
854 			multiplier = 2;
855 		break;
856 		case 6:
857 			multiplier = 4;
858 		break;
859 		default: assert(0);
860 	}
861 
862 	bytesPerLine = h.width * multiplier * h.depth / 8;
863 	if((h.width * multiplier * h.depth) % 8 != 0)
864 		bytesPerLine += 1;
865 
866 	assert(bytesPerLine >= 1);
867 	Chunk dat;
868 	dat.type = ['I', 'D', 'A', 'T'];
869 	size_t pos = 0;
870 
871 	const(ubyte)[] output;
872 	while(pos+bytesPerLine <= data.length) {
873 		output ~= 0;
874 		output ~= data[pos..pos+bytesPerLine];
875 		pos += bytesPerLine;
876 	}
877 
878 	auto com = cast(ubyte[]) compress(output);
879 	dat.size = cast(int) com.length;
880 	dat.payload = com;
881 	dat.checksum = crc("IDAT", dat.payload);
882 
883 	png.chunks ~= dat;
884 
885 	if(addIend) {
886 		Chunk c;
887 
888 		c.size = 0;
889 		c.type = ['I', 'E', 'N', 'D'];
890 		c.checksum = crc("IEND", c.payload);
891 
892 		png.chunks ~= c;
893 	}
894 
895 }
896 
897 deprecated alias PngHeader PNGHeader;
898 
899 // bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey
900 
901 /+
902 	Uncompresses the raw datastream out of the file chunks, but does not continue processing it, so the scanlines are still filtered, etc.
903 +/
904 ubyte[] getDatastream(PNG* p) {
905 	import std.zlib;
906 	ubyte[] compressed;
907 
908 	foreach(c; p.chunks) {
909 		if(c.stype != "IDAT")
910 			continue;
911 		compressed ~= c.payload;
912 	}
913 
914 	return cast(ubyte[]) uncompress(compressed);
915 }
916 
917 /+
918 	Gets a raw datastream out of a 8 bpp png. See also [getANDMask]
919 +/
920 // FIXME: Assuming 8 bits per pixel
921 ubyte[] getUnfilteredDatastream(PNG* p) {
922 	PngHeader h = getHeader(p);
923 	assert(h.filterMethod == 0);
924 
925 	assert(h.type == 3); // FIXME
926 	assert(h.depth == 8); // FIXME
927 
928 	ubyte[] data = getDatastream(p);
929 	ubyte[] ufdata = new ubyte[data.length - h.height];
930 
931 	int bytesPerLine = cast(int) ufdata.length / h.height;
932 
933 	int pos = 0, pos2 = 0;
934 	for(int a = 0; a < h.height; a++) {
935 		assert(data[pos2] == 0);
936 		ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1];
937 		pos+= bytesPerLine;
938 		pos2+= bytesPerLine + 1;
939 	}
940 
941 	return ufdata;
942 }
943 
944 /+
945 	Gets the unfiltered raw datastream for conversion to Windows ico files. See also [getANDMask] and [fetchPaletteWin32].
946 +/
947 ubyte[] getFlippedUnfilteredDatastream(PNG* p) {
948 	PngHeader h = getHeader(p);
949 	assert(h.filterMethod == 0);
950 
951 	assert(h.type == 3); // FIXME
952 	assert(h.depth == 8 || h.depth == 4); // FIXME
953 
954 	ubyte[] data = getDatastream(p);
955 	ubyte[] ufdata = new ubyte[data.length - h.height];
956 
957 	int bytesPerLine = cast(int) ufdata.length / h.height;
958 
959 
960 	int pos = cast(int) ufdata.length - bytesPerLine, pos2 = 0;
961 	for(int a = 0; a < h.height; a++) {
962 		assert(data[pos2] == 0);
963 		ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1];
964 		pos-= bytesPerLine;
965 		pos2+= bytesPerLine + 1;
966 	}
967 
968 	return ufdata;
969 }
970 
971 ubyte getHighNybble(ubyte a) {
972 	return cast(ubyte)(a >> 4); // FIXME
973 }
974 
975 ubyte getLowNybble(ubyte a) {
976 	return a & 0x0f;
977 }
978 
979 /++
980 	Takes the transparency info and returns an AND mask suitable for use in a Windows ico
981 +/
982 ubyte[] getANDMask(PNG* p) {
983 	PngHeader h = getHeader(p);
984 	assert(h.filterMethod == 0);
985 
986 	assert(h.type == 3); // FIXME
987 	assert(h.depth == 8 || h.depth == 4); // FIXME
988 
989 	assert(h.width % 8 == 0); // might actually be %2
990 
991 	ubyte[] data = getDatastream(p);
992 	ubyte[] ufdata = new ubyte[h.height*((((h.width+7)/8)+3)&~3)]; // gotta pad to DWORDs...
993 
994 	Color[] colors = fetchPalette(p);
995 
996 	int pos = 0, pos2 = (h.width/((h.depth == 8) ? 1 : 2)+1)*(h.height-1);
997 	bool bits = false;
998 	for(int a = 0; a < h.height; a++) {
999 		assert(data[pos2++] == 0);
1000 		for(int b = 0; b < h.width; b++) {
1001 			if(h.depth == 4) {
1002 				ufdata[pos/8] |= ((colors[bits? getLowNybble(data[pos2]) : getHighNybble(data[pos2])].a <= 30) << (7-(pos%8)));
1003 			} else
1004 				ufdata[pos/8] |= ((colors[data[pos2]].a == 0) << (7-(pos%8)));
1005 			pos++;
1006 			if(h.depth == 4) {
1007 				if(bits) {
1008 					pos2++;
1009 				}
1010 				bits = !bits;
1011 			} else
1012 				pos2++;
1013 		}
1014 
1015 		int pad = 0;
1016 		for(; pad < ((pos/8) % 4); pad++) {
1017 			ufdata[pos/8] = 0;
1018 			pos+=8;
1019 		}
1020 		if(h.depth == 4)
1021 			pos2 -= h.width + 2;
1022 		else
1023 			pos2-= 2*(h.width) +2;
1024 	}
1025 
1026 	return ufdata;
1027 }
1028 
1029 // Done with assumption
1030 
1031 /++
1032 	Gets the parsed [PngHeader] data out of the [PNG] object.
1033 +/
1034 @nogc @safe pure
1035 PngHeader getHeader(PNG* p) {
1036 	PngHeader h;
1037 	ubyte[] data = p.getChunkNullable("IHDR").payload;
1038 
1039 	int pos = 0;
1040 
1041 	h.width |= data[pos++] << 24;
1042 	h.width |= data[pos++] << 16;
1043 	h.width |= data[pos++] << 8;
1044 	h.width |= data[pos++] << 0;
1045 
1046 	h.height |= data[pos++] << 24;
1047 	h.height |= data[pos++] << 16;
1048 	h.height |= data[pos++] << 8;
1049 	h.height |= data[pos++] << 0;
1050 
1051 	h.depth = data[pos++];
1052 	h.type = data[pos++];
1053 	h.compressionMethod = data[pos++];
1054 	h.filterMethod = data[pos++];
1055 	h.interlaceMethod = data[pos++];
1056 
1057 	return h;
1058 }
1059 
1060 /*
1061 struct Color {
1062 	ubyte r;
1063 	ubyte g;
1064 	ubyte b;
1065 	ubyte a;
1066 }
1067 */
1068 
1069 /+
1070 class Image {
1071 	Color[][] trueColorData;
1072 	ubyte[] indexData;
1073 
1074 	Color[] palette;
1075 
1076 	uint width;
1077 	uint height;
1078 
1079 	this(uint w, uint h) {}
1080 }
1081 
1082 Image fromPNG(PNG* p) {
1083 
1084 }
1085 
1086 PNG* toPNG(Image i) {
1087 
1088 }
1089 +/		struct RGBQUAD {
1090 			ubyte rgbBlue;
1091 			ubyte rgbGreen;
1092 			ubyte rgbRed;
1093 			ubyte rgbReserved;
1094 		}
1095 
1096 /+
1097 	Gets the palette out of the format Windows expects for bmp and ico files.
1098 
1099 	See also getANDMask
1100 +/
1101 RGBQUAD[] fetchPaletteWin32(PNG* p) {
1102 	RGBQUAD[] colors;
1103 
1104 	auto palette = p.getChunk("PLTE");
1105 
1106 	colors.length = (palette.size) / 3;
1107 
1108 	for(int i = 0; i < colors.length; i++) {
1109 		colors[i].rgbRed = palette.payload[i*3+0];
1110 		colors[i].rgbGreen = palette.payload[i*3+1];
1111 		colors[i].rgbBlue = palette.payload[i*3+2];
1112 		colors[i].rgbReserved = 0;
1113 	}
1114 
1115 	return colors;
1116 
1117 }
1118 
1119 /++
1120 	Extracts the palette chunk from a PNG object as an array of RGBA quads.
1121 
1122 	See_Also:
1123 		[replacePalette]
1124 +/
1125 Color[] fetchPalette(PNG* p) {
1126 	Color[] colors;
1127 
1128 	auto header = getHeader(p);
1129 	if(header.type == 0) { // greyscale
1130 		colors.length = 256;
1131 		foreach(i; 0..256)
1132 			colors[i] = Color(cast(ubyte) i, cast(ubyte) i, cast(ubyte) i);
1133 		return colors;
1134 	}
1135 
1136 	// assuming this is indexed
1137 	assert(header.type == 3);
1138 
1139 	auto palette = p.getChunk("PLTE");
1140 
1141 	Chunk* alpha = p.getChunkNullable("tRNS");
1142 
1143 	colors.length = palette.size / 3;
1144 
1145 	for(int i = 0; i < colors.length; i++) {
1146 		colors[i].r = palette.payload[i*3+0];
1147 		colors[i].g = palette.payload[i*3+1];
1148 		colors[i].b = palette.payload[i*3+2];
1149 		if(alpha !is null && i < alpha.size)
1150 			colors[i].a = alpha.payload[i];
1151 		else
1152 			colors[i].a = 255;
1153 
1154 		//writefln("%2d: %3d %3d %3d %3d", i, colors[i].r, colors[i].g, colors[i].b, colors[i].a);
1155 	}
1156 
1157 	return colors;
1158 }
1159 
1160 /++
1161 	Replaces the palette data in a [PNG] object.
1162 
1163 	See_Also:
1164 		[fetchPalette]
1165 +/
1166 void replacePalette(PNG* p, Color[] colors) {
1167 	auto palette = p.getChunk("PLTE");
1168 	auto alpha = p.getChunkNullable("tRNS");
1169 
1170 	//import std.string;
1171 	//assert(0, format("%s %s", colors.length, alpha.size));
1172 	//assert(colors.length == alpha.size);
1173 	if(alpha) {
1174 		alpha.size = cast(int) colors.length;
1175 		alpha.payload.length = colors.length; // we make sure there's room for our simple method below
1176 	}
1177 	p.length = 0; // so write will recalculate
1178 
1179 	for(int i = 0; i < colors.length; i++) {
1180 		palette.payload[i*3+0] = colors[i].r;
1181 		palette.payload[i*3+1] = colors[i].g;
1182 		palette.payload[i*3+2] = colors[i].b;
1183 		if(alpha)
1184 			alpha.payload[i] = colors[i].a;
1185 	}
1186 
1187 	palette.checksum = crc("PLTE", palette.payload);
1188 	if(alpha)
1189 		alpha.checksum = crc("tRNS", alpha.payload);
1190 }
1191 
1192 @safe nothrow pure @nogc
1193 uint update_crc(in uint crc, in ubyte[] buf){
1194 	static const uint[256] crc_table = [0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117];
1195 
1196 	uint c = crc;
1197 
1198 	foreach(b; buf)
1199 		c = crc_table[(c ^ b) & 0xff] ^ (c >> 8);
1200 
1201 	return c;
1202 }
1203 
1204 /+
1205 	Figures out the crc for a chunk. Used internally.
1206 
1207 	lol is just the chunk name
1208 +/
1209 uint crc(in string lol, in ubyte[] buf){
1210 	uint c = update_crc(0xffffffffL, cast(ubyte[]) lol);
1211 	return update_crc(c, buf) ^ 0xffffffffL;
1212 }
1213 
1214 
1215 /* former module arsd.lazypng follows */
1216 
1217 // this is like png.d but all range based so more complicated...
1218 // and I don't remember how to actually use it.
1219 
1220 // some day I'll prolly merge it with png.d but for now just throwing it up there
1221 
1222 //module arsd.lazypng;
1223 
1224 //import arsd.color;
1225 
1226 //import std.stdio;
1227 
1228 import std.range;
1229 import std.traits;
1230 import std.exception;
1231 import std.string;
1232 //import std.conv;
1233 
1234 /*
1235 struct Color {
1236 	ubyte r;
1237 	ubyte g;
1238 	ubyte b;
1239 	ubyte a;
1240 
1241 	string toString() {
1242 		return format("#%2x%2x%2x %2x", r, g, b, a);
1243 	}
1244 }
1245 */
1246 
1247 //import arsd.simpledisplay;
1248 
1249 struct RgbaScanline {
1250 	Color[] pixels;
1251 }
1252 
1253 
1254 // lazy range to convert some png scanlines into greyscale. part of an experiment i didn't do much with but still use sometimes.
1255 auto convertToGreyscale(ImageLines)(ImageLines lines)
1256 	if(isInputRange!ImageLines && is(ElementType!ImageLines == RgbaScanline))
1257 {
1258 	struct GreyscaleLines {
1259 		ImageLines lines;
1260 		bool isEmpty;
1261 		this(ImageLines lines) {
1262 			this.lines = lines;
1263 			if(!empty())
1264 				popFront(); // prime
1265 		}
1266 
1267 		int length() {
1268 			return lines.length;
1269 		}
1270 
1271 		bool empty() {
1272 			return isEmpty;
1273 		}
1274 
1275 		RgbaScanline current;
1276 		RgbaScanline front() {
1277 			return current;
1278 		}
1279 
1280 		void popFront() {
1281 			if(lines.empty()) {
1282 				isEmpty = true;
1283 				return;
1284 			}
1285 			auto old = lines.front();
1286 			current.pixels.length = old.pixels.length;
1287 			foreach(i, c; old.pixels) {
1288 				ubyte v = cast(ubyte) (
1289 					cast(int) c.r * 0.30 +
1290 					cast(int) c.g * 0.59 +
1291 					cast(int) c.b * 0.11);
1292 				current.pixels[i] = Color(v, v, v, c.a);
1293 			}
1294 			lines.popFront;
1295 		}
1296 	}
1297 
1298 	return GreyscaleLines(lines);
1299 }
1300 
1301 
1302 
1303 
1304 /// Lazily breaks the buffered input range into
1305 /// png chunks, as defined in the PNG spec
1306 ///
1307 /// Note: bufferedInputRange is defined in this file too.
1308 LazyPngChunks!(Range) readPngChunks(Range)(Range r)
1309 	if(isBufferedInputRange!(Range) && is(ElementType!(Range) == ubyte[]))
1310 {
1311 	// First, we need to check the header
1312 	// Then we'll lazily pull the chunks
1313 
1314 	while(r.front.length < 8) {
1315 		enforce(!r.empty(), "This isn't big enough to be a PNG file");
1316 		r.appendToFront();
1317 	}
1318 
1319 	enforce(r.front[0..8] == PNG_MAGIC_NUMBER,
1320 		"The file's magic number doesn't look like PNG");
1321 
1322 	r.consumeFromFront(8);
1323 
1324 	return LazyPngChunks!Range(r);
1325 }
1326 
1327 /// Same as above, but takes a regular input range instead of a buffered one.
1328 /// Provided for easier compatibility with standard input ranges
1329 /// (for example, std.stdio.File.byChunk)
1330 auto readPngChunks(Range)(Range r)
1331 	if(!isBufferedInputRange!(Range) && isInputRange!(Range))
1332 {
1333 	return readPngChunks(BufferedInputRange!Range(r));
1334 }
1335 
1336 /// Given an input range of bytes, return a lazy PNG file
1337 auto pngFromBytes(Range)(Range r)
1338 	if(isInputRange!(Range) && is(ElementType!Range == ubyte[]))
1339 {
1340 	auto chunks = readPngChunks(r);
1341 	auto file = LazyPngFile!(typeof(chunks))(chunks);
1342 
1343 	return file;
1344 }
1345 
1346 /// See: [readPngChunks]
1347 struct LazyPngChunks(T)
1348 	if(isBufferedInputRange!(T) && is(ElementType!T == ubyte[]))
1349 {
1350 	T bytes;
1351 	Chunk current;
1352 
1353 	this(T range) {
1354 		bytes = range;
1355 		popFront(); // priming it
1356 	}
1357 
1358 	Chunk front() {
1359 		return current;
1360 	}
1361 
1362 	bool empty() {
1363 		return (bytes.front.length == 0 && bytes.empty);
1364 	}
1365 
1366 	void popFront() {
1367 		enforce(!empty());
1368 
1369 		while(bytes.front().length < 4) {
1370 			enforce(!bytes.empty,
1371 				format("Malformed PNG file - chunk size too short (%s < 4)",
1372 					bytes.front().length));
1373 			bytes.appendToFront();
1374 		}
1375 
1376 		Chunk n;
1377 		n.size |= bytes.front()[0] << 24;
1378 		n.size |= bytes.front()[1] << 16;
1379 		n.size |= bytes.front()[2] << 8;
1380 		n.size |= bytes.front()[3] << 0;
1381 
1382 		bytes.consumeFromFront(4);
1383 
1384 		while(bytes.front().length < n.size + 8) {
1385 			enforce(!bytes.empty,
1386 				format("Malformed PNG file - chunk too short (%s < %s)",
1387 					bytes.front.length, n.size));
1388 			bytes.appendToFront();
1389 		}
1390 		n.type[0 .. 4] = bytes.front()[0 .. 4];
1391 		bytes.consumeFromFront(4);
1392 
1393 		n.payload.length = n.size;
1394 		n.payload[0 .. n.size] = bytes.front()[0 .. n.size];
1395 		bytes.consumeFromFront(n.size);
1396 
1397 		n.checksum |= bytes.front()[0] << 24;
1398 		n.checksum |= bytes.front()[1] << 16;
1399 		n.checksum |= bytes.front()[2] << 8;
1400 		n.checksum |= bytes.front()[3] << 0;
1401 
1402 		bytes.consumeFromFront(4);
1403 
1404 		enforce(n.checksum == crcPng(n.stype, n.payload), "Chunk checksum didn't match");
1405 
1406 		current = n;
1407 	}
1408 }
1409 
1410 /// Lazily reads out basic info from a png (header, palette, image data)
1411 /// It will only allocate memory to read a palette, and only copies on
1412 /// the header and the palette. It ignores everything else.
1413 ///
1414 /// FIXME: it doesn't handle interlaced files.
1415 struct LazyPngFile(LazyPngChunksProvider)
1416 	if(isInputRange!(LazyPngChunksProvider) &&
1417 		is(ElementType!(LazyPngChunksProvider) == Chunk))
1418 {
1419 	LazyPngChunksProvider chunks;
1420 
1421 	this(LazyPngChunksProvider chunks) {
1422 		enforce(!chunks.empty(), "There are no chunks in this png");
1423 
1424 		header = PngHeader.fromChunk(chunks.front());
1425 		chunks.popFront();
1426 
1427 		// And now, find the datastream so we're primed for lazy
1428 		// reading, saving the palette and transparency info, if
1429 		// present
1430 
1431 		chunkLoop:
1432 		while(!chunks.empty()) {
1433 			auto chunk = chunks.front();
1434 			switch(chunks.front.stype) {
1435 				case "PLTE":
1436 					// if it is in color, palettes are
1437 					// always stored as 8 bit per channel
1438 					// RGB triplets Alpha is stored elsewhere.
1439 
1440 					// FIXME: doesn't do greyscale palettes!
1441 
1442 					enforce(chunk.size % 3 == 0);
1443 					palette.length = chunk.size / 3;
1444 
1445 					auto offset = 0;
1446 					foreach(i; 0 .. palette.length) {
1447 						palette[i] = Color(
1448 							chunk.payload[offset+0],
1449 							chunk.payload[offset+1],
1450 							chunk.payload[offset+2],
1451 							255);
1452 						offset += 3;
1453 					}
1454 				break;
1455 				case "tRNS":
1456 					// 8 bit channel in same order as
1457 					// palette
1458 
1459 					if(chunk.size > palette.length)
1460 						palette.length = chunk.size;
1461 
1462 					foreach(i, a; chunk.payload)
1463 						palette[i].a = a;
1464 				break;
1465 				case "IDAT":
1466 					// leave the datastream for later
1467 					break chunkLoop;
1468 				default:
1469 					// ignore chunks we don't care about
1470 			}
1471 			chunks.popFront();
1472 		}
1473 
1474 		this.chunks = chunks;
1475 		enforce(!chunks.empty() && chunks.front().stype == "IDAT",
1476 			"Malformed PNG file - no image data is present");
1477 	}
1478 
1479 	/// Lazily reads and decompresses the image datastream, returning chunkSize bytes of
1480 	/// it per front. It does *not* change anything, so the filter byte is still there.
1481 	///
1482 	/// If chunkSize == 0, it automatically calculates chunk size to give you data by line.
1483 	auto rawDatastreamByChunk(int chunkSize = 0) {
1484 		assert(chunks.front().stype == "IDAT");
1485 
1486 		if(chunkSize == 0)
1487 			chunkSize = bytesPerLine();
1488 
1489 		struct DatastreamByChunk(T) {
1490 			private import etc.c.zlib;
1491 			z_stream* zs; // we have to malloc this too, as dmd can move the struct, and zlib 1.2.10 is intolerant to that
1492 			int chunkSize;
1493 			int bufpos;
1494 			int plpos; // bytes eaten in current chunk payload
1495 			T chunks;
1496 			bool eoz;
1497 
1498 			this(int cs, T chunks) {
1499 				import core.stdc.stdlib : malloc;
1500 				import core.stdc.string : memset;
1501 				this.chunkSize = cs;
1502 				this.chunks = chunks;
1503 				assert(chunkSize > 0);
1504 				buffer = (cast(ubyte*)malloc(chunkSize))[0..chunkSize];
1505 				pkbuf = (cast(ubyte*)malloc(32768))[0..32768]; // arbitrary number
1506 				zs = cast(z_stream*)malloc(z_stream.sizeof);
1507 				memset(zs, 0, z_stream.sizeof);
1508 				zs.avail_in = 0;
1509 				zs.avail_out = 0;
1510 				auto res = inflateInit2(zs, 15);
1511 				assert(res == Z_OK);
1512 				popFront(); // priming
1513 			}
1514 
1515 			~this () {
1516 				version(arsdpng_debug) { import core.stdc.stdio : printf; printf("destroying lazy PNG reader...\n"); }
1517 				import core.stdc.stdlib : free;
1518 				if (zs !is null) { inflateEnd(zs); free(zs); }
1519 				if (pkbuf.ptr !is null) free(pkbuf.ptr);
1520 				if (buffer.ptr !is null) free(buffer.ptr);
1521 			}
1522 
1523 			@disable this (this); // no copies!
1524 
1525 			ubyte[] front () { return (bufpos > 0 ? buffer[0..bufpos] : null); }
1526 
1527 			ubyte[] buffer;
1528 			ubyte[] pkbuf; // we will keep some packed data here in case payload moves, lol
1529 
1530 			void popFront () {
1531 				bufpos = 0;
1532 				while (plpos != plpos.max && bufpos < chunkSize) {
1533 					// do we have some bytes in zstream?
1534 					if (zs.avail_in > 0) {
1535 						// just unpack
1536 						zs.next_out = cast(typeof(zs.next_out))(buffer.ptr+bufpos);
1537 						int rd = chunkSize-bufpos;
1538 						zs.avail_out = rd;
1539 						auto err = inflate(zs, Z_SYNC_FLUSH);
1540 						if (err != Z_STREAM_END && err != Z_OK) throw new Exception("PNG unpack error");
1541 						if (err == Z_STREAM_END) {
1542 							if(zs.avail_in != 0) {
1543 								// this thing is malformed..
1544 								// libpng would warn here "libpng warning: IDAT: Extra compressed data"
1545 								// i used to just throw with the assertion on the next line
1546 								// but now just gonna discard the extra data to be a bit more permissive
1547 								zs.avail_in = 0;
1548 							}
1549 							assert(zs.avail_in == 0);
1550 							eoz = true;
1551 						}
1552 						bufpos += rd-zs.avail_out;
1553 						continue;
1554 					}
1555 					// no more zstream bytes; do we have something in current chunk?
1556 					if (plpos == plpos.max || plpos >= chunks.front.payload.length) {
1557 						// current chunk is complete, do we have more chunks?
1558 						if (chunks.front.stype != "IDAT") break; // this chunk is not IDAT, that means that... alas
1559 						chunks.popFront(); // remove current IDAT
1560 						plpos = 0;
1561 						if (chunks.empty || chunks.front.stype != "IDAT") plpos = plpos.max; // special value
1562 						continue;
1563 					}
1564 					if (plpos < chunks.front.payload.length) {
1565 						// current chunk is not complete, get some more bytes from it
1566 						int rd = cast(int)(chunks.front.payload.length-plpos <= pkbuf.length ? chunks.front.payload.length-plpos : pkbuf.length);
1567 						assert(rd > 0);
1568 						pkbuf[0..rd] = chunks.front.payload[plpos..plpos+rd];
1569 						plpos += rd;
1570 						if (eoz) {
1571 							// we did hit end-of-stream, reinit zlib (well, well, i know that we can reset it... meh)
1572 							inflateEnd(zs);
1573 							zs.avail_in = 0;
1574 							zs.avail_out = 0;
1575 							auto res = inflateInit2(zs, 15);
1576 							assert(res == Z_OK);
1577 							eoz = false;
1578 						}
1579 						// setup read pointer
1580 						zs.next_in = cast(typeof(zs.next_in))pkbuf.ptr;
1581 						zs.avail_in = cast(uint)rd;
1582 						continue;
1583 					}
1584 					assert(0, "wtf?! we should not be here!");
1585 				}
1586 			}
1587 
1588 			bool empty () { return (bufpos == 0); }
1589 		}
1590 
1591 		return DatastreamByChunk!(typeof(chunks))(chunkSize, chunks);
1592 	}
1593 
1594 	// FIXME: no longer compiles
1595 	version(none)
1596 	auto byRgbaScanline() {
1597 		static struct ByRgbaScanline {
1598 			ReturnType!(rawDatastreamByChunk) datastream;
1599 			RgbaScanline current;
1600 			PngHeader header;
1601 			int bpp;
1602 			Color[] palette;
1603 
1604 			bool isEmpty = false;
1605 
1606 			bool empty() {
1607 				return isEmpty;
1608 			}
1609 
1610 			@property int length() {
1611 				return header.height;
1612 			}
1613 
1614 			// This is needed for the filter algorithms
1615 			immutable(ubyte)[] previousLine;
1616 
1617 			// FIXME: I think my range logic got screwed somewhere
1618 			// in the stack... this is messed up.
1619 			void popFront() {
1620 				assert(!empty());
1621 				if(datastream.empty()) {
1622 					isEmpty = true;
1623 					return;
1624 				}
1625 				current.pixels.length = header.width;
1626 
1627 				// ensure it is primed
1628 				if(datastream.front.length == 0)
1629 					datastream.popFront;
1630 
1631 				auto rawData = datastream.front();
1632 				auto filter = rawData[0];
1633 				auto data = unfilter(filter, rawData[1 .. $], previousLine, bpp);
1634 
1635 				if(data.length == 0) {
1636 					isEmpty = true;
1637 					return;
1638 				}
1639 
1640 				assert(data.length);
1641 
1642 				previousLine = data;
1643 
1644 				// FIXME: if it's rgba, this could probably be faster
1645 				assert(header.depth == 8,
1646 					"Sorry, depths other than 8 aren't implemented yet.");
1647 
1648 				auto offset = 0;
1649 				foreach(i; 0 .. header.width) {
1650 					switch(header.type) {
1651 						case 0: // greyscale
1652 						case 4: // grey with alpha
1653 							auto value = data[offset++];
1654 							current.pixels[i] = Color(
1655 								value,
1656 								value,
1657 								value,
1658 								(header.type == 4)
1659 									? data[offset++] : 255
1660 							);
1661 						break;
1662 						case 3: // indexed
1663 							current.pixels[i] = palette[data[offset++]];
1664 						break;
1665 						case 2: // truecolor
1666 						case 6: // true with alpha
1667 							current.pixels[i] = Color(
1668 								data[offset++],
1669 								data[offset++],
1670 								data[offset++],
1671 								(header.type == 6)
1672 									? data[offset++] : 255
1673 							);
1674 						break;
1675 						default:
1676 							throw new Exception("invalid png file");
1677 					}
1678 				}
1679 
1680 				assert(offset == data.length);
1681 				if(!datastream.empty())
1682 					datastream.popFront();
1683 			}
1684 
1685 			RgbaScanline front() {
1686 				return current;
1687 			}
1688 		}
1689 
1690 		assert(chunks.front.stype == "IDAT");
1691 
1692 		ByRgbaScanline range;
1693 		range.header = header;
1694 		range.bpp = bytesPerPixel;
1695 		range.palette = palette;
1696 		range.datastream = rawDatastreamByChunk(bytesPerLine());
1697 		range.popFront();
1698 
1699 		return range;
1700 	}
1701 
1702 	int bytesPerPixel() {
1703 		return .bytesPerPixel(header);
1704 	}
1705 
1706 	int bytesPerLine() {
1707 		return .bytesPerLineOfPng(header.depth, header.type, header.width);
1708 	}
1709 
1710 	PngHeader header;
1711 	Color[] palette;
1712 }
1713 
1714 // FIXME: doesn't handle interlacing... I think
1715 // note it returns the length including the filter byte!!
1716 @nogc @safe pure nothrow
1717 int bytesPerLineOfPng(ubyte depth, ubyte type, uint width) {
1718 	immutable bitsPerChannel = depth;
1719 
1720 	int bitsPerPixel = bitsPerChannel;
1721 	if(type & 2 && !(type & 1)) // in color, but no palette
1722 		bitsPerPixel *= 3;
1723 	if(type & 4) // has alpha channel
1724 		bitsPerPixel += bitsPerChannel;
1725 
1726 	immutable int sizeInBits = width * bitsPerPixel;
1727 
1728 	// need to round up to the nearest byte
1729 	int sizeInBytes = (sizeInBits + 7) / 8;
1730 
1731 	return sizeInBytes + 1; // the +1 is for the filter byte that precedes all lines
1732 }
1733 
1734 /**************************************************
1735  * Buffered input range - generic, non-image code
1736 ***************************************************/
1737 
1738 /// Is the given range a buffered input range? That is, an input range
1739 /// that also provides consumeFromFront(int) and appendToFront()
1740 ///
1741 /// THIS IS BAD CODE. I wrote it before understanding how ranges are supposed to work.
1742 template isBufferedInputRange(R) {
1743 	enum bool isBufferedInputRange =
1744 		isInputRange!(R) && is(typeof(
1745 	{
1746 		R r;
1747 		r.consumeFromFront(0);
1748 		r.appendToFront();
1749 	}()));
1750 }
1751 
1752 /// Allows appending to front on a regular input range, if that range is
1753 /// an array. It appends to the array rather than creating an array of
1754 /// arrays; it's meant to make the illusion of one continuous front rather
1755 /// than simply adding capability to walk backward to an existing input range.
1756 ///
1757 /// I think something like this should be standard; I find File.byChunk
1758 /// to be almost useless without this capability.
1759 
1760 // FIXME: what if Range is actually an array itself? We should just use
1761 // slices right into it... I guess maybe r.front() would be the whole
1762 // thing in that case though, so we would indeed be slicing in right now.
1763 // Gotta check it though.
1764 struct BufferedInputRange(Range)
1765 	if(isInputRange!(Range) && isArray!(ElementType!(Range)))
1766 {
1767 	private Range underlyingRange;
1768 	private ElementType!(Range) buffer;
1769 
1770 	/// Creates a buffer for the given range. You probably shouldn't
1771 	/// keep using the underlying range directly.
1772 	///
1773 	/// It assumes the underlying range has already been primed.
1774 	this(Range r) {
1775 		underlyingRange = r;
1776 		// Is this really correct? Want to make sure r.front
1777 		// is valid but it doesn't necessarily need to have
1778 		// more elements...
1779 		enforce(!r.empty());
1780 
1781 		buffer = r.front();
1782 		usingUnderlyingBuffer = true;
1783 	}
1784 
1785 	/// Forwards to the underlying range's empty function
1786 	bool empty() {
1787 		return underlyingRange.empty();
1788 	}
1789 
1790 	/// Returns the current buffer
1791 	ElementType!(Range) front() {
1792 		return buffer;
1793 	}
1794 
1795 	// actually, not terribly useful IMO. appendToFront calls it
1796 	// implicitly when necessary
1797 
1798 	/// Discard the current buffer and get the next item off the
1799 	/// underlying range. Be sure to call at least once to prime
1800 	/// the range (after checking if it is empty, of course)
1801 	void popFront() {
1802 		enforce(!empty());
1803 		underlyingRange.popFront();
1804 		buffer = underlyingRange.front();
1805 		usingUnderlyingBuffer = true;
1806 	}
1807 
1808 	bool usingUnderlyingBuffer = false;
1809 
1810 	/// Remove the first count items from the buffer
1811 	void consumeFromFront(int count) {
1812 		buffer = buffer[count .. $];
1813 	}
1814 
1815 	/// Append the next item available on the underlying range to
1816 	/// our buffer.
1817 	void appendToFront() {
1818 		if(buffer.length == 0) {
1819 			// may let us reuse the underlying range's buffer,
1820 			// hopefully avoiding an extra allocation
1821 			popFront();
1822 		} else {
1823 			enforce(!underlyingRange.empty());
1824 
1825 			// need to make sure underlyingRange.popFront doesn't overwrite any
1826 			// of our buffer...
1827 			if(usingUnderlyingBuffer) {
1828 				buffer = buffer.dup;
1829 				usingUnderlyingBuffer = false;
1830 			}
1831 
1832 			underlyingRange.popFront();
1833 
1834 			buffer ~= underlyingRange.front();
1835 		}
1836 	}
1837 }
1838 
1839 /**************************************************
1840  * Lower level implementations of image formats.
1841  * and associated helper functions.
1842  *
1843  * Related to the module, but not particularly
1844  * interesting, so it's at the bottom.
1845 ***************************************************/
1846 
1847 
1848 /* PNG file format implementation */
1849 
1850 //import std.zlib;
1851 import std.math;
1852 
1853 /// All PNG files are supposed to open with these bytes according to the spec
1854 static immutable(ubyte[]) PNG_MAGIC_NUMBER = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
1855 
1856 /// A PNG file consists of the magic number then a stream of chunks. This
1857 /// struct represents those chunks.
1858 struct Chunk {
1859 	uint size;
1860 	ubyte[4] type;
1861 	ubyte[] payload;
1862 	uint checksum;
1863 
1864 	/// returns the type as a string for easier comparison with literals
1865 	@nogc @safe nothrow pure
1866 	const(char)[] stype() return const {
1867 		return cast(const(char)[]) type;
1868 	}
1869 
1870 	@trusted nothrow pure /* trusted because of the cast of name to ubyte. It is copied into a new buffer anyway though so obviously harmless. */
1871 	static Chunk* create(string type, ubyte[] payload)
1872 		in {
1873 			assert(type.length == 4);
1874 		}
1875 	do {
1876 		Chunk* c = new Chunk;
1877 		c.size = cast(int) payload.length;
1878 		c.type[] = (cast(ubyte[]) type)[];
1879 		c.payload = payload;
1880 
1881 		c.checksum = crcPng(type, payload);
1882 
1883 		return c;
1884 	}
1885 
1886 	/// Puts it into the format for outputting to a file
1887 	@safe nothrow pure
1888 	ubyte[] toArray() {
1889 		ubyte[] a;
1890 		a.length = size + 12;
1891 
1892 		int pos = 0;
1893 
1894 		a[pos++] = (size & 0xff000000) >> 24;
1895 		a[pos++] = (size & 0x00ff0000) >> 16;
1896 		a[pos++] = (size & 0x0000ff00) >> 8;
1897 		a[pos++] = (size & 0x000000ff) >> 0;
1898 
1899 		a[pos .. pos + 4] = type[0 .. 4];
1900 		pos += 4;
1901 
1902 		a[pos .. pos + size] = payload[0 .. size];
1903 
1904 		pos += size;
1905 
1906 		assert(checksum);
1907 
1908 		a[pos++] = (checksum & 0xff000000) >> 24;
1909 		a[pos++] = (checksum & 0x00ff0000) >> 16;
1910 		a[pos++] = (checksum & 0x0000ff00) >> 8;
1911 		a[pos++] = (checksum & 0x000000ff) >> 0;
1912 
1913 		return a;
1914 	}
1915 }
1916 
1917 /// The first chunk in a PNG file is a header that contains this info
1918 struct PngHeader {
1919 	/// Width of the image, in pixels.
1920 	uint width;
1921 
1922 	/// Height of the image, in pixels.
1923 	uint height;
1924 
1925 	/**
1926 		This is bits per channel - per color for truecolor or grey
1927 		and per pixel for palette.
1928 
1929 		Indexed ones can have depth of 1,2,4, or 8,
1930 
1931 		Greyscale can be 1,2,4,8,16
1932 
1933 		Everything else must be 8 or 16.
1934 	*/
1935 	ubyte depth = 8;
1936 
1937 	/** Types from the PNG spec:
1938 		0 - greyscale
1939 		2 - truecolor
1940 		3 - indexed color
1941 		4 - grey with alpha
1942 		6 - true with alpha
1943 
1944 		1, 5, and 7 are invalid.
1945 
1946 		There's a kind of bitmask going on here:
1947 			If type&1, it has a palette.
1948 			If type&2, it is in color.
1949 			If type&4, it has an alpha channel in the datastream.
1950 	*/
1951 	ubyte type = 6;
1952 
1953 	ubyte compressionMethod = 0; /// should be zero
1954 	ubyte filterMethod = 0; /// should be zero
1955 	/// 0 is non interlaced, 1 if Adam7. No more are defined in the spec
1956 	ubyte interlaceMethod = 0;
1957 
1958 	pure @safe // @nogc with -dip1008 too......
1959 	static PngHeader fromChunk(in Chunk c) {
1960 		if(c.stype != "IHDR")
1961 			throw new Exception("The chunk is not an image header");
1962 
1963 		PngHeader h;
1964 		auto data = c.payload;
1965 		int pos = 0;
1966 
1967 		if(data.length != 13)
1968 			throw new Exception("Malformed PNG file - the IHDR is the wrong size");
1969 
1970 		h.width |= data[pos++] << 24;
1971 		h.width |= data[pos++] << 16;
1972 		h.width |= data[pos++] << 8;
1973 		h.width |= data[pos++] << 0;
1974 
1975 		h.height |= data[pos++] << 24;
1976 		h.height |= data[pos++] << 16;
1977 		h.height |= data[pos++] << 8;
1978 		h.height |= data[pos++] << 0;
1979 
1980 		h.depth = data[pos++];
1981 		h.type = data[pos++];
1982 		h.compressionMethod = data[pos++];
1983 		h.filterMethod = data[pos++];
1984 		h.interlaceMethod = data[pos++];
1985 
1986 		return h;
1987 	}
1988 
1989 	Chunk* toChunk() {
1990 		ubyte[] data;
1991 		data.length = 13;
1992 		int pos = 0;
1993 
1994 		data[pos++] = width >> 24;
1995 		data[pos++] = (width >> 16) & 0xff;
1996 		data[pos++] = (width >> 8) & 0xff;
1997 		data[pos++] = width & 0xff;
1998 
1999 		data[pos++] = height >> 24;
2000 		data[pos++] = (height >> 16) & 0xff;
2001 		data[pos++] = (height >> 8) & 0xff;
2002 		data[pos++] = height & 0xff;
2003 
2004 		data[pos++] = depth;
2005 		data[pos++] = type;
2006 		data[pos++] = compressionMethod;
2007 		data[pos++] = filterMethod;
2008 		data[pos++] = interlaceMethod;
2009 
2010 		assert(pos == 13);
2011 
2012 		return Chunk.create("IHDR", data);
2013 	}
2014 }
2015 
2016 /// turns a range of png scanlines into a png file in the output range. really weird
2017 void writePngLazy(OutputRange, InputRange)(ref OutputRange where, InputRange image)
2018 	if(
2019 		isOutputRange!(OutputRange, ubyte[]) &&
2020 		isInputRange!(InputRange) &&
2021 		is(ElementType!InputRange == RgbaScanline))
2022 {
2023 	import std.zlib;
2024 	where.put(PNG_MAGIC_NUMBER);
2025 	PngHeader header;
2026 
2027 	assert(!image.empty());
2028 
2029 	// using the default values for header here... FIXME not super clear
2030 
2031 	header.width = image.front.pixels.length;
2032 	header.height = image.length;
2033 
2034 	enforce(header.width > 0, "Image width <= 0");
2035 	enforce(header.height > 0, "Image height <= 0");
2036 
2037 	where.put(header.toChunk().toArray());
2038 
2039 	auto compressor = new std.zlib.Compress();
2040 	const(void)[] compressedData;
2041 	int cnt;
2042 	foreach(line; image) {
2043 		// YOU'VE GOT TO BE FUCKING KIDDING ME!
2044 		// I have to /cast/ to void[]!??!?
2045 
2046 		ubyte[] data;
2047 		data.length = 1 + header.width * 4;
2048 		data[0] = 0; // filter type
2049 		int offset = 1;
2050 		foreach(pixel; line.pixels) {
2051 			data[offset++] = pixel.r;
2052 			data[offset++] = pixel.g;
2053 			data[offset++] = pixel.b;
2054 			data[offset++] = pixel.a;
2055 		}
2056 
2057 		compressedData ~= compressor.compress(cast(void[])
2058 			data);
2059 		if(compressedData.length > 2_000) {
2060 			where.put(Chunk.create("IDAT", cast(ubyte[])
2061 				compressedData).toArray());
2062 			compressedData = null;
2063 		}
2064 
2065 		cnt++;
2066 	}
2067 
2068 	assert(cnt == header.height, format("Got %d lines instead of %d", cnt, header.height));
2069 
2070 	compressedData ~= compressor.flush();
2071 	if(compressedData.length)
2072 		where.put(Chunk.create("IDAT", cast(ubyte[])
2073 			compressedData).toArray());
2074 
2075 	where.put(Chunk.create("IEND", null).toArray());
2076 }
2077 
2078 // bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey
2079 
2080 @trusted nothrow pure @nogc /* trusted because of the cast from char to ubyte */
2081 uint crcPng(in char[] chunkName, in ubyte[] buf){
2082 	uint c = update_crc(0xffffffffL, cast(ubyte[]) chunkName);
2083 	return update_crc(c, buf) ^ 0xffffffffL;
2084 }
2085 
2086 /++
2087 	Png files apply a filter to each line in the datastream, hoping to aid in compression. This undoes that as you load.
2088 +/
2089 immutable(ubyte)[] unfilter(ubyte filterType, in ubyte[] data, in ubyte[] previousLine, int bpp) {
2090 	// Note: the overflow arithmetic on the ubytes in here is intentional
2091 	switch(filterType) {
2092 		case 0:
2093 			return data.idup; // FIXME is copying really necessary?
2094 		case 1:
2095 			auto arr = data.dup;
2096 			// first byte gets zero added to it so nothing special
2097 			foreach(i; bpp .. arr.length) {
2098 				arr[i] += arr[i - bpp];
2099 			}
2100 
2101 			return assumeUnique(arr);
2102 		case 2:
2103 			auto arr = data.dup;
2104 			if(previousLine.length)
2105 			foreach(i; 0 .. arr.length) {
2106 				arr[i] += previousLine[i];
2107 			}
2108 
2109 			return assumeUnique(arr);
2110 		case 3:
2111 			auto arr = data.dup;
2112 			foreach(i; 0 .. arr.length) {
2113 				auto left = i < bpp ? 0 : arr[i - bpp];
2114 				auto above = previousLine.length ? previousLine[i] : 0;
2115 
2116 				arr[i] += cast(ubyte) ((left + above) / 2);
2117 			}
2118 
2119 			return assumeUnique(arr);
2120 		case 4:
2121 			auto arr = data.dup;
2122 			foreach(i; 0 .. arr.length) {
2123 				ubyte prev   = i < bpp ? 0 : arr[i - bpp];
2124 				ubyte prevLL = i < bpp ? 0 : (i < previousLine.length ? previousLine[i - bpp] : 0);
2125 
2126 				arr[i] += PaethPredictor(prev, (i < previousLine.length ? previousLine[i] : 0), prevLL);
2127 			}
2128 
2129 			return assumeUnique(arr);
2130 		default:
2131 			throw new Exception("invalid PNG file, bad filter type");
2132 	}
2133 }
2134 
2135 ubyte PaethPredictor(ubyte a, ubyte b, ubyte c) {
2136 	int p = cast(int) a + b - c;
2137 	auto pa = abs(p - a);
2138 	auto pb = abs(p - b);
2139 	auto pc = abs(p - c);
2140 
2141 	if(pa <= pb && pa <= pc)
2142 		return a;
2143 	if(pb <= pc)
2144 		return b;
2145 	return c;
2146 }
2147 
2148 ///
2149 int bytesPerPixel(PngHeader header) {
2150 	immutable bitsPerChannel = header.depth;
2151 
2152 	int bitsPerPixel = bitsPerChannel;
2153 	if(header.type & 2 && !(header.type & 1)) // in color, but no palette
2154 		bitsPerPixel *= 3;
2155 	if(header.type & 4) // has alpha channel
2156 		bitsPerPixel += bitsPerChannel;
2157 
2158 	return (bitsPerPixel + 7) / 8;
2159 }