1 /*
2   Based on reference QOI reference implementation: https://github.com/phoboslab/qoi/blob/master/qoi.h
3   (by Dominic Szablewski - https://phoboslab.org)
4   Ported by aquaratixc (Oleg Bakharev) and LightHouse Software (lhs-blog.info)
5   
6   Written in D programming language
7 */
8 module qoiformats;
9 
10 private {
11 	import core.stdc.stdio;
12 	import core.stdc.stdlib : free, malloc;
13 	import core.stdc.string : memset;
14 	
15 	import std.algorithm : clamp;
16 	import std.conv : to;
17 	import std.string : format, toLower;
18 		
19 	template addProperty(T, string propertyName, string defaultValue = T.init.to!string)
20 	{	 
21 		const char[] addProperty = format(
22 			`
23 			private %2$s %1$s = %4$s;
24 	 
25 			void set%3$s(%2$s %1$s)
26 			{
27 				this.%1$s = %1$s;
28 			}
29 	 
30 			%2$s get%3$s()
31 			{
32 				return %1$s;
33 			}
34 			`,
35 			"_" ~ propertyName.toLower,
36 			T.stringof,
37 			propertyName,
38 			defaultValue
39 			);
40 	}
41 	
42 	enum QOI_CHANNELS : ubyte
43 	{
44 		RGB  = 3,
45 		RGBA = 4
46 	};
47 	
48 	enum QOI_COLORSPACE : ubyte
49 	{
50 		SRGB   = 0,
51 		LINEAR = 1
52 	};
53 	
54 	class QoiImageInfo
55 	{
56 		mixin(addProperty!(uint, "Width"));
57 		mixin(addProperty!(uint, "Height"));
58 		mixin(addProperty!(QOI_CHANNELS, "Channels", "QOI_CHANNELS.RGB"));
59 		mixin(addProperty!(QOI_COLORSPACE, "Colorspace", "QOI_COLORSPACE.SRGB"));
60 		
61 		this(
62 			uint width = 0, 
63 			uint height = 0, 
64 			QOI_CHANNELS channels = QOI_CHANNELS.RGB, 
65 			QOI_COLORSPACE colorspace = QOI_COLORSPACE.SRGB
66 		)
67 		{
68 			_width = width;
69 			_height = height;
70 			_channels = channels;
71 			_colorspace = colorspace;
72 		}
73 		
74 		override string toString()
75 		{		
76 			return format(
77 				"QoiImageInfo(width = %d, height = %d, channels = %s, colorspace = %s)", 
78 				_width,
79 				_height,
80 				_channels.to!string,
81 				_colorspace.to!string
82 			);
83 		}
84 	}
85 	
86 	class QoiOperation
87 	{
88 		static void write32(ubyte* bytes, int* p, uint v)
89 		{
90 			bytes[(*p)++] = (0xff000000 & v) >> 24;
91 			bytes[(*p)++] = (0x00ff0000 & v) >> 16;
92 			bytes[(*p)++] = (0x0000ff00 & v) >> 8;
93 			bytes[(*p)++] = (0x000000ff & v);
94 		}
95 		
96 		static uint read32(ubyte* bytes, int* p)
97 		{
98 			uint a = bytes[(*p)++];
99 			uint b = bytes[(*p)++];
100 			uint c = bytes[(*p)++];
101 			uint d = bytes[(*p)++];
102 			return (
103 				(a << 24) | (b << 16) | (c << 8) | d
104 			);
105 		}
106 		
107 		static uint hash32(RGBA rgba) 
108 		{
109 			return rgba.r * 3 + rgba.g * 5 + rgba.b * 7 + rgba.a * 11;
110 		}
111 	}
112 		
113 	enum QOI_OP : ubyte
114 	{
115 		INDEX =  0x00,
116 		DIFF  =  0x40,
117 		LUMA  =  0x80,
118 		RUN   =  0xc0,
119 		RGB   =  0xfe, 
120 		RGBA  =  0xff 
121 	}
122 	
123 	enum ubyte QOI_MASK_2 = 0xc0;
124 	
125 	enum uint QOI_MAGIC = (
126 		(cast(uint) 'q') << 24 | 
127 		(cast(uint) 'o') << 16 | 
128 		(cast(uint) 'i') <<  8 | 
129 		(cast(uint) 'f')
130 	);
131 	
132 	enum ubyte QOI_HEADER_SIZE = 14;
133 
134 	enum uint QOI_PIXELS_MAX = cast(uint) 400_000_000;
135 	
136 	static ubyte[8] QOI_PADDING = [0, 0, 0, 0, 0, 0, 0, 1];
137 	
138 	union RGBA {
139 		struct {ubyte r, g, b, a; };
140 		uint v;
141 	}
142 }
143 
144 class QoiColor
145 {
146 	private
147 	{
148 		RGBA _rgba;
149 	}
150 	
151 	this(ubyte R = 0, ubyte G = 0, ubyte B = 0, ubyte A = 0)
152 	{
153 		_rgba.r = R;
154 		_rgba.g = G;
155 		_rgba.b = B;
156 		_rgba.a = A;
157 	}
158 	
159 	ubyte getR()
160 	{
161 		return _rgba.r;
162 	}
163 	
164 	ubyte getG()
165 	{
166 		return _rgba.g;
167 	}
168 	
169 	ubyte getB()
170 	{
171 		return _rgba.b;
172 	}
173 	
174 	ubyte getA()
175 	{
176 		return _rgba.a;
177 	}
178 	
179 	void setR(ubyte r)
180 	{
181 		_rgba.r = r;
182 	}
183 	
184 	void setG(ubyte g)
185 	{
186 		_rgba.g = g;
187 	}
188 	
189 	void setB(ubyte b)
190 	{
191 		_rgba.b = b;
192 	}
193 	
194 	void setA(ubyte a)
195 	{
196 		_rgba.a = a;
197 	}
198 	
199 	RGBA get()
200 	{
201 		return _rgba;
202 	}
203 	
204 	const float luminance709()
205 	{
206 	   return (_rgba.r  * 0.2126f + _rgba.g  * 0.7152f + _rgba.b  * 0.0722f);
207 	}
208 	
209 	const float luminance601()
210 	{
211 	   return (_rgba.r * 0.3f + _rgba.g * 0.59f + _rgba.b * 0.11f);
212 	}
213 	
214 	const float luminanceAverage()
215 	{
216 	   return (_rgba.r + _rgba.g + _rgba.b) / 3.0;
217 	}
218 
219 	alias luminance = luminance709;
220 
221 	override string toString()
222 	{		
223 		return format(
224 			"QoiColor(%d, %d, %d, %d, I = %f)", 
225 			_rgba.r, _rgba.g, _rgba.b, _rgba.a, this.luminance
226 		);
227 	}
228 
229 	QoiColor opBinary(string op, T)(auto ref T rhs)
230 	{
231 		return mixin(
232 			format(`new QoiColor( 
233 				cast(ubyte) clamp((_rgba.r %1$s rhs), 0, 255),
234 				cast(ubyte) clamp((_rgba.g %1$s rhs), 0, 255),
235 				cast(ubyte) clamp((_rgba.b %1$s rhs), 0, 255),
236 				cast(ubyte) clamp((_rgba.a %1$s rhs), 0, 255)
237 				)
238 			`,
239 			op
240 			)
241 		);
242 	}
243 	
244 	QoiColor opBinary(string op)(QoiColor rhs)
245 	{
246 		return mixin(
247 			format(`new QoiColor( 
248 				cast(ubyte) clamp((_rgba.r %1$s rhs.getR), 0, 255),
249 				cast(ubyte) clamp((_rgba.g %1$s rhs.getG), 0, 255),
250 				cast(ubyte) clamp((_rgba.b %1$s rhs.getB), 0, 255),
251 				cast(ubyte) clamp((_rgba.a %1$s rhs.getA), 0, 255)
252 				)
253 			`,
254 			op
255 			)
256 		);
257 	}
258 	
259 	alias get this; 
260 }
261 
262 class QoiImage
263 {
264 	private
265 	{
266 		QoiColor[]   _image;
267 		QoiImageInfo  _info;
268 	}
269 	
270 	private
271 	{
272 		auto actualIndex(uint i)
273 		{
274 			auto S = _info.getHeight;
275 		
276 			return clamp(i, 0, S - 1);
277 		}
278 
279 		auto actualIndex(uint i, uint j)
280 		{
281 			auto W = cast(size_t) clamp(i, 0, this.getWidth - 1);
282 			auto H = cast(size_t) clamp(j, 0, this.getHeight - 1);
283 			auto S = this.getArea;
284 		
285 			return clamp(W + H * this.getWidth, 0, S);
286 		}
287 		
288 		void* encode(void* data, QoiImageInfo info, int* outputLength) 
289 		{
290 			RGBA[64] index;
291 			RGBA px, pxPrevious;
292 			int i, maximalSize, p, run;
293 			int pxLength, pxEnd, pxPosition, channels;
294 			ubyte* bytes, pixels;
295 		
296 		
297 			if (
298 				(data is null) || (outputLength is null) || (info is null) ||
299 				(info.getWidth == 0) || (info.getHeight == 0) ||
300 				(info.getChannels < 3) || (info.getChannels > 4 ) ||
301 				(info.getColorspace > 1) ||
302 				(info.getHeight >= QOI_PIXELS_MAX / info.getWidth)
303 			) {
304 				return null;
305 			}
306 		
307 			maximalSize = cast(int) (
308 				info.getWidth * info.getHeight * (info.getChannels + 1) +
309 				QOI_HEADER_SIZE + QOI_PADDING.length
310 			);
311 		
312 			p = 0;
313 			bytes = cast(ubyte*) malloc(maximalSize);
314 			if (!bytes) 
315 			{
316 				return null;
317 			}
318 		
319 			QoiOperation.write32(bytes, &p, QOI_MAGIC);
320 			QoiOperation.write32(bytes, &p, info.getWidth);
321 			QoiOperation.write32(bytes, &p, info.getHeight);
322 			bytes[p++] = cast(byte) info.getChannels;
323 			bytes[p++] = cast(byte) info.getColorspace;
324 		
325 			pixels = cast(ubyte*) data;
326 		
327 			memset(index.ptr, 0, index.length);
328 		
329 			run = 0;
330 			with (pxPrevious)
331 			{
332 				r = 0;
333 				g = 0;
334 				b = 0;
335 				a = 255;
336 			}
337 			px = pxPrevious;
338 		
339 			pxLength = info.getWidth * info.getHeight * info.getChannels;
340 			pxEnd = pxLength - info.getChannels;
341 			channels = info.getChannels;
342 		
343 			for (pxPosition = 0; pxPosition < pxLength; pxPosition += channels) 
344 			{
345 				if (channels == 4) 
346 				{
347 					px = *(cast(RGBA*)(pixels + pxPosition));
348 				}
349 				else 
350 				{
351 					with (px)
352 					{
353 						r = pixels[pxPosition + 0];
354 						g = pixels[pxPosition + 1];
355 						b = pixels[pxPosition + 2];
356 					}
357 				}
358 		
359 				if (px.v == pxPrevious.v) {
360 					run++;
361 					if (run == 62 || pxPosition == pxEnd) {
362 						bytes[p++] = cast(ubyte) (QOI_OP.RUN | (run - 1));
363 						run = 0;
364 					}
365 				}
366 				else {
367 					int index_pos;
368 		
369 					if (run > 0) {
370 						bytes[p++] = cast(ubyte) (QOI_OP.RUN | (run - 1));
371 						run = 0;
372 					}
373 		
374 					index_pos = QoiOperation.hash32(px) % 64;
375 		
376 					if (index[index_pos].v == px.v) {
377 						bytes[p++] = cast(byte) (QOI_OP.INDEX | index_pos);
378 					}
379 					else {
380 						index[index_pos] = px;
381 		
382 						if (px.a == pxPrevious.a) {
383 							byte vr = cast(byte) (px.r - pxPrevious.r);
384 							byte vg = cast(byte) (px.g - pxPrevious.g);
385 							byte vb = cast(byte) (px.b - pxPrevious.b);
386 							
387 							byte vg_r = cast(byte) (vr - vg);
388 							byte vg_b = cast(byte) (vb - vg);
389 		
390 							if (
391 								(vr > -3) && (vr < 2) &&
392 								(vg > -3) && (vg < 2) &&
393 								(vb > -3) && (vb < 2)
394 							) {
395 								bytes[p++] = cast(byte) (QOI_OP.DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2));
396 							}
397 							else if (
398 								(vg_r >  -9) && (vg_r <  8) &&
399 								(vg   > -33) && (vg   < 32) &&
400 								(vg_b >  -9) && (vg_b <  8)
401 							) {
402 								bytes[p++] = cast(byte) (QOI_OP.LUMA     | (vg   + 32));
403 								bytes[p++] = cast(byte) ((vg_r + 8) << 4 | (vg_b +  8));
404 							}
405 							else {
406 								bytes[p++] = QOI_OP.RGB;
407 								with (px)
408 								{
409 									bytes[p++] = r;
410 									bytes[p++] = g;
411 									bytes[p++] = b;
412 								}
413 							}
414 						}
415 						else {
416 							bytes[p++] = QOI_OP.RGBA;
417 							with (px)
418 							{
419 								bytes[p++] = r;
420 								bytes[p++] = g;
421 								bytes[p++] = b;
422 								bytes[p++] = a;
423 							}
424 						}
425 					}
426 				}
427 				pxPrevious = px;
428 			}
429 		
430 			for (i = 0; i < cast(int) QOI_PADDING.length; i++) 
431 			{
432 				bytes[p++] = QOI_PADDING[i];
433 			}
434 		
435 			*outputLength = p;
436 			return bytes;
437 		}
438 		
439 		void* decode(void* data, int size, QoiImageInfo info, int channels)
440 		{
441 			RGBA[64] index;
442 			RGBA px;
443 			ubyte* bytes, pixels;
444 			uint headerMagic;
445 			int p, run, pxLength, chunksLength, pxPosition;
446 		
447 			if (
448 				(data is null) || 
449 				(info is null) ||  
450 				(channels != 0 && channels != 3 && channels != 4) ||
451 			    (size < QOI_HEADER_SIZE + cast(int) QOI_PADDING.length)
452 			) 
453 			{
454 				return null;
455 			}
456 		
457 			bytes = cast(ubyte*) data;
458 		
459 			headerMagic = QoiOperation.read32(bytes, &p);
460 			
461 			with (info)
462 			{
463 				setWidth = QoiOperation.read32(bytes, &p);
464 				setHeight = QoiOperation.read32(bytes, &p);
465 				setChannels = cast(QOI_CHANNELS) bytes[p++];
466 				setColorspace = cast(QOI_COLORSPACE) bytes[p++];
467 			}
468 		
469 			if (
470 				(info.getWidth == 0)  || 
471 				(info.getHeight == 0) ||
472 				(info.getChannels < 3)   || 
473 				(info.getChannels > 4) ||
474 				(info.getColorspace > 1) ||
475 				(headerMagic != QOI_MAGIC) ||
476 				(info.getHeight >= QOI_PIXELS_MAX / info.getWidth)
477 			) 
478 			{
479 				return null;
480 			}
481 		
482 			if (channels == 0) 
483 			{
484 				channels = info.getChannels;
485 			}
486 		
487 			pxLength = info.getWidth * info.getHeight * channels;
488 			pixels = cast(ubyte*) malloc(pxLength);
489 			
490 			if (!pixels) 
491 			{
492 				return null;
493 			}
494 		
495 			memset(index.ptr, 0, index.length);
496 			
497 			with (px)
498 			{
499 				r = 0;
500 				g = 0;
501 				b = 0;
502 				a = 255;
503 			}
504 		
505 			chunksLength = size - cast(int) QOI_PADDING.length;
506 			
507 			for (pxPosition = 0; pxPosition < pxLength; pxPosition += channels) 
508 			{
509 				if (run > 0) 
510 				{
511 					run--;
512 				}
513 				else if (p < chunksLength) {
514 					int b1 = bytes[p++];
515 		
516 					if (b1 == QOI_OP.RGB) {
517 						with (px)
518 						{
519 							r = bytes[p++];
520 							g = bytes[p++];
521 							b = bytes[p++];
522 						}
523 					}
524 					else if (b1 == QOI_OP.RGBA) {
525 						with (px)
526 						{
527 							r = bytes[p++];
528 							g = bytes[p++];
529 							b = bytes[p++];
530 							a = bytes[p++];
531 						}
532 					}
533 					else if ((b1 & QOI_MASK_2) == QOI_OP.INDEX) {
534 						px = index[b1];
535 					}
536 					else if ((b1 & QOI_MASK_2) == QOI_OP.DIFF) {
537 						with (px)
538 						{
539 							r += ((b1 >> 4) & 0x03) - 2;
540 							g += ((b1 >> 2) & 0x03) - 2;
541 							b += ( b1       & 0x03) - 2;
542 						}
543 					}
544 					else if ((b1 & QOI_MASK_2) == QOI_OP.LUMA) {
545 						int b2 = bytes[p++];
546 						int vg = (b1 & 0x3f) - 32;
547 						with (px)
548 						{
549 							r += vg - 8 + ((b2 >> 4) & 0x0f);
550 							g += vg;
551 							b += vg - 8 +  (b2       & 0x0f);
552 						}
553 					}
554 					else if ((b1 & QOI_MASK_2) == QOI_OP.RUN) {
555 						run = (b1 & 0x3f);
556 					}
557 		
558 					index[QoiOperation.hash32(px) % 64] = px;
559 				}
560 		
561 				if (channels == 4) 
562 				{
563 					*(cast(RGBA*)(pixels + pxPosition)) = px;
564 				}
565 				else 
566 				{
567 					with (px)
568 					{
569 						pixels[pxPosition + 0] = r;
570 						pixels[pxPosition + 1] = g;
571 						pixels[pxPosition + 2] = b;
572 					}
573 				}
574 			}
575 		
576 			return pixels;
577 		}
578 		
579 		
580 		void* read(char* filename, QoiImageInfo info, int channels) 
581 		{
582 			int size, bytesRead;
583 			void* pixels, data;
584 			
585 			FILE* f = fopen(filename, "rb");
586 			if (!f) 
587 			{
588 				return null;
589 			}
590 		
591 			fseek(f, 0, SEEK_END);
592 			size = cast(int) ftell(f);
593 			
594 			if (size <= 0) 
595 			{
596 				fclose(f);
597 				return null;
598 			}
599 			
600 			fseek(f, 0, SEEK_SET);
601 		
602 			data = malloc(size);
603 			
604 			if (!data) 
605 			{
606 				fclose(f);
607 				return null;
608 			}
609 		
610 			bytesRead = cast(int) fread(data, 1, size, f);
611 			fclose(f);
612 		
613 			pixels = decode(data, bytesRead, info, channels);
614 			free(data);
615 			
616 			return pixels;
617 		}
618 		
619 		int write(const char *filename, void* data, QoiImageInfo info) 
620 		{
621 			int size;
622 			void* encoded;
623 			
624 			FILE* f = fopen(filename, "wb");
625 			if (!f) 
626 			{
627 				return 0;
628 			}
629 		
630 			encoded = encode(data, info, &size);
631 			if (!encoded) 
632 			{
633 				fclose(f);
634 				return 0;
635 			}
636 		
637 			fwrite(encoded, 1, size, f);
638 			fclose(f);
639 		
640 			free(encoded);
641 			return size;
642 		}
643 	}
644 	
645 	this(
646 		uint width = 0, 
647 		uint height = 0, 
648 		QoiColor color = new QoiColor(0, 0, 0),
649 		QOI_CHANNELS channels = QOI_CHANNELS.RGB, 
650 		QOI_COLORSPACE colorspace = QOI_COLORSPACE.SRGB
651 	)
652 	{
653 		_info = new QoiImageInfo(width, height, channels, colorspace);
654 		
655 		foreach (x; 0..width)
656 		{
657 			foreach (y; 0..height)
658 			{
659 				_image ~= color;
660 			}	
661 		}
662 	}
663 	
664 	// image width
665 	uint getWidth()
666 	{
667 		return _info.getWidth;
668 	}
669 	
670 	// image height
671 	uint getHeight()
672 	{
673 		return _info.getHeight;
674 	}
675 	
676 	// image area
677 	uint getArea()
678 	{
679 		return _info.getWidth * _info.getHeight;
680 	}
681 	
682 	// image as array
683 	QoiColor[] getImage()
684 	{
685 		return _image;
686 	}
687 	
688 	// image info
689 	QoiImageInfo getInfo()
690 	{
691 		return _info;
692 	}
693 	
694 	// img[x, y] = color
695 	QoiColor opIndexAssign(QoiColor color, uint x, uint y)
696 	{
697 		_image[actualIndex(x, y)] = color;
698 		return color;
699 	}
700 
701 	// img[x] = color
702 	QoiColor opIndexAssign(QoiColor color, uint x)
703 	{
704 		_image[actualIndex(x)] = color;
705 		return color;
706 	}
707 
708 	// img[x, y]
709 	QoiColor opIndex(uint x, uint y)
710 	{
711 		return _image[actualIndex(x, y)];
712 	}
713 
714 	// img[x]
715 	QoiColor opIndex(uint x)
716 	{
717 		return _image[actualIndex(x)];
718 	}
719 
720 	// image as string
721 	override string toString()
722 	{
723 		string accumulator = "[";
724 
725 		foreach (x; 0..this.getWidth)
726 		{
727 			string tmp = "[";
728 			foreach (y; 0..this.getHeight)
729 			{
730 				tmp ~= _image[actualIndex(x, y)].toString ~ ", ";				
731 			}
732 			tmp = tmp[0..$-2] ~ "], ";
733 			accumulator ~= tmp;
734 		}
735 		return accumulator[0..$-2] ~ "]";
736 	}
737 	
738 	// load QOI file
739 	void load(string filename)
740 	{
741 		char* name = cast(char*) filename.ptr;
742 		void* data = read(name, _info, 0);
743 		
744 		if (data !is null)
745 		{
746 			auto channels = _info.getChannels;
747 			auto squared = this.area * channels;
748 			
749 			for (uint i = 0; i < squared; i += channels)
750 			{
751 				QoiColor rgba;
752 				
753 				final switch (channels) with (QOI_CHANNELS)
754 				{
755 					case RGB:
756 						auto q = cast(ubyte[]) data[i..i+3];
757 						rgba = new QoiColor(q[0], q[1], q[2]);
758 						break;
759 					case RGBA:
760 						auto q = cast(ubyte[]) data[i..i+4];
761 						rgba = new QoiColor(q[0], q[1], q[2], q[3]);
762 						break;
763 				}
764 				
765 				_image ~= rgba;
766 			}
767 		}
768 	}
769 	
770 	// save QOI file
771 	void save(string filename)
772 	{
773 		char* name = cast(char*) filename.ptr;
774 		auto channels = _info.getChannels;
775 		auto squared = this.area;
776 		
777 		ubyte[] data;
778 		
779 		for (uint i = 0; i < squared; i++)
780 		{
781 			auto q = _image[i];
782 			final switch (channels) with (QOI_CHANNELS)
783 			{
784 				case RGB:
785 					data ~= [q.getR, q.getG, q.getB];
786 					break;
787 				case RGBA:
788 					data ~= [q.getR, q.getG, q.getB, q.getA];
789 					break;
790 			}
791 		}
792 		
793 		write(name, cast(void*) data.ptr, _info);
794 	}
795 	
796 	
797 	// aliases
798 	alias width = getWidth;
799 	alias height = getHeight;
800 	alias area = getArea;
801 	alias image = getImage;
802 	alias info = getInfo;
803 }