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 PF_RGB_BINARY = "PF", 504 } 505 506 /** 507 Common ancestor for all subsequent image types. 508 Implements a generic way to load/save images by providing generic load/save methods. 509 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. 510 511 Implementation Note: The specific loading method is already implemented by descendant classes by overriding the abstract loader/saver methods in their implementations. 512 */ 513 class PixMapFile 514 { 515 mixin(addProperty!(PixMapImage, "Image")); 516 517 protected 518 { 519 File _file; 520 PixMapFormat _header; 521 522 abstract void loader(); 523 abstract void saver(); 524 } 525 526 private 527 { 528 /// Set i/o mode for reading/writing Portable Anymap Images. Actual for OS Windows. For internal use. 529 auto IOMode(string mode) 530 { 531 532 if (isBinaryFormat) 533 { 534 return mode ~ `b`; 535 } 536 else 537 { 538 return mode; 539 } 540 } 541 } 542 543 /// Basic file loading procedure 544 void load(string filename) 545 { 546 with (_file) 547 { 548 open(filename, IOMode(`r`)); 549 550 if (readln.strip == EnumValue(_header)) 551 { 552 auto imageSize = readln.split; 553 auto width = imageSize[0].parse!int; 554 auto height = imageSize[1].parse!int; 555 556 _image = new PixMapImage(width, height); 557 558 loader; 559 } 560 } 561 } 562 563 /// Basic file saving procedure 564 void save(string filename) 565 { 566 with (_file) 567 { 568 open(filename, IOMode("w")); 569 writeln(EnumValue(_header)); 570 writeln(_image.width, " ", _image.height); 571 572 saver; 573 } 574 } 575 576 /// Is raw format ? 577 final bool isBinaryFormat() 578 { 579 return 580 ( 581 (_header == PixMapFormat.PBM_BINARY) | 582 (_header == PixMapFormat.PGM_BINARY) | 583 (_header == PixMapFormat.PPM_BINARY) | 584 (_header == PixMapFormat.PF_RGB_BINARY) 585 ); 586 } 587 588 /// Is text format ? 589 final bool isTextFormat() 590 { 591 return 592 ( 593 (_header == PixMapFormat.PBM_TEXT) | 594 (_header == PixMapFormat.PGM_TEXT) | 595 (_header == PixMapFormat.PPM_TEXT) 596 ); 597 } 598 599 /// Get image object as PixMapImage object 600 final PixMapImage image() 601 { 602 return this.getImage; 603 } 604 605 /// Set image object as PixMapImage object 606 final void image(PixMapImage image) 607 { 608 this.setImage(image); 609 } 610 611 /// Convenient alias for working with PixMapFile same as PixMapImage 612 alias image this; 613 } 614 615 /** 616 A class that provides the ability to work with color images in P6 format. 617 NB: The format is raw binary. 618 619 Note: 620 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides P6 file loading/saving capabilities. 621 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.ppm`. 622 623 Typical usage: 624 ---- 625 // creating of empty image 626 auto img = new P6Image; 627 // load image from file `Lenna.ppm` 628 img.load(`Lenna.ppm`); 629 // change pixel at coords (10; 10), now are white 630 img[10, 10] = new RGBColor(255, 255, 255); 631 // get color of 11th pixel 632 img[10].writeln; 633 // save file as `Lenna2.ppm` 634 img.save(`Lenna2.ppm`); 635 636 // creating image of 10x10, all pixels are red 637 auto img2 = new P6Image(10, 10, new RGBColor(255, 0, 255)); 638 // increasing luminance by two 639 img2[10] = img2[10] * 2; 640 // save as `test.ppm` 641 img2.save(`test.ppm`); 642 ---- 643 */ 644 class P6Image : PixMapFile 645 { 646 mixin(addProperty!(int, "Intensity", "255")); 647 mixin addConstructor!(PixMapFormat.PPM_BINARY); 648 649 override void loader() 650 { 651 auto data = _file.readln; 652 _intensity = data.parse!int; 653 654 auto buffer = new ubyte[width * 3]; 655 656 for (uint i = 0; i < height; i++) 657 { 658 _file.rawRead!ubyte(buffer); 659 660 for (uint j = 0; j < width; j++) 661 { 662 auto R = buffer[j * 3]; 663 auto G = buffer[j * 3 + 1]; 664 auto B = buffer[j * 3 + 2]; 665 _image[j, i] = new RGBColor( 666 (R > _intensity) ? _intensity : R, 667 (G > _intensity) ? _intensity : G, 668 (B > _intensity) ? _intensity : B 669 ); 670 } 671 } 672 } 673 674 override void saver() 675 { 676 _file.writeln(_intensity); 677 678 foreach (e; _image.array) 679 { 680 auto R = e.getR; 681 auto G = e.getG; 682 auto B = e.getB; 683 684 auto rr = (R > _intensity) ? _intensity : R; 685 auto gg = (G > _intensity) ? _intensity : G; 686 auto bb = (B > _intensity) ? _intensity : B; 687 688 _file.write( 689 cast(char) rr, 690 cast(char) gg, 691 cast(char) bb 692 ); 693 } 694 } 695 } 696 697 /** 698 A class that provides the ability to work with color images in P3 format. 699 NB: The format is raw text. 700 701 Note: 702 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides P3 file loading/saving capabilities. 703 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.ppm`. 704 705 Typical usage: 706 ---- 707 // creating of empty image 708 auto img = new P3Image; 709 // load image from file `Lenna.ppm` 710 img.load(`Lenna.ppm`); 711 // change pixel at coords (10; 10), now are white 712 img[10, 10] = new RGBColor(255, 255, 255); 713 // get color of 11th pixel 714 img[10].writeln; 715 // save file as `Lenna2.ppm` 716 img.save(`Lenna2.ppm`); 717 718 // creating image of 10x10, all pixels are red 719 auto img2 = new P3Image(10, 10, new RGBColor(255, 0, 255)); 720 // increasing luminance by two 721 img2[10] = img2[10] * 2; 722 // save as `test.ppm` 723 img2.save(`test.ppm`); 724 ---- 725 */ 726 class P3Image : PixMapFile 727 { 728 mixin(addProperty!(int, "Intensity", "255")); 729 mixin addConstructor!(PixMapFormat.PPM_TEXT); 730 731 override void loader() 732 { 733 // skip maximal intensity description 734 auto data = _file.readln; 735 _intensity = data.parse!int; 736 737 string triplet; 738 int index = 0; 739 740 while ((triplet = _file.readln) !is null) 741 { 742 auto rgb = triplet.split; 743 auto R = rgb[0].parse!int; 744 auto G = rgb[1].parse!int; 745 auto B = rgb[2].parse!int; 746 747 _image[index] = new RGBColor( 748 (R > _intensity) ? _intensity : R, 749 (G > _intensity) ? _intensity : G, 750 (B > _intensity) ? _intensity : B 751 ); 752 index++; 753 } 754 } 755 756 override void saver() 757 { 758 _file.writeln(_intensity); 759 760 foreach (e; _image.array) 761 { 762 auto R = e.getR; 763 auto G = e.getG; 764 auto B = e.getB; 765 766 _file.writefln( 767 "%d %d %d", 768 (R > _intensity) ? _intensity : R, 769 (G > _intensity) ? _intensity : G, 770 (B > _intensity) ? _intensity : B 771 ); 772 } 773 } 774 } 775 776 /** 777 A class that provides the ability to work with color images in P1 format. 778 NB: The format is raw text. 779 780 Note: 781 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides P1 file loading/saving capabilities. 782 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pbm`. 783 784 Typical usage: 785 ---- 786 // creating of empty image 787 auto img = new P1Image; 788 // load image from file `Lenna.pbm` 789 img.load(`Lenna.pbm`); 790 // change pixel at coords (10; 10), now are white 791 img[10, 10] = new RGBColor(255, 255, 255); 792 // get color of 11th pixel 793 img[10].writeln; 794 // save file as `Lenna2.pbm` 795 mg.save(`Lenna2.pbm`); 796 797 // creating image of 10x10, all pixels are black 798 auto img2 = new P1Image(10, 10, new RGBColor(0, 0, 0)); 799 // increasing luminance by two 800 img2[10] = img2[10] * 2; 801 // save as `test.pbm` 802 img2.save(`test.pbm`); 803 ---- 804 */ 805 class P1Image : PixMapFile 806 { 807 mixin addConstructor!(PixMapFormat.PBM_TEXT); 808 809 override void loader() 810 { 811 string line; 812 int index; 813 814 auto WHITE = new RGBColor(255, 255, 255); 815 auto BLACK = new RGBColor(0, 0, 0); 816 817 while ((line = _file.readln) !is null) 818 { 819 auto row = line.replace(" ", "").replace("\n", ""); 820 821 foreach (i, e; row) 822 { 823 _image[i, index] = (e.to!string == "0") ? WHITE : BLACK; 824 } 825 index++; 826 } 827 } 828 829 override void saver() 830 { 831 foreach (rows; _image.array.chunks(width)) 832 { 833 _file.writeln( 834 rows 835 .map!(a => (a.luminance < 255) ? "1" : "0") 836 .join(" ") 837 ); 838 } 839 } 840 } 841 842 /** 843 A class that provides the ability to work with color images in P2 format. 844 NB: The format is raw text. 845 846 Note: 847 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides P2 file loading/saving capabilities. 848 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pgm`. 849 850 Typical usage: 851 ---- 852 // creating of empty image 853 auto img = new P2Image; 854 // load image from file `Lenna.pgm` 855 img.load(`Lenna.pgm`); 856 // change pixel at coords (10; 10), now are white 857 img[10, 10] = new RGBColor(255, 255, 255); 858 // get color of 11th pixel 859 img[10].writeln; 860 // save file as `Lenna2.pgm` 861 img.save(`Lenna2.pgm`); 862 863 // creating image of 10x10, pixels are black 864 auto img2 = new P2Image(10, 10, new RGBColor(0, 0, 0)); 865 // increasing luminance by two 866 img2[10] = img2[10] * 2; 867 // save as `test.pgm` 868 img2.save(`test.pgm`); 869 ---- 870 */ 871 class P2Image : PixMapFile 872 { 873 mixin(addProperty!(int, "Intensity", "255")); 874 mixin addConstructor!(PixMapFormat.PGM_TEXT); 875 876 override void loader() 877 { 878 // skip maximal intensity description 879 auto data = _file.readln; 880 _intensity = data.parse!int; 881 882 string line; 883 int index; 884 885 while ((line = _file.readln) !is null) 886 { 887 auto row = line.split; 888 889 foreach (i, e; row) 890 { 891 auto l = e.parse!int; 892 auto I = (l > _intensity) ? _intensity : l; 893 _image[i, index] = new RGBColor(I, I, I); 894 } 895 index++; 896 } 897 } 898 899 override void saver() 900 { 901 _file.writeln(_intensity); 902 903 foreach (rows; _image.array.chunks(width)) 904 { 905 auto toIntensity(RGBColor color) 906 { 907 int I; 908 if ((color.getR == color.getG) && (color.getG == color.getB) && (color.getR == color.getB)) 909 { 910 I = color.getR; 911 } 912 else 913 { 914 I = color.luminance601.to!int; 915 } 916 return (I > _intensity) ? _intensity : I; 917 } 918 919 _file.writeln( 920 rows 921 .map!(a => toIntensity(a).to!string) 922 .join(" ") 923 ); 924 } 925 } 926 } 927 928 /** 929 A class that provides the ability to work with color images in P5 format. 930 NB: The format is raw binary. 931 932 Note: 933 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides P5 file loading/saving capabilities. 934 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pgm`. 935 936 Typical usage: 937 ---- 938 // create empty image 939 auto img = new P5Image; 940 // load from file 941 img.load(`Lenna.pgm`); 942 // set pixel at (10;10) to white color 943 img[10, 10] = new RGBColor(255, 255, 255); 944 // get color of 11th pixel 945 img[10].writeln; 946 // save to file 947 img.save(`Lenna2.pgm`); 948 949 // creating image of size 10x10, all pixels black 950 auto img2 = new P5Image(10, 10, new RGBColor(0, 0, 0)); 951 // increase luminance twice 952 img2[10] = img2[10] * 2; 953 // save as pgm file 954 img2.save(`test.pgm`); 955 ---- 956 */ 957 class P5Image : PixMapFile 958 { 959 mixin(addProperty!(int, "Intensity", "255")); 960 mixin addConstructor!(PixMapFormat.PGM_BINARY); 961 962 override void loader() 963 { 964 // skip maximal intensity description 965 auto data = _file.readln; 966 _intensity = data.to!int; 967 968 auto buffer = new ubyte[width * height]; 969 _file.rawRead!ubyte(buffer); 970 971 foreach (i, e; buffer) 972 { 973 auto I = (e > _intensity) ? _intensity : e; 974 _image[i] = new RGBColor(I, I, I); 975 } 976 } 977 978 override void saver() 979 { 980 _file.writeln(_intensity); 981 982 foreach (e; _image.array) 983 { 984 ubyte I; 985 if ((e.getR == e.getG) && (e.getG == e.getB) && (e.getR == e.getB)) 986 { 987 I = e.getR.to!ubyte; 988 } 989 else 990 { 991 I = e.luminance601.to!ubyte; 992 } 993 994 _file.write( 995 cast(char) I 996 ); 997 } 998 } 999 } 1000 1001 /** 1002 A class that provides the ability to work with color images in P4 format. 1003 NB: The format is raw binary. 1004 1005 Note: 1006 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides P4 file loading/saving capabilities. 1007 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pbm`. 1008 1009 Typical usage: 1010 ---- 1011 // create empty P4 image 1012 auto img = new P4Image; 1013 // load from file 1014 img.load(`Lenna.pbm`); 1015 // set pixel at (10; 10) as white 1016 img[10, 10] = new RGBColor(255, 255, 255); 1017 // get 11th pixel 1018 img[10].writeln; 1019 // save to file 1020 img.save(`Lenna2.pbm`); 1021 1022 // new P4 image, size is 10x10, all pixels black 1023 auto img2 = new P4Image(10, 10, new RGBColor(0, 0, 0)); 1024 // increase two times 1025 img2[10] = img2[10] * 2; 1026 // save as pbm file 1027 img2.save(`test.pbm`); 1028 ---- 1029 */ 1030 class P4Image : PixMapFile 1031 { 1032 mixin addConstructor!(PixMapFormat.PBM_BINARY); 1033 1034 auto setBit(int value, int n) 1035 { 1036 return (value | (1 << n)); 1037 } 1038 1039 auto getBit(int value, int n) 1040 { 1041 return ((value >> n) & 1); 1042 } 1043 1044 auto clearBit(int value, int n) 1045 { 1046 return (value & ~(1 << n)); 1047 } 1048 1049 auto BLACK = new RGBColor(0, 0, 0); 1050 auto WHITE = new RGBColor(255, 255, 255); 1051 1052 override void loader() 1053 { 1054 auto imageSize = width * height; 1055 auto buffer = new ubyte[imageSize]; 1056 _file.rawRead!ubyte(buffer); 1057 1058 int index; 1059 1060 foreach (e; buffer) 1061 { 1062 if (index < imageSize) 1063 { 1064 foreach (i; 0..8) 1065 { 1066 auto I = getBit(cast(int) e, 7 - i); 1067 _image[index] = I ? BLACK : WHITE; 1068 index++; 1069 } 1070 } 1071 else 1072 { 1073 break; 1074 } 1075 } 1076 } 1077 1078 override void saver() 1079 { 1080 foreach (e; _image.array.chunks(width)) 1081 { 1082 foreach (r; e.chunks(8)) 1083 { 1084 auto bits = 0x00; 1085 1086 foreach (i, b; r) 1087 { 1088 auto I = (b.luminance == 0) ? 1 : 0; 1089 1090 if (I == 1) 1091 { 1092 bits = setBit(bits, cast(int) (7 - i)); 1093 } 1094 } 1095 _file.write( 1096 cast(char) bits 1097 ); 1098 } 1099 } 1100 } 1101 } 1102 1103 /// Endianess (i.e byte-order) 1104 enum BYTE_ORDER 1105 { 1106 /// Little-endian byte-order 1107 LITTLE_ENDIAN, 1108 /// Big-endian byte-order 1109 BIG_ENDIAN 1110 } 1111 1112 1113 /** 1114 A class that provides the ability to work with color images in PF (portable floatmpa image) format. 1115 NB: The format is raw binary. Support of this format is EXPERIMENTAL (!!!). 1116 1117 Note: 1118 This class supports indexing and assigning values to specific pixels via 1D or 2D indexing, and provides PF file loading/saving capabilities. 1119 According to the accepted convention, in the original description of the format inside the Netpbm package, the extension of these files should be `*.pfm`. 1120 1121 Typical usage: 1122 ---- 1123 // create empty PF image 1124 auto img = new PFImage; 1125 // load from file 1126 img.load(`Lenna.pfm`); 1127 // set pixel at (10; 10) as white 1128 img[10, 10] = new RGBColor(255, 255, 255); 1129 // get 11th pixel 1130 img[10].writeln; 1131 // save to file 1132 img.save(`Lenna2.pfm`); 1133 1134 // new PF image, size is 10x10, all pixels black 1135 auto img2 = new PFImage(10, 10, new RGBColor(0, 0, 0)); 1136 // increase two times 1137 img2[10] = img2[10] * 2; 1138 // select byte order for saving (by default, little-endian, i.e BYTE_ORDER.LITTLE_ENDIAN) 1139 img2.setOrder(BYTE_ORDER.BIG_ENDIAN); 1140 // save as pfm file 1141 img2.save(`test.pfm`); 1142 ---- 1143 */ 1144 class PFImage : PixMapFile 1145 { 1146 mixin(addProperty!(uint, "Intensity", "255")); 1147 mixin(addProperty!(uint, "Order", "BYTE_ORDER.LITTLE_ENDIAN")); 1148 1149 mixin addConstructor!(PixMapFormat.PF_RGB_BINARY); 1150 1151 private 1152 { 1153 /// reconstruct unsigned integer value from unsigned bytes (little-endian order) 1154 static uint fromLEBytes(ubyte[] bytes) 1155 { 1156 return ((bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]); 1157 } 1158 1159 /// reconstruct unsigned integer value from unsigned bytes (big-endian order) 1160 static uint fromBEBytes(ubyte[] bytes) 1161 { 1162 return ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]); 1163 } 1164 1165 static ubyte[] toBEBytes(uint value) 1166 { 1167 ubyte[] bytes; 1168 1169 bytes ~= (value & 0xff000000) >> 24; 1170 bytes ~= (value & 0x00ff0000) >> 16; 1171 bytes ~= (value & 0x0000ff00) >> 8; 1172 bytes ~= (value & 0x000000ff); 1173 1174 return bytes; 1175 } 1176 1177 static ubyte[] toLEBytes(uint value) 1178 { 1179 ubyte[] bytes; 1180 1181 bytes ~= (value & 0x000000ff); 1182 bytes ~= (value & 0x0000ff00) >> 8; 1183 bytes ~= (value & 0x00ff0000) >> 16; 1184 bytes ~= (value & 0xff000000) >> 24; 1185 1186 return bytes; 1187 } 1188 } 1189 1190 override void loader() 1191 { 1192 auto data = _file.readln; 1193 auto ef = data.parse!float; 1194 1195 uint function(ubyte[]) byteLoader; 1196 1197 if (ef < 0) 1198 { 1199 _order = BYTE_ORDER.LITTLE_ENDIAN; 1200 byteLoader = &fromLEBytes; 1201 } 1202 else 1203 { 1204 _order = BYTE_ORDER.BIG_ENDIAN; 1205 byteLoader = &fromBEBytes; 1206 } 1207 1208 float bytes2float(ubyte[] bytes) 1209 { 1210 uint tmp = byteLoader(bytes); 1211 float value = *(cast(float*) &tmp); 1212 return value; 1213 } 1214 1215 auto blockSize = 3 * float.sizeof; 1216 auto buffer = new ubyte[_width * blockSize]; 1217 1218 foreach (i; 0.._height) 1219 { 1220 _file.rawRead!ubyte(buffer); 1221 1222 foreach (j; 0.._width) 1223 { 1224 auto wq = buffer[(j * blockSize)..(j * blockSize + blockSize)]; 1225 1226 _image[j, _height - i] = new RGBColor( 1227 cast(int) (_intensity * bytes2float(wq[0..4])), 1228 cast(int) (_intensity * bytes2float(wq[4..8])), 1229 cast(int) (_intensity * bytes2float(wq[8..12])) 1230 ); 1231 } 1232 } 1233 } 1234 1235 override void saver() 1236 { 1237 ubyte[] function(uint) byteSaver; 1238 1239 final switch (_order) with (BYTE_ORDER) { 1240 case LITTLE_ENDIAN: 1241 _file.writeln(-1.0); 1242 byteSaver = &toLEBytes; 1243 break; 1244 case BIG_ENDIAN: 1245 _file.writeln(1.0); 1246 byteSaver = &toBEBytes; 1247 break; 1248 } 1249 1250 ubyte[] int2bytes(int value) 1251 { 1252 float I = float(value) / float(_intensity); 1253 uint tmp = *(cast(uint*) &I); 1254 return byteSaver(tmp); 1255 } 1256 1257 foreach (i; 0.._height) 1258 { 1259 foreach (j; 0.._width) 1260 { 1261 auto color = _image[j, _height - i]; 1262 1263 _file.write( 1264 cast(char[]) int2bytes(color.getR), 1265 cast(char[]) int2bytes(color.getG), 1266 cast(char[]) int2bytes(color.getB) 1267 ); 1268 } 1269 } 1270 } 1271 } 1272 1273 /** 1274 A constructor function that creates an image with the given length, width, and format. 1275 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. 1276 Params: 1277 width = Width of image as size_t value. 1278 height = Height of image as size_t value. 1279 pmFormat = Image format as enum PixMapFormat 1280 1281 Typical usage: 1282 ---- 1283 auto img = image(20, 20, PixMapFormat.PPM_TEXT); // creates image with P3 format type 1284 ---- 1285 */ 1286 PixMapFile image(size_t width = 0, size_t height = 0, PixMapFormat pmFormat = PixMapFormat.PPM_BINARY) 1287 { 1288 PixMapFile pixmap; 1289 1290 final switch (pmFormat) with (PixMapFormat) 1291 { 1292 case PBM_TEXT: 1293 pixmap = new P1Image(width, height); 1294 break; 1295 case PBM_BINARY: 1296 pixmap = new P4Image(width, height); 1297 break; 1298 case PGM_TEXT: 1299 pixmap = new P2Image(width, height); 1300 break; 1301 case PGM_BINARY: 1302 pixmap = new P5Image(width, height); 1303 break; 1304 case PPM_TEXT: 1305 pixmap = new P3Image(width, height); 1306 break; 1307 case PPM_BINARY: 1308 pixmap = new P6Image(width, height); 1309 break; 1310 case PF_RGB_BINARY: 1311 pixmap = new PFImage(width, height); 1312 break; 1313 } 1314 1315 return pixmap; 1316 } 1317 1318 /** 1319 A constructor function that creates an image with the given length, width, and format. 1320 By default, all parameters are 0, and the format is represented by the "P6" value, which corresponds to an image with a P6 format. 1321 Params: 1322 width = Width of image as size_t value. 1323 height = Height of image as size_t value. 1324 pmFormat = Image format as string 1325 1326 Typical usage: 1327 ---- 1328 auto img = image(20, 20, "P3"); // creates image with P3 format type 1329 ---- 1330 */ 1331 PixMapFile image(size_t width = 0, size_t height = 0, string pmFormat = "P6") 1332 { 1333 PixMapFile pixmap; 1334 1335 switch (pmFormat) 1336 { 1337 case "P1": 1338 pixmap = new P1Image(width, height); 1339 break; 1340 case "P4": 1341 pixmap = new P4Image(width, height); 1342 break; 1343 case "P2": 1344 pixmap = new P2Image(width, height); 1345 break; 1346 case "P5": 1347 pixmap = new P5Image(width, height); 1348 break; 1349 case "P3": 1350 pixmap = new P3Image(width, height); 1351 break; 1352 case "P6": 1353 pixmap = new P6Image(width, height); 1354 break; 1355 case "PF": 1356 pixmap = new PFImage(width, height); 1357 break; 1358 default: 1359 assert(0); 1360 } 1361 1362 return pixmap; 1363 }