1 // Written in the D programming language.
2 
3 /**
4 Minimalistic library for working with Netpbm image formats. Currently, work with formats from P1 to P6 is supported.
5 An overview of the internal structure of the library, its interfaces and applications is available at the link (in Russian): https://lhs-blog.info/programming/dlang/ppmformats-library/
6 
7 Copyright: LightHouse Software, 2020 - 2022
8 Authors:   Oleg Bakharev,
9 		   Ilya Pertsev
10 */
11 module ppmformats;
12 
13 private
14 {
15 	import std.algorithm;
16 	import std.conv;
17 	import std.range;
18 	import std.stdio;
19 	import std.string;
20 	import std.traits;
21 	
22 	/**
23 	A handy template for creating setters and getters for properties. For internal use only.
24 	See_Also:
25 		https://lhs-blog.info/programming/dlang/udobnoe-sozdanie-svoystv-v-klassah-i-strukturah/ (in Russian)
26 	*/
27 	template addProperty(T, string propertyName, string defaultValue = T.init.to!string)
28 	{	 
29 		const char[] addProperty = format(
30 			`
31 			private %2$s %1$s = %4$s;
32 	 
33 			void set%3$s(%2$s %1$s)
34 			{
35 				this.%1$s = %1$s;
36 			}
37 	 
38 			%2$s get%3$s()
39 			{
40 				return %1$s;
41 			}
42 			`,
43 			"_" ~ propertyName.toLower,
44 			T.stringof,
45 			propertyName,
46 			defaultValue
47 			);
48 	}
49 
50 	/// Extract type value from enum. For internal use.
51 	auto EnumValue(E)(E e) 
52 		if(is(E == enum)) 
53 	{
54 		OriginalType!E tmp = e;
55 		return tmp; 
56 	}
57 	
58 	/// Add basic constructor for all image filetypes. For internal use.
59 	mixin template addConstructor(alias pmf)
60 	{
61 		this(size_t width = 0, size_t height = 0, RGBColor color = new RGBColor(0, 0, 0))
62 		{
63 			_image  = new PixMapImage(width, height, color);
64 			_header = pmf; 
65 		}
66 	
67 		alias image this;
68 	}
69 }
70 
71 /**
72 	Class for representing color in RGB format.
73 */
74 class RGBColor
75 {
76 	mixin(addProperty!(int, "R"));
77 	mixin(addProperty!(int, "G"));
78 	mixin(addProperty!(int, "B"));
79 	
80 	
81 	/**
82 	Constructor for creating colors in RGB format. 
83 	If called without parameters, then all three values ​​of the channels R, G and B take the value 0, which corresponds to black.
84     Params:
85     R = 32-bit value for red channel. The value ranges from 0 (minimum value) to 255 (maximum value).
86     G = 32-bit value for green channel. The value ranges from 0 (minimum value) to 255 (maximum value).
87     B = 32-bit value for blue channel. The value ranges from 0 (minimum value) to 255 (maximum value).
88     
89     Typical usage:
90     ----
91     // Black color
92     RGBColor color = new RGBColor;
93     
94     // Red color	
95     RGBColor color = new RGBColor(255, 0, 0);
96     
97     // White color
98     RGBColor color = new RGBColor(255, 255, 255); 
99     ----
100     */
101 	this(int R = 0, int G = 0, int B = 0)
102 	{
103 		this._r = R;
104 		this._g = G;
105 		this._b = B;
106 	}
107 
108 	/**
109 	Luminance according to ITU 709 standard.
110 	Returns:
111 	Luminance for a specific color as a floating point value.
112     
113     Typical usage:
114     ----
115     import std.stdio : writeln;
116     
117     // Red color
118     RGBColor color = new RGBColor(255, 0, 0); 
119 	color.luminance709.writeln;
120     ----
121     */
122 	const float luminance709()
123 	{
124 	   return (_r  * 0.2126f + _g  * 0.7152f + _b  * 0.0722f);
125 	}
126 	
127 	/**
128 	Luminance according to ITU 601 standard.
129 	Returns:
130 	Luminance for a specific color as a floating point value.
131     
132     Typical usage:
133     ----
134     import std.stdio : writeln;
135     
136     // Red color
137     RGBColor color = new RGBColor(255, 0, 0); 
138 	color.luminance601.writeln;
139     ----
140     */
141 	const float luminance601()
142 	{
143 	   return (_r * 0.3f + _g * 0.59f + _b * 0.11f);
144 	}
145 	
146 	/**
147 	Average luminance.
148 	Returns:
149 	Luminance for a specific color as a floating point value.
150     
151     Typical usage:
152     ----
153     import std.stdio : writeln;
154     
155     // Red color
156     RGBColor color = new RGBColor(255, 0, 0); 
157 	color.luminanceAverage.writeln;
158     ----
159     */
160 	const float luminanceAverage()
161 	{
162 	   return (_r + _g + _b) / 3.0;
163 	}
164 
165 	/// Alias for standard (default) luminance calculation. Value is the same as luminance709.
166 	alias luminance = luminance709;
167 
168 	/**
169 	A string representation of a color.
170 	The color is output as a string in the following format: RGBColor(R=<value>, G=<value>, B=<value>, I=<value>), where R,G,B are the values ​​of the three color channels, and I is color brightness according to ITU 709.
171 	Returns:
172 	String color representation.
173     
174     Typical usage:
175     ----
176     import std.stdio : writeln;
177     
178     // Red color
179     RGBColor color = new RGBColor(255, 0, 0); 
180 	color.writeln;
181     ----
182     */
183 	override string toString()
184 	{		
185 		return format("RGBColor(%d, %d, %d, I = %f)", _r, _g, _b, this.luminance);
186 	}
187 
188 	/**
189 	Basic arithmetic for color operations. The value on the right can be a value of any numeric type.
190     
191     Typical usage:
192     ----
193     // Red color
194     RGBColor color = new RGBColor(255, 0, 0);
195        	
196 	// Add two for all channels in color
197 	auto newColor = color + 2;	
198 	// Divide all channels by two					
199 	color = color / 2;								
200     ----
201     */
202 	RGBColor opBinary(string op, T)(auto ref T rhs)
203 	{
204 		return mixin(
205 			format(`new RGBColor( 
206 				clamp(cast(int) (_r  %1$s rhs), 0, 255),
207 				clamp(cast(int) (_g  %1$s rhs), 0, 255),
208 				clamp(cast(int) (_b  %1$s rhs), 0, 255)
209 				)
210 			`,
211 			op
212 			)
213 		);
214 	}
215 
216 	/**
217 	Basic arithmetic for color operations. Only the RGBColor type can be used as the value on the right.
218     
219     Typical usage:
220     ----
221     // Red color
222     RGBColor color  = new RGBColor(255, 0, 0);
223     // Blue color  	
224 	RGBColor color2 = new RGBColor(0, 0, 255); 		
225 	
226 	// mix two colors
227 	auto mix = color + color2;
228 	// difference between color
229 	auto diff = color - color2;
230     ----
231     */
232 	RGBColor opBinary(string op)(RGBColor rhs)
233 	{
234 		return mixin(
235 			format(`new RGBColor( 
236 				clamp(cast(int) (_r  %1$s rhs.getR), 0, 255),
237 				clamp(cast(int) (_g  %1$s rhs.getG), 0, 255),
238 				clamp(cast(int) (_b  %1$s rhs.getB), 0, 255)
239 				)
240 			`,
241 			op
242 			)
243 		);
244 	}
245 }
246 
247 /**
248 	A class that provides a convenient interface for working with images. Represents a one-dimensional array.
249 */
250 class PixMapImage
251 {
252 	mixin(addProperty!(size_t, "Width"));
253 	mixin(addProperty!(size_t, "Height"));
254 	
255 	private
256 	{
257 		RGBColor[] _image;
258 
259 		/**
260 		Calculation of the real index in the internal one-dimensional array storing pixels.
261 		The real length of the internal array is taken into account, therefore it is allowed to specify an index value greater than the actual length of the array.
262 		
263 		Internal use only for implementing class object accessor methods through indexing operators.
264 		*/
265 		auto actualIndex(size_t i)
266 		{
267 			auto S = _width * _height;
268 		
269 			return clamp(i, 0, S - 1);
270 		}
271 
272 		/**
273 		Calculation of the real index in a one-dimensional array through two indexes. 
274 		Thus, the possibility of referring to the internal array as a two-dimensional one is realized. 
275 		As in the previous method, the binding to the actual length of the internal array is taken into account, so both indexes can be greater than the actual values ​​of the length and width of the image.
276 		
277 		Internal use only for implementing class object accessor methods through indexing operators.
278 		*/
279 		auto actualIndex(size_t i, size_t j)
280 		{
281 			auto W = cast(size_t) clamp(i, 0, _width - 1);
282 			auto H = cast(size_t) clamp(j, 0, _height - 1);
283 			auto S = _width * _height;
284 		
285 			return clamp(W + H * _width, 0, S);
286 		}
287 	}
288 
289 	/**
290 	A constructor for creating an image with given dimensions (length and width) and starting color for pixels. 
291 	By default, all values ​​are zero, and black (i.e: RGBColor(0, 0, 0)) is used as the starting color.
292     Params:
293     width = Width of image as size_t value. 
294     height = Height of image as size_t value.
295     color = Initial color for pixels in image.
296     
297     Typical usage:
298     ----
299     // creating of empty image
300     PixMapImage pmi = new PixMapImage;  									
301     
302     // creating image of size 20x20, all pixels are black
303     PixMapImage pmi2 = new PixMapImage(20, 20);								
304     
305     // creating image of size 20x20, all pixels are red
306     PixMapImage pmi3 = new PixMapImage(20, 20, new RGBColor(255, 0, 255));	
307     ----
308     */	
309 	this(size_t width = 0, size_t height = 0, RGBColor color = new RGBColor(0, 0, 0))
310 	{
311 		this._width = width;
312 		this._height = height;
313 
314 		foreach (x; 0.._width)
315 		{
316 			foreach (y; 0.._height)
317 			{
318 				_image ~= color;
319 			}	
320 		}
321 	}
322 
323 	/**
324 	Assigning a color value to an individual pixel through a two-index indexing operation.
325 	Note: It is allowed to use as indices values ​​greater than the length and width (or less than 0) as indices, since the values ​​will be converted to the actual length of the image array.
326     
327     Typical usage:
328     ----
329     // creating image of size 20x20, all pixels are black
330     auto pmi = new PixMapImage(20, 20);   
331     // pixel at coords (5;5) now are green
332     pmi[5, 5] = new RGBColor(0, 255, 0);  
333     ----
334 	*/	
335 	RGBColor opIndexAssign(RGBColor color, size_t x, size_t y)
336 	{
337 		_image[actualIndex(x, y)] = color;
338 		return color;
339 	}
340 
341 	/**
342 	Assigning a color value to an individual pixel through a one-index indexing operation.
343 	Note: It is allowed to use an index greater than the actual length of the array or less than 0, since it is bound to the real length of the internal array of the image.
344     
345     Typical usage:
346     ----
347     // creating image of size 20x20, all pixels are black
348     auto pmi = new PixMapImage(20, 20);   
349     // 6th pixel now are green
350     pmi[5] = new RGBColor(0, 255, 0);  	  
351     ----
352 	*/	
353 	RGBColor opIndexAssign(RGBColor color, size_t x)
354 	{
355 		_image[actualIndex(x)] = color;
356 		return color;
357 	}
358 
359 	/**
360 	Getting a color value from an individual pixel through a two-index indexing operation.
361 	Note: It is allowed to use as indices values ​​greater than the length and width (or less than 0) as indices, since the values ​​will be converted to the actual length of the image array.
362     
363     Typical usage:
364     ----
365     // creating image of size 20x20, all pixels are black
366     auto pmi = new PixMapImage(20, 20);   
367     // get pixel color at coords (5;5)
368     pmi[5, 5].writeln;  				  
369     ----
370 	*/	
371 	RGBColor opIndex(size_t x, size_t y)
372 	{
373 		return _image[actualIndex(x, y)];
374 	}
375 
376 	/**
377 	Assigning a color value to an individual pixel through a one-index indexing operation.
378 	Note: It is allowed to use an index greater than the actual length of the array or less than 0, since it is bound to the real length of the internal array of the image.
379     
380     Typical usage:
381     ----
382     // creating image of size 20x20, all pixels are black
383     auto pmi = new PixMapImage(20, 20);   
384     // getting color of 6th pixel
385     pmi[5].writeln;  	  				  
386     ----
387 	*/	
388 	RGBColor opIndex(size_t x)
389 	{
390 		return _image[actualIndex(x)];
391 	}
392 
393 	/**
394 	The string representation of the image. Returns a string representing the image as a two-dimensional array of RGBColor objects.
395 	*/	
396 	override string toString()
397 	{
398 		string accumulator = "[";
399 
400 		foreach (x; 0.._width)
401 		{
402 			string tmp = "[";
403 			foreach (y; 0.._height)
404 			{
405 				tmp ~= _image[actualIndex(x, y)].toString ~ ", ";				
406 			}
407 			tmp = tmp[0..$-2] ~ "], ";
408 			accumulator ~= tmp;
409 		}
410 		return accumulator[0..$-2] ~ "]";
411 	}
412 
413 	/// Returns actual width of image as size_t value
414 	alias width = getWidth;
415 	/// Returns actual height of image as size_t value
416 	alias height = getHeight;
417 
418 	/**
419 	Returns the entire internal one-dimensional pixel array of the image.
420 	Returns:
421 	One-dimensional array of RGBColor objects.
422     
423     Typical usage:
424     ----
425     PixMapImage pmi = new PixMapFile(10, 10);
426     // get all pixels 
427 	RGBColor[] pixels = pmi.array;	
428     ----
429     */
430 	final RGBColor[] array()
431 	{
432 		return _image;
433 	}
434 	
435 	/**
436 	Sets the inner pixel array of the image by feeding the outer array. 
437 	The size of the array must be equal to the actual size of the image (i.e. the size of the given one-dimensional array must be equal to the product of the length of the image and its width)
438 	Throws:
439 	Exception if the length of the supplied array does not match the actual length of the internal array, as above.
440     
441     Typical usage:
442     ----
443     PixMapImage pmi = new PixMapFile(2);
444 	RGBColor[] pixels = [new RGBColor(255, 255, 255), new RGBColor(255, 255, 255)];
445 	// set all pixels as white
446 	pmi.array(pixels);	
447     ----
448     */
449 	final void array(RGBColor[] image)
450 	{
451 		if (image.length == _image.length) 
452 		{
453 			this._image = image;
454 		}
455 		else
456 		{
457 			throw new Exception("Lengths must be the same");
458 		}
459 	}
460 	
461 	/**
462 	Resizing an image according to its given length and width. 
463 	Note:
464 	If the length and/or width are smaller than the original values, then a literal cropping to the desired dimensions will be performed (not interpolation or approximation, but real cropping!). 
465 	If the size parameters are larger than the original ones, then the image size will be increased by adding the default color to the end of the image (real array addition will be performed, not interpolation).
466     
467     WARNING:
468 		The method is highly controversial and experimental. We strongly discourage its use in real projects.
469     */
470 	final void changeCapacity(size_t x, size_t y)
471 	{
472 		long newLength = (x * y);
473 		
474 		if (newLength > _image.length)
475 		{
476 			auto restLength = cast(long) newLength - _image.length;
477 			_image.length += cast(size_t) restLength;
478 		}
479 		else
480 		{
481 			if (newLength < _image.length)
482 			{
483 				auto restLength = cast(long) _image.length - newLength;
484 				_image.length -= cast(size_t) restLength;
485 			}
486 		}
487 		_width = x;
488 		_height = y;
489 	}
490 }
491 
492 /**
493 	All possible types of Portable Anymap Image formats in the form of a convenient division into binary and text image formats.
494 */
495 enum PixMapFormat : string
496 {
497 	PBM_TEXT 	= 	"P1",
498 	PBM_BINARY 	=  	"P4",
499 	PGM_TEXT 	= 	"P2",
500 	PGM_BINARY	=	"P5",
501 	PPM_TEXT	=	"P3",
502 	PPM_BINARY	= 	"P6",
503 }
504 
505 /**
506 	Common ancestor for all subsequent image types.
507 	Implements a generic way to load/save images by providing generic load/save methods. 
508 	Also, inheritance from this class allows descendant classes to have methods for working with images: indexing, assigning values ​​to pixels and accessing them without the need to create an object of the PixMapImage class to manipulate images.
509 	
510 	Implementation Note: The specific loading method is already implemented by descendant classes by overriding the abstract loader/saver methods in their implementations.
511 */
512 class PixMapFile
513 {
514 	mixin(addProperty!(PixMapImage, "Image"));
515 	
516 	protected
517 	{
518 		File _file;
519 		PixMapFormat _header;
520 		
521 		abstract void loader();
522 		abstract void saver();
523 	}
524 
525 	private
526 	{
527 		/// Set i/o mode for reading/writing Portable Anymap Images. Actual for OS Windows. For internal use.
528 		auto IOMode(string mode)
529 		{
530 			
531 			if (isBinaryFormat) 
532 			{
533 				return mode ~ `b`;
534 			}
535 			else
536 			{
537 				return mode;
538 			}
539 		}
540 	}
541 
542 	/// Basic file loading procedure
543 	void load(string filename)
544 	{
545 		with (_file)
546 		{
547 			open(filename, IOMode(`r`));
548 
549 			if (readln.strip == EnumValue(_header))
550 			{
551 				auto imageSize = readln.split;
552 				auto width = imageSize[0].parse!int;
553 				auto height = imageSize[1].parse!int;
554 
555 				_image = new PixMapImage(width, height);
556 
557 				loader;
558 			}
559 		}
560 	}
561 	
562 	/// Basic file saving procedure
563 	void save(string filename)
564 	{
565 		with (_file)
566 		{
567 			open(filename, IOMode("w"));
568 			writeln(EnumValue(_header));
569 			writeln(_image.width, " ", _image.height);
570 
571 			saver;
572 		}
573 	}
574 
575 	/// Is raw format ?
576 	final bool isBinaryFormat()
577 	{
578 		return 
579 				( 
580 				  (_header == PixMapFormat.PBM_BINARY) |
581 				  (_header == PixMapFormat.PGM_BINARY) |
582 				  (_header == PixMapFormat.PPM_BINARY)
583 				);
584 	}
585 
586 	/// Is text format ?
587 	final bool isTextFormat()
588 	{
589 		return 
590 				( 
591 				  (_header == PixMapFormat.PBM_TEXT) |
592 				  (_header == PixMapFormat.PGM_TEXT) |
593 				  (_header == PixMapFormat.PPM_TEXT)
594 				);
595 	}	
596 
597 	/// Get image object as PixMapImage object
598 	final PixMapImage image() 
599 	{ 
600 		return this.getImage; 
601 	}
602 	
603 	/// Set image object as PixMapImage object
604 	final void image(PixMapImage image)
605 	{
606 		this.setImage(image);
607 	}
608 
609 	/// Convenient alias for working with PixMapFile same as PixMapImage
610 	alias image this;
611 }
612 
613 /**
614 	A class that provides the ability to work with color images in P6 format. 
615 	NB: The format is raw binary. 
616 	
617 	Note: 
618 		This class supports indexing and assigning values ​​to specific pixels via 1D or 2D indexing, and provides P6 file loading/saving capabilities. 
619 		According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.ppm`.
620 		
621 	Typical usage:
622     ----
623     // creating of empty image
624     auto img = new P6Image;  					
625     // load image from file `Lenna.ppm`
626     img.load(`Lenna.ppm`);   					
627     // change pixel at coords (10; 10), now are white
628     img[10, 10] = new RGBColor(255, 255, 255); 	
629     // get color of 11th pixel
630     img[10].writeln;							
631     // save file as `Lenna2.ppm`
632     img.save(`Lenna2.ppm`);						
633     
634     // creating image of 10x10, all pixels are red
635     auto img2 = new P6Image(10, 10, new RGBColor(255, 0, 255)); 
636     // increasing luminance by two
637     img2[10] = img2[10] * 2; 									
638     // save as `test.ppm`
639     img2.save(`test.ppm`);										
640     ----
641 */
642 class P6Image : PixMapFile
643 {
644 	mixin(addProperty!(int, "Intensity", "255"));
645 	mixin addConstructor!(PixMapFormat.PPM_BINARY);
646 
647 	override void loader()
648 	{
649 		auto data = _file.readln;
650 		_intensity = data.parse!int;
651 
652 		auto buffer = new ubyte[width * 3];
653 		
654 		for (uint i = 0; i < height; i++)
655 		{
656 		 	_file.rawRead!ubyte(buffer);
657 						
658 		    for (uint j = 0; j < width; j++)
659 		    {
660 				auto R = buffer[j * 3];
661 				auto G = buffer[j * 3 + 1];
662 				auto B = buffer[j * 3 + 2];
663 		 	 	_image[j, i] = new RGBColor(
664 					(R > _intensity) ? _intensity : R,
665 					(G > _intensity) ? _intensity : G,
666 					(B > _intensity) ? _intensity : B
667 				);
668 		    } 
669 		}
670 	}
671 
672 	override void saver()
673 	{
674 		_file.writeln(_intensity);
675 
676 		foreach (e; _image.array)
677 		{
678 			auto R = e.getR;
679 			auto G = e.getG;
680 			auto B = e.getB;
681 
682 			auto rr = (R > _intensity) ? _intensity : R;
683 			auto gg = (G > _intensity) ? _intensity : G;
684 			auto bb = (B > _intensity) ? _intensity : B;
685 
686 			_file.write(
687 				cast(char) rr,
688 				cast(char) gg,
689 				cast(char) bb
690 		    );
691 	    }
692 	}
693 }
694 
695 /**
696 	A class that provides the ability to work with color images in P3 format. 
697 	NB: The format is raw text. 
698 	
699 	Note: 
700 		This class supports indexing and assigning values ​​to specific pixels via 1D or 2D indexing, and provides P3 file loading/saving capabilities. 
701 		According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.ppm`.
702 		
703 	Typical usage:
704     ----
705     // creating of empty image
706     auto img = new P3Image;  					
707     // load image from file `Lenna.ppm`
708     img.load(`Lenna.ppm`);   					
709     // change pixel at coords (10; 10), now are white
710     img[10, 10] = new RGBColor(255, 255, 255); 	
711     // get color of 11th pixel
712     img[10].writeln;							
713     // save file as `Lenna2.ppm`
714     img.save(`Lenna2.ppm`);						
715     
716     // creating image of 10x10, all pixels are red
717     auto img2 = new P3Image(10, 10, new RGBColor(255, 0, 255)); 
718     // increasing luminance by two
719     img2[10] = img2[10] * 2; 									
720     // save as `test.ppm`
721     img2.save(`test.ppm`);										
722     ----
723 */
724 class P3Image : PixMapFile
725 {
726 	mixin(addProperty!(int, "Intensity", "255"));
727 	mixin addConstructor!(PixMapFormat.PPM_TEXT);
728 
729 	override void loader()
730 	{
731 		// skip maximal intensity description
732 		auto data = _file.readln;
733 		_intensity = data.parse!int;
734 		
735 		string triplet;
736 		int index = 0;
737 						
738 		while ((triplet = _file.readln) !is null)
739 		{				
740 			auto rgb = triplet.split;
741 			auto R = rgb[0].parse!int;
742 		    auto G = rgb[1].parse!int;
743 		    auto B = rgb[2].parse!int;
744 
745 			_image[index] = new RGBColor(
746 		 		(R > _intensity) ? _intensity : R,
747 		        (G > _intensity) ? _intensity : G,
748 		        (B > _intensity) ? _intensity : B		
749  			);
750 		 	index++;
751 		}
752 	}
753 
754 	override void saver()
755 	{
756 		_file.writeln(_intensity);
757 
758 		foreach (e; _image.array)
759 		{
760 			auto R = e.getR;
761 			auto G = e.getG;
762 			auto B = e.getB;
763 
764 			_file.writefln(
765 				"%d %d %d",
766 				(R > _intensity) ? _intensity : R,
767 				(G > _intensity) ? _intensity : G,
768 				(B > _intensity) ? _intensity : B
769 		    );
770 	    }
771      }
772 }
773 
774 /**
775 	A class that provides the ability to work with color images in P1 format. 
776 	NB: The format is raw text. 
777 	
778 	Note: 
779 		This class supports indexing and assigning values ​​to specific pixels via 1D or 2D indexing, and provides P1 file loading/saving capabilities. 
780 		According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pbm`.
781 		
782 	Typical usage:
783     ----
784     // creating of empty image
785     auto img = new P1Image;  					
786     // load image from file `Lenna.pbm`
787     img.load(`Lenna.pbm`);   					
788     // change pixel at coords (10; 10), now are white
789     img[10, 10] = new RGBColor(255, 255, 255); 	
790     // get color of 11th pixel
791     img[10].writeln;							
792     // save file as `Lenna2.pbm`
793     mg.save(`Lenna2.pbm`);						
794     
795     // creating image of 10x10, all pixels are black
796     auto img2 = new P1Image(10, 10, new RGBColor(0, 0, 0)); 
797     // increasing luminance by two
798     img2[10] = img2[10] * 2; 									
799     // save as `test.pbm`
800     img2.save(`test.pbm`);										
801     ----
802 */
803 class P1Image : PixMapFile
804 {
805 	mixin addConstructor!(PixMapFormat.PBM_TEXT);
806 
807 	override void loader()
808 	{
809 		 string line;
810 		 int index;
811 		
812 		 auto WHITE = new RGBColor(255, 255, 255);
813 		 auto BLACK = new RGBColor(0, 0, 0);
814 						
815 		 while ((line = _file.readln) !is null)
816 		 {
817 		 	auto row  = line.replace(" ", "").replace("\n", "");
818 		
819 		 	foreach (i, e; row)
820 		 	{
821 		 		_image[i, index] = (e.to!string == "0") ? WHITE : BLACK; 			
822 		 	}					
823 		 	index++;
824 		 }				
825 	}
826 
827 	override void saver()
828 	{
829 		foreach (rows; _image.array.chunks(width))
830 		{
831 		 	_file.writeln(
832 		 		rows
833 		 			.map!(a => (a.luminance < 255) ? "1" : "0")
834 		 			.join(" ")
835 		 	);
836 		}
837 	}
838 }
839 
840 /**
841 	A class that provides the ability to work with color images in P2 format. 
842 	NB: The format is raw text. 
843 	
844 	Note: 
845 		This class supports indexing and assigning values ​​to specific pixels via 1D or 2D indexing, and provides P2 file loading/saving capabilities. 
846 		According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pgm`.
847 		
848 	Typical usage:
849     ----
850     // creating of empty image
851     auto img = new P2Image;  					
852     // load image from file `Lenna.pgm`
853     img.load(`Lenna.pgm`);   					
854     // change pixel at coords (10; 10), now are white
855     img[10, 10] = new RGBColor(255, 255, 255); 	
856     // get color of 11th pixel
857     img[10].writeln;							
858     // save file as `Lenna2.pgm`
859     img.save(`Lenna2.pgm`);						
860     
861     // creating image of 10x10, pixels are black
862     auto img2 = new P2Image(10, 10, new RGBColor(0, 0, 0)); 
863     // increasing luminance by two
864     img2[10] = img2[10] * 2; 									
865     // save as `test.pgm`
866     img2.save(`test.pgm`);										
867     ----
868 */
869 class P2Image : PixMapFile
870 {
871 	mixin(addProperty!(int, "Intensity", "255"));
872 	mixin addConstructor!(PixMapFormat.PGM_TEXT);
873 
874 	override void loader()
875 	{
876 		 // skip maximal intensity description
877 		 auto data = _file.readln;
878 		 _intensity = data.parse!int;
879 		
880 	     string line;
881 		 int index;
882 		
883 		 while ((line = _file.readln) !is null)
884 		 {
885 		 	auto row  = line.split;
886 		
887 		 	foreach (i, e; row)
888 		 	{
889 		 		auto l = e.parse!int;
890 				auto I = (l > _intensity) ? _intensity : l;
891 		 		_image[i, index] = new RGBColor(I, I, I);  						
892 		 	}					
893 		 	index++;
894 		 } 
895 	}
896 
897 	override void saver()
898 	{
899 		_file.writeln(_intensity);
900 	
901 	   	foreach (rows; _image.array.chunks(width))
902 	    {
903 			auto toIntensity(RGBColor color)
904 			{
905 				int I;
906 				if ((color.getR == color.getG) && (color.getG == color.getB) && (color.getR == color.getB))
907 				{
908 					I = color.getR;
909 				}
910 				else
911 				{
912 					I = color.luminance601.to!int;
913 				}
914 				return (I > _intensity) ? _intensity : I;				
915 			}
916 			
917 	    	_file.writeln(
918 	    		 rows
919 	    		 	.map!(a => toIntensity(a).to!string)
920 	    		 	.join(" ")
921 	    	);
922 	    }
923      }
924 }
925 
926 /**
927 	A class that provides the ability to work with color images in P5 format. 
928 	NB: The format is raw binary. 
929 	
930 	Note: 
931 		This class supports indexing and assigning values ​​to specific pixels via 1D or 2D indexing, and provides P5 file loading/saving capabilities. 
932 		According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pgm`.
933 		
934 	Typical usage:
935     ----
936     // create empty image
937     auto img = new P5Image;
938     // load from file  					
939     img.load(`Lenna.pgm`);   					
940     // set pixel at (10;10) to white color
941     img[10, 10] = new RGBColor(255, 255, 255);
942     // get color of 11th pixel 	
943     img[10].writeln;							
944     // save to file
945     img.save(`Lenna2.pgm`);						
946     
947     // creating image of size 10x10, all pixels black
948     auto img2 = new P5Image(10, 10, new RGBColor(0, 0, 0)); 
949     // increase luminance twice
950     img2[10] = img2[10] * 2;
951     // save as pgm file 									
952     img2.save(`test.pgm`);										
953     ----
954 */
955 class P5Image : PixMapFile
956 {
957 	mixin(addProperty!(int, "Intensity", "255"));
958 	mixin addConstructor!(PixMapFormat.PGM_BINARY);
959 
960 	override void loader()
961 	{
962 		// skip maximal intensity description
963 		auto data = _file.readln;
964 		_intensity = data.to!int;
965 
966 		auto buffer = new ubyte[width * height];
967 		_file.rawRead!ubyte(buffer);
968 
969 		foreach (i, e; buffer)
970 		{
971 			auto I =  (e > _intensity) ? _intensity : e;
972 			_image[i] = new RGBColor(I, I, I);
973 		}
974 	}
975 
976 	override void saver()
977 	{
978 		_file.writeln(_intensity);
979 
980 		foreach (e; _image.array)
981 		{
982 			ubyte I;
983 			if ((e.getR == e.getG) && (e.getG == e.getB) && (e.getR == e.getB))
984 			{
985 				I = e.getR.to!ubyte;
986 			}
987 			else
988 			{
989 				I = e.luminance601.to!ubyte;
990 			}
991 
992 			_file.write(
993 				cast(char) I
994 			);
995 		}
996      }
997 }
998 
999 /**
1000 	A class that provides the ability to work with color images in P4 format. 
1001 	NB: The format is raw binary. 
1002 	
1003 	Note: 
1004 		This class supports indexing and assigning values ​​to specific pixels via 1D or 2D indexing, and provides P4 file loading/saving capabilities. 
1005 		According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pbm`.
1006 		
1007 	Typical usage:
1008     ----
1009     // create empty P4 image
1010     auto img = new P4Image; 
1011     // load from file 					
1012     img.load(`Lenna.pbm`);   					
1013     // set pixel at (10; 10) as white
1014     img[10, 10] = new RGBColor(255, 255, 255); 	
1015     // get 11th pixel
1016     img[10].writeln;							
1017     // save to file
1018     img.save(`Lenna2.pbm`);						
1019     
1020     // new P4 image, size is 10x10, all pixels black
1021     auto img2 = new P4Image(10, 10, new RGBColor(0, 0, 0)); 
1022     // increase two times
1023     img2[10] = img2[10] * 2; 									
1024     // save as pbm file
1025     img2.save(`test.pbm`);										
1026     ----
1027 */
1028 class P4Image : PixMapFile
1029 {
1030 	mixin addConstructor!(PixMapFormat.PBM_BINARY);
1031 
1032 	auto setBit(int value, int n)
1033 	{
1034 		return (value | (1 << n));
1035 	}
1036 
1037 	auto getBit(int value, int n)
1038 	{
1039 		return ((value >> n) & 1);
1040 	}
1041 
1042 	auto clearBit(int value, int n)
1043 	{
1044 		return (value & ~(1 << n));
1045 	}
1046 	
1047 	auto BLACK = new RGBColor(0, 0, 0);
1048 	auto WHITE = new RGBColor(255, 255, 255);
1049 
1050 	override void loader()
1051 	{
1052 		auto imageSize = width * height;
1053 		auto buffer = new ubyte[imageSize];
1054 		_file.rawRead!ubyte(buffer);
1055 
1056 		int index;
1057 
1058 		foreach (e; buffer)
1059 		{
1060 			if (index < imageSize)
1061 			{
1062 				foreach (i; 0..8)
1063 				{
1064 					auto I = getBit(cast(int) e, 7 - i);
1065 					_image[index] = I ? BLACK : WHITE;
1066 					index++;
1067 				}
1068 			}
1069 			else
1070 			{
1071 				break;
1072 			}
1073 		}				
1074 	}
1075 
1076 	override void saver()
1077 	{	    
1078 	    foreach (e; _image.array.chunks(width))
1079 	    {
1080 			foreach (r; e.chunks(8))
1081 			{
1082 				auto bits = 0x00;
1083 				
1084 				foreach (i, b; r)
1085 				{
1086 					auto I = (b.luminance == 0) ? 1 : 0;
1087 					
1088 					if (I == 1)
1089 					{
1090 						bits = setBit(bits, cast(int) (7 - i));
1091 					}
1092 				}
1093 				_file.write(
1094 					cast(char) bits
1095 				);
1096 			}
1097 		}
1098 	}
1099 }
1100 
1101 /**
1102 A constructor function that creates an image with the given length, width, and format. 
1103 By default, all parameters are 0, and the format is represented by the PixMapFormat.PPM_BINARY value, which corresponds to an image with a P6 format.
1104 Params:
1105 width = Width of image as size_t value. 
1106 height = Height of image as size_t value.
1107 pmFormat = Image format as enum PixMapFormat
1108 
1109 Typical usage:
1110 ----
1111 auto img = image(20, 20, PixMapFormat.PPM_TEXT); 	// creates image with P3 format type
1112 ----
1113 */
1114 PixMapFile image(size_t width = 0, size_t height = 0, PixMapFormat pmFormat = PixMapFormat.PPM_BINARY)
1115 {
1116 	PixMapFile pixmap;
1117 
1118 	final switch (pmFormat) with (PixMapFormat)
1119 	{
1120 		case PBM_TEXT:
1121 			pixmap = new P1Image(width, height);
1122 			break;
1123 		case PBM_BINARY:
1124 			pixmap = new P4Image(width, height);
1125 			break;
1126 		case PGM_TEXT:
1127 			pixmap = new P2Image(width, height);
1128 			break;
1129 		case PGM_BINARY:
1130 			pixmap = new P5Image(width, height);
1131 			break;
1132 		case PPM_TEXT:
1133 			pixmap = new P3Image(width, height);
1134 			break;
1135 		case PPM_BINARY:
1136 			pixmap = new P6Image(width, height);
1137 			break;
1138 	}
1139 
1140 	return pixmap;
1141 }
1142 
1143 /**
1144 A constructor function that creates an image with the given length, width, and format. 
1145 By default, all parameters are 0, and the format is represented by the "P6" value, which corresponds to an image with a P6 format.
1146 Params:
1147 width = Width of image as size_t value. 
1148 height = Height of image as size_t value.
1149 pmFormat = Image format as string
1150 
1151 Typical usage:
1152 ----
1153 auto img = image(20, 20, "P3"); 	// creates image with P3 format type
1154 ----
1155 */
1156 PixMapFile image(size_t width = 0, size_t height = 0, string pmFormat = "P6")
1157 {
1158 	PixMapFile pixmap;
1159 
1160 	switch (pmFormat) 
1161 	{
1162 		case "P1":
1163 			pixmap = new P1Image(width, height);
1164 			break;
1165 		case "P4":
1166 			pixmap = new P4Image(width, height);
1167 			break;
1168 		case "P2":
1169 			pixmap = new P2Image(width, height);
1170 			break;
1171 		case "P5":
1172 			pixmap = new P5Image(width, height);
1173 			break;
1174 		case "P3":
1175 			pixmap = new P3Image(width, height);
1176 			break;
1177 		case "P6":
1178 			pixmap = new P6Image(width, height);
1179 			break;
1180 		default:
1181 			assert(0);
1182 	}
1183 
1184 	return pixmap;
1185 }