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 auto data = "P6\n2\n2\n255\n" ~ 400 x"000000 FFF000" ~ 401 x"000FFF FFFFFF"; 402 auto i = data.parsePBM!RGB(); 403 assert(i[0, 0] == RGB.fromHex("000000")); 404 assert(i[0, 1] == RGB.fromHex("000FFF")); 405 } 406 407 unittest 408 { 409 auto data = "P5\n2\n2\n255\n" ~ 410 x"00 55" ~ 411 x"AA FF"; 412 auto i = data.parsePBM!L8(); 413 assert(i[0, 0] == L8(0x00)); 414 assert(i[0, 1] == L8(0xAA)); 415 } 416 417 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 418 ubyte[] toPBM(SRC)(auto ref SRC src) 419 if (isView!SRC) 420 { 421 alias COLOR = ViewColor!SRC; 422 423 auto length = src.w * src.h; 424 ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n" 425 .format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max); 426 ubyte[] data = new ubyte[header.length + length * COLOR.sizeof]; 427 428 data[0..header.length] = header; 429 src.copyPixels(cast(COLOR[])data[header.length..$]); 430 431 static if (ChannelType!COLOR.sizeof > 1) 432 foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$]) 433 p = swapBytes(p); // TODO: proper endianness support 434 435 return data; 436 } 437 438 unittest 439 { 440 assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ x"01 02 03"); 441 assert(onePixel(L8 (1) ).toPBM == "P5\n1 1 255\n" ~ x"01" ); 442 } 443 444 // *************************************************************************** 445 446 /// Loads a raw COLOR[] into an image of the indicated size. 447 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h, 448 auto ref TARGET target) 449 if (isWritableView!TARGET 450 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET)) 451 { 452 alias COLOR = ViewColor!TARGET; 453 454 auto pixels = cast(COLOR[])input; 455 enforce(pixels.length == w*h, "Dimension / filesize mismatch"); 456 target.size(w, h); 457 target.pixels[] = pixels; 458 return target; 459 } 460 461 /// ditto 462 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h) 463 { 464 alias COLOR = GetInputColor!(C, INPUT); 465 Image!COLOR target; 466 return fromPixels!COLOR(input, w, h, target); 467 } 468 469 unittest 470 { 471 Image!L8 i; 472 i = x"42".fromPixels!L8(1, 1); 473 i = x"42".fromPixels!L8(1, 1, i); 474 assert(i[0, 0].l == 0x42); 475 i = (cast(L8[])x"42").fromPixels(1, 1); 476 i = (cast(L8[])x"42").fromPixels(1, 1, i); 477 } 478 479 // *************************************************************************** 480 481 static import ae.utils.graphics.bitmap; 482 483 template bitmapBitCount(COLOR) 484 { 485 static if (is(COLOR == BGR)) 486 enum bitmapBitCount = 24; 487 else 488 static if (is(COLOR == BGRX) || is(COLOR == BGRA)) 489 enum bitmapBitCount = 32; 490 else 491 static if (is(COLOR == L8)) 492 enum bitmapBitCount = 8; 493 else 494 static assert(false, "Unsupported BMP color type: " ~ COLOR.stringof); 495 } 496 497 @property int bitmapPixelStride(COLOR)(int w) 498 { 499 int pixelStride = w * cast(uint)COLOR.sizeof; 500 pixelStride = (pixelStride+3) & ~3; 501 return pixelStride; 502 } 503 504 /// Parses a Windows bitmap (.bmp) file. 505 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target) 506 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 507 { 508 alias COLOR = ViewColor!TARGET; 509 510 import ae.utils.graphics.bitmap; 511 alias BitmapHeader!3 Header; 512 enforce(data.length > Header.sizeof); 513 Header* header = cast(Header*) data.ptr; 514 enforce(header.bfType == "BM", "Invalid signature"); 515 enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)" 516 .format(header.bfSize, data.length)); 517 enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof); 518 519 auto w = header.bcWidth; 520 auto h = header.bcHeight; 521 enforce(header.bcPlanes==1, "Multiplane BMPs not supported"); 522 523 enforce(header.bcBitCount == bitmapBitCount!COLOR, 524 "Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image" 525 .format(header.bcBitCount, bitmapBitCount!COLOR)); 526 527 auto pixelData = data[header.bfOffBits..$]; 528 auto pixelStride = bitmapPixelStride!COLOR(w); 529 size_t pos = 0; 530 531 if (h < 0) 532 h = -h; 533 else 534 { 535 pos = pixelStride*(h-1); 536 pixelStride = -pixelStride; 537 } 538 539 target.size(w, h); 540 foreach (y; 0..h) 541 { 542 target.scanline(y)[] = (cast(COLOR*)(pixelData.ptr+pos))[0..w]; 543 pos += pixelStride; 544 } 545 546 return target; 547 } 548 /// ditto 549 auto parseBMP(COLOR)(const(void)[] data) 550 { 551 Image!COLOR target; 552 return data.parseBMP(target); 553 } 554 555 unittest 556 { 557 alias parseBMP!BGR parseBMP24; 558 } 559 560 /// Creates a Windows bitmap (.bmp) file. 561 ubyte[] toBMP(SRC)(auto ref SRC src) 562 if (isView!SRC) 563 { 564 alias COLOR = ViewColor!SRC; 565 566 import ae.utils.graphics.bitmap; 567 static if (COLOR.sizeof > 3) 568 alias BitmapHeader!4 Header; 569 else 570 alias BitmapHeader!3 Header; 571 572 auto pixelStride = bitmapPixelStride!COLOR(src.w); 573 auto bitmapDataSize = src.h * pixelStride; 574 ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize]; 575 auto header = cast(Header*)data.ptr; 576 *header = Header.init; 577 header.bfSize = to!uint(data.length); 578 header.bfOffBits = Header.sizeof; 579 header.bcWidth = src.w; 580 header.bcHeight = -src.h; 581 header.bcPlanes = 1; 582 header.biSizeImage = bitmapDataSize; 583 header.bcBitCount = bitmapBitCount!COLOR; 584 585 static if (header.VERSION >= 4) 586 { 587 header.biCompression = BI_BITFIELDS; 588 589 COLOR c; 590 foreach (i, f; c.tupleof) 591 { 592 enum CHAN = c.tupleof[i].stringof[2..$]; 593 enum MASK = (cast(uint)typeof(c.tupleof[i]).max) << (c.tupleof[i].offsetof*8); 594 static if (CHAN=="r") 595 header.bV4RedMask |= MASK; 596 else 597 static if (CHAN=="g") 598 header.bV4GreenMask |= MASK; 599 else 600 static if (CHAN=="b") 601 header.bV4BlueMask |= MASK; 602 else 603 static if (CHAN=="a") 604 header.bV4AlphaMask |= MASK; 605 } 606 } 607 608 auto pixelData = data[header.bfOffBits..$]; 609 auto ptr = pixelData.ptr; 610 size_t pos = 0; 611 612 foreach (y; 0..src.h) 613 { 614 src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]); 615 ptr += pixelStride; 616 } 617 618 return data; 619 } 620 621 unittest 622 { 623 Image!BGR output; 624 onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output); 625 } 626 627 // *************************************************************************** 628 629 private // https://issues.dlang.org/show_bug.cgi?id=16563 630 { 631 struct PNGChunk 632 { 633 char[4] type; 634 const(void)[] data; 635 636 uint crc32() 637 { 638 import std.digest.crc; 639 CRC32 crc; 640 crc.put(cast(ubyte[])(type[])); 641 crc.put(cast(ubyte[])data); 642 ubyte[4] hash = crc.finish(); 643 return *cast(uint*)hash.ptr; 644 } 645 646 this(string type, const(void)[] data) 647 { 648 this.type[] = type[]; 649 this.data = data; 650 } 651 } 652 653 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 } 654 enum PNGCompressionMethod : ubyte { DEFLATE } 655 enum PNGFilterMethod : ubyte { ADAPTIVE } 656 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 } 657 658 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH } 659 660 align(1) 661 struct PNGHeader 662 { 663 align(1): 664 uint width, height; 665 ubyte colourDepth; 666 PNGColourType colourType; 667 PNGCompressionMethod compressionMethod; 668 PNGFilterMethod filterMethod; 669 PNGInterlaceMethod interlaceMethod; 670 static assert(PNGHeader.sizeof == 13); 671 } 672 } 673 674 /// Creates a PNG file. 675 /// Only basic PNG features are supported 676 /// (no filters, interlacing, palettes etc.) 677 ubyte[] toPNG(SRC)(auto ref SRC src) 678 if (isView!SRC) 679 { 680 import std.zlib : compress; 681 import ae.utils.math : swapBytes; // TODO: proper endianness support 682 683 enum : ulong { SIGNATURE = 0x0a1a0a0d474e5089 } 684 685 alias COLOR = ViewColor!SRC; 686 static if (!is(COLOR == struct)) 687 enum COLOUR_TYPE = PNGColourType.G; 688 else 689 static if (structFields!COLOR == ["l"]) 690 enum COLOUR_TYPE = PNGColourType.G; 691 else 692 static if (structFields!COLOR == ["r","g","b"]) 693 enum COLOUR_TYPE = PNGColourType.RGB; 694 else 695 static if (structFields!COLOR == ["l","a"]) 696 enum COLOUR_TYPE = PNGColourType.GA; 697 else 698 static if (structFields!COLOR == ["r","g","b","a"]) 699 enum COLOUR_TYPE = PNGColourType.RGBA; 700 else 701 static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof); 702 703 PNGChunk[] chunks; 704 PNGHeader header = { 705 width : swapBytes(src.w), 706 height : swapBytes(src.h), 707 colourDepth : ChannelType!COLOR.sizeof * 8, 708 colourType : COLOUR_TYPE, 709 compressionMethod : PNGCompressionMethod.DEFLATE, 710 filterMethod : PNGFilterMethod.ADAPTIVE, 711 interlaceMethod : PNGInterlaceMethod.NONE, 712 }; 713 chunks ~= PNGChunk("IHDR", cast(void[])[header]); 714 uint idatStride = to!uint(src.w * COLOR.sizeof+1); 715 ubyte[] idatData = new ubyte[src.h * idatStride]; 716 for (uint y=0; y<src.h; y++) 717 { 718 idatData[y*idatStride] = PNGFilterAdaptive.NONE; 719 auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride]; 720 src.copyScanline(y, rowPixels); 721 722 static if (ChannelType!COLOR.sizeof > 1) 723 foreach (ref p; cast(ChannelType!COLOR[])rowPixels) 724 p = swapBytes(p); 725 } 726 chunks ~= PNGChunk("IDAT", compress(idatData, 5)); 727 chunks ~= PNGChunk("IEND", null); 728 729 uint totalSize = 8; 730 foreach (chunk; chunks) 731 totalSize += 8 + chunk.data.length + 4; 732 ubyte[] data = new ubyte[totalSize]; 733 734 *cast(ulong*)data.ptr = SIGNATURE; 735 uint pos = 8; 736 foreach(chunk;chunks) 737 { 738 uint i = pos; 739 uint chunkLength = to!uint(chunk.data.length); 740 pos += 12 + chunkLength; 741 *cast(uint*)&data[i] = swapBytes(chunkLength); 742 (cast(char[])data[i+4 .. i+8])[] = chunk.type[]; 743 data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[]; 744 *cast(uint*)&data[i+8+chunk.data.length] = swapBytes(chunk.crc32()); 745 assert(pos == i+12+chunk.data.length); 746 } 747 748 return data; 749 } 750 751 unittest 752 { 753 onePixel(RGB(1,2,3)).toPNG(); 754 onePixel(5).toPNG(); 755 }