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