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