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