1 module ppmformats;
2 
3 private
4 {
5 	import std.conv;
6 	import std.stdio;
7 	import std.string;
8 	
9 	template addProperty(T, string propertyName, string defaultValue = T.init.to!string)
10 	{
11 		import std.string : format, toLower;
12 	 
13 		const char[] addProperty = format(
14 			`
15 			private %2$s %1$s = %4$s;
16 	 
17 			void set%3$s(%2$s %1$s)
18 			{
19 				this.%1$s = %1$s;
20 			}
21 	 
22 			%2$s get%3$s()
23 			{
24 				return %1$s;
25 			}
26 			`,
27 			"_" ~ propertyName.toLower,
28 			T.stringof,
29 			propertyName,
30 			defaultValue
31 			);
32 	}
33 
34 	auto EnumValue(E)(E e) 
35 		if(is(E == enum)) 
36 	{
37 		import std.traits : OriginalType;
38 		OriginalType!E tmp = e;
39 		return tmp; 
40 	}
41 	
42 	mixin template addConstructor(alias pmf)
43 	{
44 		this(size_t width = 0, size_t height = 0, RGBColor color = new RGBColor(0, 0, 0))
45 		{
46 			_image  = new PixMapImage(width, height, color);
47 			_header = pmf; 
48 		}
49 	
50 		alias image this;
51 	}
52 }
53 
54 class RGBColor
55 {
56 	mixin(addProperty!(int, "R"));
57 	mixin(addProperty!(int, "G"));
58 	mixin(addProperty!(int, "B"));
59 
60 	this(int R = 0, int G = 0, int B = 0)
61 	{
62 		this._r = R;
63 		this._g = G;
64 		this._b = B;
65 	}
66 
67 	const float luminance709()
68 	{
69 	   return (_r  * 0.2126f + _g  * 0.7152f + _b  * 0.0722f);
70 	}
71 	
72 	const float luminance601()
73 	{
74 	   return (_r * 0.3f + _g * 0.59f + _b * 0.11f);
75 	}
76 	
77 	const float luminanceAverage()
78 	{
79 	   return (_r + _g + _b) / 3.0;
80 	}
81 
82 	alias luminance = luminance709;
83 
84 	override string toString()
85 	{
86 		import std.string : format;
87 		
88 		return format("RGBColor(%d, %d, %d, I = %f)", _r, _g, _b, this.luminance);
89 	}
90 
91 	RGBColor opBinary(string op, T)(auto ref T rhs)
92 	{
93 		import std.algorithm : clamp;
94 		import std.string : format;
95 
96 		return mixin(
97 			format(`new RGBColor( 
98 				clamp(cast(int) (_r  %1$s rhs), 0, 255),
99 				clamp(cast(int) (_g  %1$s rhs), 0, 255),
100 				clamp(cast(int) (_b  %1$s rhs), 0, 255)
101 				)
102 			`,
103 			op
104 			)
105 		);
106 	}
107 
108 	RGBColor opBinary(string op)(RGBColor rhs)
109 	{
110 		import std.algorithm : clamp;
111 		import std.string : format;
112 
113 		return mixin(
114 			format(`new RGBColor( 
115 				clamp(cast(int) (_r  %1$s rhs.getR), 0, 255),
116 				clamp(cast(int) (_g  %1$s rhs.getG), 0, 255),
117 				clamp(cast(int) (_b  %1$s rhs.getB), 0, 255)
118 				)
119 			`,
120 			op
121 			)
122 		);
123 	}
124 }
125 
126 class PixMapImage
127 {
128 	mixin(addProperty!(size_t, "Width"));
129 	mixin(addProperty!(size_t, "Height"));
130 	
131 	private
132 	{
133 		RGBColor[] _image;
134 
135 		import std.algorithm : clamp;
136 
137 		auto actualIndex(size_t i)
138 		{
139 			auto S = _width * _height;
140 		
141 			return clamp(i, 0, S);
142 		}
143 
144 		auto actualIndex(size_t i, size_t j)
145 		{
146 			auto W = cast(size_t) clamp(i, 0, _width - 1);
147 			auto H = cast(size_t) clamp(j, 0, _height - 1);
148 			auto S = _width * _height;
149 		
150 			return clamp(W + H * _width, 0, S);
151 		}
152 	}
153 
154 	this(size_t width = 0, size_t height = 0, RGBColor color = new RGBColor(0, 0, 0))
155 	{
156 		this._width = width;
157 		this._height = height;
158 
159 		foreach (x; 0.._width)
160 		{
161 			foreach (y; 0.._height)
162 			{
163 				_image ~= color;
164 			}	
165 		}
166 	}
167 
168 	RGBColor opIndexAssign(RGBColor color, size_t x, size_t y)
169 	{
170 		_image[actualIndex(x, y)] = color;
171 		return color;
172 	}
173 
174 	RGBColor opIndexAssign(RGBColor color, size_t x)
175 	{
176 		_image[actualIndex(x)] = color;
177 		return color;
178 	}
179 
180 	RGBColor opIndex(size_t x, size_t y)
181 	{
182 		return _image[actualIndex(x, y)];
183 	}
184 
185 	RGBColor opIndex(size_t x)
186 	{
187 		return _image[actualIndex(x)];
188 	}
189 
190 	override string toString()
191 	{
192 		string accumulator = "[";
193 
194 		foreach (x; 0.._width)
195 		{
196 			string tmp = "[";
197 			foreach (y; 0.._height)
198 			{
199 				tmp ~= _image[actualIndex(x, y)].toString ~ ", ";				
200 			}
201 			tmp = tmp[0..$-2] ~ "], ";
202 			accumulator ~= tmp;
203 		}
204 		return accumulator[0..$-2] ~ "]";
205 	}
206 
207 	alias width = getWidth;
208 	alias height = getHeight;
209 
210 	final RGBColor[] array()
211 	{
212 		return _image;
213 	}
214 
215 	// experimental feature (!)
216 	final void changeCapacity(size_t x, size_t y)
217 	{
218 		long newLength = (x * y);
219 		
220 		if (newLength > _image.length)
221 		{
222 			auto restLength = cast(long) newLength - _image.length;
223 			_image.length += cast(size_t) restLength;
224 		}
225 		else
226 		{
227 			if (newLength < _image.length)
228 			{
229 				auto restLength = cast(long) _image.length - newLength;
230 				_image.length -= cast(size_t) restLength;
231 			}
232 		}
233 		_width = x;
234 		_height = y;
235 	}
236 }
237 
238 
239 enum PixMapFormat : string
240 {
241 	PBM_TEXT 	= 	"P1",
242 	PBM_BINARY 	=  	"P4",
243 	PGM_TEXT 	= 	"P2",
244 	PGM_BINARY	=	"P5",
245 	PPM_TEXT	=	"P3",
246 	PPM_BINARY	= 	"P6",
247 }
248 
249 class PixMapFile
250 {
251 	mixin(addProperty!(PixMapImage, "Image"));
252 	protected
253 	{
254 		File _file;
255 		PixMapFormat _header;
256 
257 		abstract void loader();
258 		abstract void saver();
259 	}
260 
261 	private
262 	{
263 		// set i/o mode (actual for windows system)
264 		auto IOMode(string mode)
265 		{
266 			
267 			if (isBinaryFormat) 
268 			{
269 				return mode ~ `b`;
270 			}
271 			else
272 			{
273 				return mode;
274 			}
275 		}
276 	}
277 
278 	void load(string filename)
279 	{
280 		with (_file)
281 		{
282 			open(filename, IOMode(`r`));
283 
284 			if (readln.strip == EnumValue(_header))
285 			{
286 				auto imageSize = readln.split;
287 				auto width = imageSize[0].parse!int;
288 				auto height = imageSize[1].parse!int;
289 
290 				_image = new PixMapImage(width, height);
291 
292 				loader;
293 			}
294 		}
295 	}
296 	
297 	void save(string filename)
298 	{
299 		with (_file)
300 		{
301 			open(filename, IOMode("w"));
302 			writeln(EnumValue(_header));
303 			writeln(_image.width, " ", _image.height);
304 
305 			saver;
306 		}
307 	}
308 
309 	final bool isBinaryFormat()
310 	{
311 		return 
312 				( 
313 				  (_header == PixMapFormat.PBM_BINARY) |
314 				  (_header == PixMapFormat.PGM_BINARY) |
315 				  (_header == PixMapFormat.PPM_BINARY)
316 				);
317 	}
318 
319 	final bool isTextFormat()
320 	{
321 		return 
322 				( 
323 				  (_header == PixMapFormat.PBM_TEXT) |
324 				  (_header == PixMapFormat.PGM_TEXT) |
325 				  (_header == PixMapFormat.PPM_TEXT)
326 				);
327 	}	
328 
329 	final PixMapImage image() 
330 	{ 
331 		return _image; 
332 	}
333 
334 	alias image this;
335 }
336 
337 
338 class P6Image : PixMapFile
339 {
340 	mixin(addProperty!(int, "Intensity", "255"));
341 	mixin addConstructor!(PixMapFormat.PPM_BINARY);
342 
343 	override void loader()
344 	{
345 		auto data = _file.readln;
346 		_intensity = data.parse!int;
347 
348 		auto buffer = new ubyte[width * 3];
349 		
350 		for (uint i = 0; i < height; i++)
351 		{
352 		 	_file.rawRead!ubyte(buffer);
353 						
354 		    for (uint j = 0; j < width; j++)
355 		    {
356 				auto R = buffer[j * 3];
357 				auto G = buffer[j * 3 + 1];
358 				auto B = buffer[j * 3 + 2];
359 		 	 	_image[j, i] = new RGBColor(
360 					(R > _intensity) ? _intensity : R,
361 					(G > _intensity) ? _intensity : G,
362 					(B > _intensity) ? _intensity : B
363 				);
364 		    } 
365 		}
366 	}
367 
368 	override void saver()
369 	{
370 		_file.writeln(_intensity);
371 
372 		foreach (e; _image.array)
373 		{
374 			auto R = e.getR;
375 			auto G = e.getG;
376 			auto B = e.getB;
377 
378 			auto rr = (R > _intensity) ? _intensity : R;
379 			auto gg = (G > _intensity) ? _intensity : G;
380 			auto bb = (B > _intensity) ? _intensity : B;
381 
382 			_file.write(
383 				cast(char) rr,
384 				cast(char) gg,
385 				cast(char) bb
386 		    );
387 	    }
388 	}
389 }
390 
391 class P3Image : PixMapFile
392 {
393 	mixin(addProperty!(int, "Intensity", "255"));
394 	mixin addConstructor!(PixMapFormat.PPM_TEXT);
395 
396 	override void loader()
397 	{
398 		// skip maximal intensity description
399 		auto data = _file.readln;
400 		_intensity = data.parse!int;
401 		
402 		string triplet;
403 		int index = 0;
404 						
405 		while ((triplet = _file.readln) !is null)
406 		{				
407 			auto rgb = triplet.split;
408 			auto R = rgb[0].parse!int;
409 		    auto G = rgb[1].parse!int;
410 		    auto B = rgb[2].parse!int;
411 
412 			_image[index] = new RGBColor(
413 		 		(R > _intensity) ? _intensity : R,
414 		        (G > _intensity) ? _intensity : G,
415 		        (B > _intensity) ? _intensity : B		
416  			);
417 		 	index++;
418 		}
419 	}
420 
421 	override void saver()
422 	{
423 		_file.writeln(_intensity);
424 
425 		foreach (e; _image.array)
426 		{
427 			auto R = e.getR;
428 			auto G = e.getG;
429 			auto B = e.getB;
430 
431 			_file.writefln(
432 				"%d %d %d",
433 				(R > _intensity) ? _intensity : R,
434 				(G > _intensity) ? _intensity : G,
435 				(B > _intensity) ? _intensity : B
436 		    );
437 	    }
438      }
439 }
440 
441 
442 class P1Image : PixMapFile
443 {
444 	mixin addConstructor!(PixMapFormat.PBM_TEXT);
445 
446 	override void loader()
447 	{
448 		 string line;
449 		 int index;
450 		
451 		 auto WHITE = new RGBColor(255, 255, 255);
452 		 auto BLACK = new RGBColor(0, 0, 0);
453 						
454 		 while ((line = _file.readln) !is null)
455 		 {
456 		 	auto row  = line.replace(" ", "");
457 		
458 		 	foreach (i, e; row)
459 		 	{
460 		 		_image[i, index] = (e.to!string == "0") ? BLACK : WHITE;  						
461 		 	}					
462 		 	index++;
463 		 }					
464 	}
465 
466 	override void saver()
467 	{
468 		import std.algorithm;
469 		import std.range : chunks;
470 		
471 		foreach (rows; _image.array.chunks(_image.width))
472 		{
473 		 	_file.writeln(
474 		 		rows
475 		 			.map!(a => (a.luminance < 255) ? "0" : "1")
476 		 			.join("")
477 		 	);
478 		}
479 	}
480 }
481 
482 
483 class P2Image : PixMapFile
484 {
485 	mixin(addProperty!(int, "Intensity", "255"));
486 	mixin addConstructor!(PixMapFormat.PGM_TEXT);
487 
488 	override void loader()
489 	{
490 		 // skip maximal intensity description
491 		 auto data = _file.readln;
492 		 _intensity = data.parse!int;
493 		
494 	     string line;
495 		 int index;
496 		
497 		 while ((line = _file.readln) !is null)
498 		 {
499 		 	auto row  = line.split;
500 		
501 		 	foreach (i, e; row)
502 		 	{
503 		 		auto l = e.parse!int;
504 				auto I = (l > _intensity) ? _intensity : l;
505 		 		_image[i, index] = new RGBColor(I, I, I);  						
506 		 	}					
507 		 	index++;
508 		 } 
509 	}
510 
511 	override void saver()
512 	{
513 		_file.writeln(_intensity);
514 
515 	    import std.algorithm;
516 	    import std.range : chunks;
517 	    		
518 	   	foreach (rows; _image.array.chunks(_image.width))
519 	    {
520 			auto toIntensity(RGBColor color)
521 			{
522 				int I;
523 				if ((color.getR == color.getG) && (color.getG == color.getB) && (color.getR == color.getB))
524 				{
525 					I = color.getR;
526 				}
527 				else
528 				{
529 					I = color.luminance601.to!int;
530 				}
531 				return (I > _intensity) ? _intensity : I;				
532 			}
533 			
534 	    	_file.writeln(
535 	    		 rows
536 	    		 	.map!(a => toIntensity(a).to!string)
537 	    		 	.join(" ")
538 	    	);
539 	    }
540      }
541 }
542 
543 class P5Image : PixMapFile
544 {
545 	mixin(addProperty!(int, "Intensity", "255"));
546 	mixin addConstructor!(PixMapFormat.PGM_BINARY);
547 
548 	override void loader()
549 	{
550 		// skip maximal intensity description
551 		auto data = _file.readln;
552 		_intensity = data.to!int;
553 
554 		auto buffer = new ubyte[width * height];
555 		_file.rawRead!ubyte(buffer);
556 
557 		foreach (i, e; buffer)
558 		{
559 			auto I =  (e > _intensity) ? _intensity : e;
560 			_image[i] = new RGBColor(I, I, I);
561 		}
562 	}
563 
564 	override void saver()
565 	{
566 		_file.writeln(_intensity);
567 
568 		foreach (e; _image.array)
569 		{
570 			ubyte I;
571 			if ((e.getR == e.getG) && (e.getG == e.getB) && (e.getR == e.getB))
572 			{
573 				I = e.getR.to!ubyte;
574 			}
575 			else
576 			{
577 				I = e.luminance601.to!ubyte;
578 			}
579 
580 			_file.write(
581 				cast(char) I
582 			);
583 		}
584      }
585 }
586 
587 
588 class P4Image : PixMapFile
589 {
590 	mixin addConstructor!(PixMapFormat.PBM_BINARY);
591 
592 	auto setBit(int value, int n)
593 	{
594 		return (value | (1 << n));
595 	}
596 
597 	auto getBit(int value, int n)
598 	{
599 		return ((value >> n) & 1);
600 	}
601 
602 	auto clearBit(int value, int n)
603 	{
604 		return (value & ~(1 << n));
605 	}
606 
607 	override void loader()
608 	{
609 		auto imageSize = width * height;
610 		auto buffer = new ubyte[imageSize];
611 		_file.rawRead!ubyte(buffer);
612 
613 		int index;
614 
615 		auto BLACK = new RGBColor(0, 0, 0);
616 		auto WHITE = new RGBColor(255, 255, 255);
617 
618 		foreach (e; buffer)
619 		{
620 			if (index < imageSize)
621 			{
622 				foreach (i; 0..8)
623 				{
624 					auto I = getBit(cast(int) e, i);
625 					//_image[index] = (I == 0) ? BLACK : WHITE;
626 					_image[index] = I ? BLACK : WHITE;
627 					index++;
628 				}
629 			}
630 			else
631 			{
632 				break;
633 			}
634 		}				
635 	}
636 
637 	override void saver()
638 	{
639 		int[] bytes;
640 		bytes ~= new int[width * height];
641 		
642 		while ((bytes.length % 8) != 0)
643 		{
644 			bytes ~= 0;
645 		}
646 
647 		int bytesCount;
648 		int shiftCount;
649 
650 		foreach (e; _image.array)
651 		{
652 			// auto I = (e.luminance == 0) ? 0 : 1;
653 			auto I = (e.luminance) ? 0 : 1;
654 			auto currentByte = bytes[bytesCount];
655 			
656 			if (I == 0)
657 			{
658 				currentByte = clearBit(currentByte, shiftCount);
659 			}
660 			else
661 			{
662 				currentByte = setBit(currentByte, shiftCount);
663 			}
664 			bytes[bytesCount] = currentByte;
665 			shiftCount++;
666 			
667 			if (shiftCount > 7)
668 			{
669 				shiftCount = 0;
670 				bytesCount++;
671 			}
672 		}
673 
674 		foreach (e; bytes)
675 		{
676 			_file.write(
677 				cast(char) e
678 			);
679 		}
680 	}
681 }
682 
683 
684 PixMapFile image(size_t width = 0, size_t height = 0, PixMapFormat pmFormat = PixMapFormat.PPM_BINARY)
685 {
686 	PixMapFile pixmap;
687 
688 	final switch (pmFormat) with (PixMapFormat)
689 	{
690 		case PBM_TEXT:
691 			pixmap = new P1Image(width, height);
692 			break;
693 		case PBM_BINARY:
694 			pixmap = new P4Image(width, height);
695 			break;
696 		case PGM_TEXT:
697 			pixmap = new P2Image(width, height);
698 			break;
699 		case PGM_BINARY:
700 			pixmap = new P5Image(width, height);
701 			break;
702 		case PPM_TEXT:
703 			pixmap = new P3Image(width, height);
704 			break;
705 		case PPM_BINARY:
706 			pixmap = new P6Image(width, height);
707 			break;
708 	}
709 
710 	return pixmap;
711 }
712 
713 PixMapFile image(size_t width = 0, size_t height = 0, string pmFormat = "P6")
714 {
715 	PixMapFile pixmap;
716 
717 	switch (pmFormat) 
718 	{
719 		case "P1":
720 			pixmap = new P1Image(width, height);
721 			break;
722 		case "P4":
723 			pixmap = new P4Image(width, height);
724 			break;
725 		case "P2":
726 			pixmap = new P2Image(width, height);
727 			break;
728 		case "P5":
729 			pixmap = new P5Image(width, height);
730 			break;
731 		case "P3":
732 			pixmap = new P3Image(width, height);
733 			break;
734 		case "P6":
735 			pixmap = new P6Image(width, height);
736 			break;
737 		default:
738 			assert(0);
739 	}
740 
741 	return pixmap;
742 }
743 
744