1 /** 2 * In-memory images and various image formats. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.graphics.image; 15 16 import std.algorithm; 17 import std.conv : to; 18 import std.exception; 19 import std.range; 20 import std.string : format; 21 22 public import ae.utils.graphics.view; 23 24 /// Represents a reference to COLOR data 25 /// already existing elsewhere in memory. 26 /// Assumes that pixels are stored row-by-row, 27 /// with a known distance between each row. 28 struct ImageRef(COLOR) 29 { 30 int w, h; 31 size_t pitch; /// In bytes, not COLORs 32 COLOR* pixels; 33 34 /// Returns an array for the pixels at row y. 35 COLOR[] scanline(int y) 36 { 37 assert(y>=0 && y<h); 38 assert(pitch); 39 auto row = cast(COLOR*)(cast(ubyte*)pixels + y*pitch); 40 return row[0..w]; 41 } 42 43 mixin DirectView; 44 } 45 46 unittest 47 { 48 static assert(isDirectView!(ImageRef!ubyte)); 49 } 50 51 /// Convert a direct view to an ImageRef. 52 /// Assumes that the rows are evenly spaced. 53 ImageRef!(ViewColor!SRC) toRef(SRC)(auto ref SRC src) 54 if (isDirectView!SRC) 55 { 56 return ImageRef!(ViewColor!SRC)(src.w, src.h, 57 src.h > 1 ? cast(ubyte*)src.scanline(1) - cast(ubyte*)src.scanline(0) : src.w, 58 src.scanline(0).ptr); 59 } 60 61 unittest 62 { 63 auto i = Image!ubyte(1, 1); 64 auto r = i.toRef(); 65 assert(r.scanline(0).ptr is i.scanline(0).ptr); 66 } 67 68 // *************************************************************************** 69 70 /// An in-memory image. 71 /// Pixels are stored in a flat array. 72 struct Image(COLOR) 73 { 74 int w, h; 75 COLOR[] pixels; 76 77 /// Returns an array for the pixels at row y. 78 COLOR[] scanline(int y) 79 { 80 assert(y>=0 && y<h); 81 return pixels[w*y..w*(y+1)]; 82 } 83 84 mixin DirectView; 85 86 this(int w, int h) 87 { 88 size(w, h); 89 } 90 91 /// Does not scale image 92 void size(int w, int h) 93 { 94 this.w = w; 95 this.h = h; 96 if (pixels.length < w*h) 97 pixels.length = w*h; 98 } 99 } 100 101 unittest 102 { 103 static assert(isDirectView!(Image!ubyte)); 104 } 105 106 // *************************************************************************** 107 108 // Functions which need a target image to operate on are currenty declared 109 // as two overloads. The code might be simplified if some of these get fixed: 110 // https://d.puremagic.com/issues/show_bug.cgi?id=8074 111 // https://d.puremagic.com/issues/show_bug.cgi?id=12386 112 // https://d.puremagic.com/issues/show_bug.cgi?id=12425 113 // https://d.puremagic.com/issues/show_bug.cgi?id=12426 114 // https://d.puremagic.com/issues/show_bug.cgi?id=12433 115 116 alias ViewImage(V) = Image!(ViewColor!V); 117 118 /// Copy the given view into the specified target. 119 auto copy(SRC, TARGET)(auto ref SRC src, auto ref TARGET target) 120 if (isView!SRC && isWritableView!TARGET) 121 { 122 target.size(src.w, src.h); 123 src.blitTo(target); 124 return target; 125 } 126 127 /// Copy the given view into a newly-allocated image. 128 auto copy(SRC)(auto ref SRC src) 129 if (isView!SRC) 130 { 131 ViewImage!SRC target; 132 return src.copy(target); 133 } 134 135 unittest 136 { 137 auto v = onePixel(0); 138 auto i = v.copy(); 139 v.copy(i); 140 141 auto c = i.crop(0, 0, 1, 1); 142 v.copy(c); 143 } 144 145 alias ElementViewImage(R) = ViewImage!(ElementType!R); 146 147 /// Splice multiple images horizontally. 148 auto hjoin(R, TARGET)(R images, auto ref TARGET target) 149 if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET) 150 { 151 int w, h; 152 foreach (ref image; images) 153 w += image.w, 154 h = max(h, image.h); 155 target.size(w, h); 156 int x; 157 foreach (ref image; images) 158 image.blitTo(target, x, 0), 159 x += image.w; 160 return target; 161 } 162 /// ditto 163 auto hjoin(R)(R images) 164 if (isInputRange!R && isView!(ElementType!R)) 165 { 166 ElementViewImage!R target; 167 return images.hjoin(target); 168 } 169 170 /// Splice multiple images vertically. 171 auto vjoin(R, TARGET)(R images, auto ref TARGET target) 172 if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET) 173 { 174 int w, h; 175 foreach (ref image; images) 176 w = max(w, image.w), 177 h += image.h; 178 target.size(w, h); 179 int y; 180 foreach (ref image; images) 181 image.blitTo(target, 0, y), 182 y += image.h; 183 return target; 184 } 185 /// ditto 186 auto vjoin(R)(R images) 187 if (isInputRange!R && isView!(ElementType!R)) 188 { 189 ElementViewImage!R target; 190 return images.vjoin(target); 191 } 192 193 unittest 194 { 195 auto h = 10 196 .iota 197 .retro 198 .map!onePixel 199 .retro 200 .hjoin(); 201 202 foreach (i; 0..10) 203 assert(h[i, 0] == i); 204 205 auto v = 10.iota.map!onePixel.vjoin(); 206 foreach (i; 0..10) 207 assert(v[0, i] == i); 208 } 209 210 // *************************************************************************** 211 212 /// Performs linear downscale by a constant factor 213 template downscale(int HRX, int HRY=HRX) 214 { 215 auto downscale(SRC, TARGET)(auto ref SRC src, auto ref TARGET target) 216 if (isDirectView!SRC && isWritableView!TARGET) 217 { 218 alias lr = target; 219 alias hr = src; 220 221 assert(hr.w % HRX == 0 && hr.h % HRY == 0, "Size mismatch"); 222 223 lr.size(hr.w / HRX, hr.h / HRY); 224 225 foreach (y; 0..lr.h) 226 foreach (x; 0..lr.w) 227 { 228 static if (HRX*HRY <= 0x100) 229 enum EXPAND_BYTES = 1; 230 else 231 static if (HRX*HRY <= 0x10000) 232 enum EXPAND_BYTES = 2; 233 else 234 static assert(0); 235 static if (is(typeof(COLOR.init.a))) // downscale with alpha 236 { 237 ExpandType!(COLOR, EXPAND_BYTES+COLOR.init.a.sizeof) sum; 238 ExpandType!(typeof(COLOR.init.a), EXPAND_BYTES) alphaSum; 239 auto start = y*HRY*hr.stride + x*HRX; 240 foreach (j; 0..HRY) 241 { 242 foreach (p; hr.pixels[start..start+HRX]) 243 { 244 foreach (i, f; p.tupleof) 245 static if (p.tupleof[i].stringof != "p.a") 246 { 247 enum FIELD = p.tupleof[i].stringof[2..$]; 248 mixin("sum."~FIELD~" += cast(typeof(sum."~FIELD~"))p."~FIELD~" * p.a;"); 249 } 250 alphaSum += p.a; 251 } 252 start += hr.stride; 253 } 254 if (alphaSum) 255 { 256 auto result = cast(COLOR)(sum / alphaSum); 257 result.a = cast(typeof(result.a))(alphaSum / (HRX*HRY)); 258 lr[x, y] = result; 259 } 260 else 261 { 262 static assert(COLOR.init.a == 0); 263 lr[x, y] = COLOR.init; 264 } 265 } 266 else 267 { 268 ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum; 269 auto x0 = x*HRX; 270 auto x1 = x0+HRX; 271 foreach (j; y*HRY..(y+1)*HRY) 272 foreach (p; hr.scanline(j)[x0..x1]) 273 sum += p; 274 lr[x, y] = cast(ViewColor!SRC)(sum / (HRX*HRY)); 275 } 276 } 277 278 return target; 279 } 280 281 auto downscale(SRC)(auto ref SRC src) 282 if (isView!SRC) 283 { 284 ViewImage!SRC target; 285 return src.downscale(target); 286 } 287 } 288 289 unittest 290 { 291 onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)(); 292 } 293 294 // *************************************************************************** 295 296 /// Copy the indicated row of src to a COLOR buffer. 297 void copyScanline(SRC, COLOR)(auto ref SRC src, int y, COLOR[] dst) 298 if (isView!SRC && is(COLOR == ViewColor!SRC)) 299 { 300 static if (isDirectView!SRC) 301 dst[] = src.scanline(y)[]; 302 else 303 { 304 assert(src.w == dst.length); 305 foreach (x; 0..src.w) 306 dst[x] = src[x, y]; 307 } 308 } 309 310 /// Copy a view's pixels (top-to-bottom) to a COLOR buffer. 311 void copyPixels(SRC, COLOR)(auto ref SRC src, COLOR[] dst) 312 if (isView!SRC && is(COLOR == ViewColor!SRC)) 313 { 314 assert(dst.length == src.w * src.h); 315 foreach (y; 0..src.h) 316 src.copyScanline(y, dst[y*src.w..(y+1)*src.w]); 317 } 318 319 // *************************************************************************** 320 321 import std.traits; 322 323 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433 324 325 struct InputColor {} 326 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR); 327 328 struct TargetColor {} 329 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET); 330 331 // *************************************************************************** 332 333 import ae.utils.graphics.color; 334 import ae.utils.meta.misc; 335 336 private string[] readPBMHeader(ref const(ubyte)[] data) 337 { 338 import std.ascii; 339 340 string[] fields; 341 uint wordStart = 0; 342 uint p; 343 for (p=1; p<data.length && fields.length<4; p++) 344 if (!isWhite(data[p-1]) && isWhite(data[p])) 345 fields ~= cast(string)data[wordStart..p]; 346 else 347 if (isWhite(data[p-1]) && !isWhite(data[p])) 348 wordStart = p; 349 data = data[p..$]; 350 enforce(fields.length==4, "Header too short"); 351 enforce(fields[0].length==2 && fields[0][0]=='P', "Invalid signature"); 352 return fields; 353 } 354 355 private template PBMSignature(COLOR) 356 { 357 static if (structFields!COLOR == ["l"]) 358 enum PBMSignature = "P5"; 359 else 360 static if (structFields!COLOR == ["r", "g", "b"]) 361 enum PBMSignature = "P6"; 362 else 363 static assert(false, "Unsupported PBM color: " ~ 364 __traits(allMembers, COLOR.Fields).stringof); 365 } 366 367 /// Parses a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 368 auto parsePBM(C = TargetColor, TARGET)(const(void)[] vdata, auto ref TARGET target) 369 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 370 { 371 alias COLOR = ViewColor!TARGET; 372 373 auto data = cast(const(ubyte)[])vdata; 374 string[] fields = readPBMHeader(data); 375 enforce(fields[0]==PBMSignature!COLOR, "Invalid signature"); 376 enforce(to!uint(fields[3]) == COLOR.tupleof[0].max, "Channel depth mismatch"); 377 378 target.size(to!uint(fields[1]), to!uint(fields[2])); 379 enforce(data.length / COLOR.sizeof == target.w * target.h, 380 "Dimension / filesize mismatch"); 381 target.pixels[] = cast(COLOR[])data; 382 383 static if (COLOR.tupleof[0].sizeof > 1) 384 foreach (ref pixel; pixels) 385 pixel = COLOR.op!q{swapBytes(a)}(pixel); // TODO: proper endianness support 386 387 return target; 388 } 389 /// ditto 390 auto parsePBM(COLOR)(const(void)[] vdata) 391 { 392 Image!COLOR target; 393 return vdata.parsePBM(target); 394 } 395 396 unittest 397 { 398 auto data = "P6\n2\n2\n255\n" ~ 399 x"000000 FFF000" 400 x"000FFF FFFFFF"; 401 auto i = data.parsePBM!RGB(); 402 assert(i[0, 0] == RGB.fromHex("000000")); 403 assert(i[0, 1] == RGB.fromHex("000FFF")); 404 } 405 406 unittest 407 { 408 auto data = "P5\n2\n2\n255\n" ~ 409 x"00 55" 410 x"AA FF"; 411 auto i = data.parsePBM!L8(); 412 assert(i[0, 0] == L8(0x00)); 413 assert(i[0, 1] == L8(0xAA)); 414 } 415 416 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 417 ubyte[] toPBM(SRC)(auto ref SRC src) 418 if (isView!SRC) 419 { 420 alias COLOR = ViewColor!SRC; 421 422 auto length = src.w * src.h; 423 ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n" 424 .format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max); 425 ubyte[] data = new ubyte[header.length + length * COLOR.sizeof]; 426 427 data[0..header.length] = header; 428 src.copyPixels(cast(COLOR[])data[header.length..$]); 429 430 static if (ChannelType!COLOR.sizeof > 1) 431 foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$]) 432 p = swapBytes(p); // TODO: proper endianness support 433 434 return data; 435 } 436 437 unittest 438 { 439 assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ x"01 02 03"); 440 assert(onePixel(L8 (1) ).toPBM == "P5\n1 1 255\n" ~ x"01" ); 441 } 442 443 // *************************************************************************** 444 445 /// Loads a raw COLOR[] into an image of the indicated size. 446 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h, 447 auto ref TARGET target) 448 if (isWritableView!TARGET 449 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET)) 450 { 451 alias COLOR = ViewColor!TARGET; 452 453 auto pixels = cast(COLOR[])input; 454 enforce(pixels.length == w*h, "Dimension / filesize mismatch"); 455 target.size(w, h); 456 target.pixels[] = pixels; 457 return target; 458 } 459 460 /// ditto 461 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h) 462 { 463 alias COLOR = GetInputColor!(C, INPUT); 464 Image!COLOR target; 465 return fromPixels!COLOR(input, w, h, target); 466 } 467 468 unittest 469 { 470 Image!L8 i; 471 i = x"42".fromPixels!L8(1, 1); 472 i = x"42".fromPixels!L8(1, 1, i); 473 assert(i[0, 0].l == 0x42); 474 i = (cast(L8[])x"42").fromPixels(1, 1); 475 i = (cast(L8[])x"42").fromPixels(1, 1, i); 476 } 477 478 // *************************************************************************** 479 480 alias int FXPT2DOT30; 481 struct CIEXYZ { FXPT2DOT30 ciexyzX, ciexyzY, ciexyzZ; } 482 struct CIEXYZTRIPLE { CIEXYZ ciexyzRed, ciexyzGreen, ciexyzBlue; } 483 enum { BI_BITFIELDS = 3 } 484 485 align(1) 486 struct BitmapHeader(uint V) 487 { 488 enum VERSION = V; 489 490 align(1): 491 // BITMAPFILEHEADER 492 char[2] bfType = "BM"; 493 uint bfSize; 494 ushort bfReserved1; 495 ushort bfReserved2; 496 uint bfOffBits; 497 498 // BITMAPCOREINFO 499 uint bcSize = this.sizeof - bcSize.offsetof; 500 int bcWidth; 501 int bcHeight; 502 ushort bcPlanes; 503 ushort bcBitCount; 504 uint biCompression; 505 uint biSizeImage; 506 uint biXPelsPerMeter; 507 uint biYPelsPerMeter; 508 uint biClrUsed; 509 uint biClrImportant; 510 511 // BITMAPV4HEADER 512 static if (V>=4) 513 { 514 uint bV4RedMask; 515 uint bV4GreenMask; 516 uint bV4BlueMask; 517 uint bV4AlphaMask; 518 uint bV4CSType; 519 CIEXYZTRIPLE bV4Endpoints; 520 uint bV4GammaRed; 521 uint bV4GammaGreen; 522 uint bV4GammaBlue; 523 } 524 525 // BITMAPV5HEADER 526 static if (V>=5) 527 { 528 uint bV5Intent; 529 uint bV5ProfileData; 530 uint bV5ProfileSize; 531 uint bV5Reserved; 532 } 533 } 534 535 template bitmapBitCount(COLOR) 536 { 537 static if (is(COLOR == BGR)) 538 enum bitmapBitCount = 24; 539 else 540 static if (is(COLOR == BGRX) || is(COLOR == BGRA)) 541 enum bitmapBitCount = 32; 542 else 543 static if (is(COLOR == L8)) 544 enum bitmapBitCount = 8; 545 else 546 static assert(false, "Unsupported BMP color type: " ~ COLOR.stringof); 547 } 548 549 @property int bitmapPixelStride(COLOR)(int w) 550 { 551 int pixelStride = w * cast(uint)COLOR.sizeof; 552 pixelStride = (pixelStride+3) & ~3; 553 return pixelStride; 554 } 555 556 /// Parses a Windows bitmap (.bmp) file. 557 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target) 558 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 559 { 560 alias COLOR = ViewColor!TARGET; 561 562 alias BitmapHeader!3 Header; 563 enforce(data.length > Header.sizeof); 564 Header* header = cast(Header*) data.ptr; 565 enforce(header.bfType == "BM", "Invalid signature"); 566 enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)" 567 .format(header.bfSize, data.length)); 568 enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof); 569 570 auto w = header.bcWidth; 571 auto h = header.bcHeight; 572 enforce(header.bcPlanes==1, "Multiplane BMPs not supported"); 573 574 enforce(header.bcBitCount == bitmapBitCount!COLOR, 575 "Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image" 576 .format(header.bcBitCount, bitmapBitCount!COLOR)); 577 578 auto pixelData = data[header.bfOffBits..$]; 579 auto pixelStride = bitmapPixelStride!COLOR(w); 580 size_t pos = 0; 581 582 if (h < 0) 583 h = -h; 584 else 585 { 586 pos = pixelStride*(h-1); 587 pixelStride = -pixelStride; 588 } 589 590 target.size(w, h); 591 foreach (y; 0..h) 592 { 593 target.scanline(y)[] = (cast(COLOR*)(pixelData.ptr+pos))[0..w]; 594 pos += pixelStride; 595 } 596 597 return target; 598 } 599 /// ditto 600 auto parseBMP(COLOR)(const(void)[] data) 601 { 602 Image!COLOR target; 603 return data.parseBMP(target); 604 } 605 606 unittest 607 { 608 alias parseBMP!BGR parseBMP24; 609 } 610 611 /// Creates a Windows bitmap (.bmp) file. 612 ubyte[] toBMP(SRC)(auto ref SRC src) 613 if (isView!SRC) 614 { 615 alias COLOR = ViewColor!SRC; 616 617 static if (COLOR.sizeof > 3) 618 alias BitmapHeader!4 Header; 619 else 620 alias BitmapHeader!3 Header; 621 622 auto pixelStride = bitmapPixelStride!COLOR(src.w); 623 auto bitmapDataSize = src.h * pixelStride; 624 ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize]; 625 auto header = cast(Header*)data.ptr; 626 *header = Header.init; 627 header.bfSize = to!uint(data.length); 628 header.bfOffBits = Header.sizeof; 629 header.bcWidth = src.w; 630 header.bcHeight = -src.h; 631 header.bcPlanes = 1; 632 header.biSizeImage = bitmapDataSize; 633 header.bcBitCount = bitmapBitCount!COLOR; 634 635 static if (header.VERSION >= 4) 636 { 637 header.biCompression = BI_BITFIELDS; 638 639 COLOR c; 640 foreach (i, f; c.tupleof) 641 { 642 enum CHAN = c.tupleof[i].stringof[2..$]; 643 enum MASK = (cast(uint)typeof(c.tupleof[i]).max) << (c.tupleof[i].offsetof*8); 644 static if (CHAN=="r") 645 header.bV4RedMask |= MASK; 646 else 647 static if (CHAN=="g") 648 header.bV4GreenMask |= MASK; 649 else 650 static if (CHAN=="b") 651 header.bV4BlueMask |= MASK; 652 else 653 static if (CHAN=="a") 654 header.bV4AlphaMask |= MASK; 655 } 656 } 657 658 auto pixelData = data[header.bfOffBits..$]; 659 auto ptr = pixelData.ptr; 660 size_t pos = 0; 661 662 foreach (y; 0..src.h) 663 { 664 src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]); 665 ptr += pixelStride; 666 } 667 668 return data; 669 } 670 671 unittest 672 { 673 Image!BGR output; 674 onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output); 675 } 676 677 // *************************************************************************** 678 679 /// Creates a PNG file. 680 /// Only basic PNG features are supported 681 /// (no filters, interlacing, palettes etc.) 682 ubyte[] toPNG(SRC)(auto ref SRC src) 683 if (isView!SRC) 684 { 685 import std.digest.crc; 686 import std.zlib : compress; 687 import ae.utils.math : swapBytes; // TODO: proper endianness support 688 689 enum : ulong { SIGNATURE = 0x0a1a0a0d474e5089 } 690 691 struct PNGChunk 692 { 693 char[4] type; 694 const(void)[] data; 695 696 uint crc32() 697 { 698 CRC32 crc; 699 crc.put(cast(ubyte[])(type[])); 700 crc.put(cast(ubyte[])data); 701 ubyte[4] hash = crc.finish(); 702 return *cast(uint*)hash.ptr; 703 } 704 705 this(string type, const(void)[] data) 706 { 707 this.type[] = type[]; 708 this.data = data; 709 } 710 } 711 712 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 } 713 enum PNGCompressionMethod : ubyte { DEFLATE } 714 enum PNGFilterMethod : ubyte { ADAPTIVE } 715 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 } 716 717 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH } 718 719 align(1) 720 struct PNGHeader 721 { 722 align(1): 723 uint width, height; 724 ubyte colourDepth; 725 PNGColourType colourType; 726 PNGCompressionMethod compressionMethod; 727 PNGFilterMethod filterMethod; 728 PNGInterlaceMethod interlaceMethod; 729 static assert(PNGHeader.sizeof == 13); 730 } 731 732 alias COLOR = ViewColor!SRC; 733 static if (!is(COLOR == struct)) 734 enum COLOUR_TYPE = PNGColourType.G; 735 else 736 static if (structFields!COLOR == ["l"]) 737 enum COLOUR_TYPE = PNGColourType.G; 738 else 739 static if (structFields!COLOR == ["r","g","b"]) 740 enum COLOUR_TYPE = PNGColourType.RGB; 741 else 742 static if (structFields!COLOR == ["l","a"]) 743 enum COLOUR_TYPE = PNGColourType.GA; 744 else 745 static if (structFields!COLOR == ["r","g","b","a"]) 746 enum COLOUR_TYPE = PNGColourType.RGBA; 747 else 748 static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof); 749 750 PNGChunk[] chunks; 751 PNGHeader header = { 752 width : swapBytes(src.w), 753 height : swapBytes(src.h), 754 colourDepth : ChannelType!COLOR.sizeof * 8, 755 colourType : COLOUR_TYPE, 756 compressionMethod : PNGCompressionMethod.DEFLATE, 757 filterMethod : PNGFilterMethod.ADAPTIVE, 758 interlaceMethod : PNGInterlaceMethod.NONE, 759 }; 760 chunks ~= PNGChunk("IHDR", cast(void[])[header]); 761 uint idatStride = to!uint(src.w * COLOR.sizeof+1); 762 ubyte[] idatData = new ubyte[src.h * idatStride]; 763 for (uint y=0; y<src.h; y++) 764 { 765 idatData[y*idatStride] = PNGFilterAdaptive.NONE; 766 auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride]; 767 src.copyScanline(y, rowPixels); 768 769 static if (ChannelType!COLOR.sizeof > 1) 770 foreach (ref p; cast(ChannelType!COLOR[])rowPixels) 771 p = swapBytes(p); 772 } 773 chunks ~= PNGChunk("IDAT", compress(idatData, 5)); 774 chunks ~= PNGChunk("IEND", null); 775 776 uint totalSize = 8; 777 foreach (chunk; chunks) 778 totalSize += 8 + chunk.data.length + 4; 779 ubyte[] data = new ubyte[totalSize]; 780 781 *cast(ulong*)data.ptr = SIGNATURE; 782 uint pos = 8; 783 foreach(chunk;chunks) 784 { 785 uint i = pos; 786 uint chunkLength = to!uint(chunk.data.length); 787 pos += 12 + chunkLength; 788 *cast(uint*)&data[i] = swapBytes(chunkLength); 789 (cast(char[])data[i+4 .. i+8])[] = chunk.type[]; 790 data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[]; 791 *cast(uint*)&data[i+8+chunk.data.length] = swapBytes(chunk.crc32()); 792 assert(pos == i+12+chunk.data.length); 793 } 794 795 return data; 796 } 797 798 unittest 799 { 800 onePixel(RGB(1,2,3)).toPNG(); 801 onePixel(5).toPNG(); 802 }