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