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 inout(COLOR)[] scanline(int y) inout 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 inout(COLOR)[] scanline(int y) inout 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 alias COLOR = ViewColor!SRC; 222 223 assert(hr.w % HRX == 0 && hr.h % HRY == 0, "Size mismatch"); 224 225 lr.size(hr.w / HRX, hr.h / HRY); 226 227 foreach (y; 0..lr.h) 228 foreach (x; 0..lr.w) 229 { 230 static if (HRX*HRY <= 0x100) 231 enum EXPAND_BYTES = 1; 232 else 233 static if (HRX*HRY <= 0x10000) 234 enum EXPAND_BYTES = 2; 235 else 236 static assert(0); 237 static if (is(typeof(COLOR.init.a))) // downscale with alpha 238 { 239 version (none) // TODO: broken 240 { 241 ExpandChannelType!(COLOR, EXPAND_BYTES+COLOR.init.a.sizeof) sum; 242 ExpandChannelType!(typeof(COLOR.init.a), EXPAND_BYTES) alphaSum; 243 auto start = y*HRY*hr.stride + x*HRX; 244 foreach (j; 0..HRY) 245 { 246 foreach (p; hr.pixels[start..start+HRX]) 247 { 248 foreach (i, f; p.tupleof) 249 static if (p.tupleof[i].stringof != "p.a") 250 { 251 enum FIELD = p.tupleof[i].stringof[2..$]; 252 mixin("sum."~FIELD~" += cast(typeof(sum."~FIELD~"))p."~FIELD~" * p.a;"); 253 } 254 alphaSum += p.a; 255 } 256 start += hr.stride; 257 } 258 if (alphaSum) 259 { 260 auto result = cast(COLOR)(sum / alphaSum); 261 result.a = cast(typeof(result.a))(alphaSum / (HRX*HRY)); 262 lr[x, y] = result; 263 } 264 else 265 { 266 static assert(COLOR.init.a == 0); 267 lr[x, y] = COLOR.init; 268 } 269 } 270 else 271 static assert(false, "Downscaling with alpha is not implemented"); 272 } 273 else 274 { 275 ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum; 276 auto x0 = x*HRX; 277 auto x1 = x0+HRX; 278 foreach (j; y*HRY..(y+1)*HRY) 279 foreach (p; hr.scanline(j)[x0..x1]) 280 sum += p; 281 lr[x, y] = cast(ViewColor!SRC)(sum / (HRX*HRY)); 282 } 283 } 284 285 return target; 286 } 287 288 auto downscale(SRC)(auto ref SRC src) 289 if (isView!SRC) 290 { 291 ViewImage!SRC target; 292 return src.downscale(target); 293 } 294 } 295 296 unittest 297 { 298 onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)(); 299 // onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)(); 300 301 Image!ubyte i; 302 i.size(4, 1); 303 i.pixels[] = [1, 3, 5, 7]; 304 auto d = i.downscale!(2, 1); 305 assert(d.pixels == [2, 6]); 306 } 307 308 // *************************************************************************** 309 310 /// Downscaling copy (averages colors in source per one pixel in target). 311 auto downscaleTo(SRC, TARGET)(auto ref SRC src, auto ref TARGET target) 312 if (isDirectView!SRC && isWritableView!TARGET) 313 { 314 alias lr = target; 315 alias hr = src; 316 alias COLOR = ViewColor!SRC; 317 318 void impl(uint EXPAND_BYTES)() 319 { 320 foreach (y; 0..lr.h) 321 foreach (x; 0..lr.w) 322 { 323 static if (is(typeof(COLOR.init.a))) // downscale with alpha 324 static assert(false, "Downscaling with alpha is not implemented"); 325 else 326 { 327 ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum; 328 auto x0 = x * hr.w / lr.w; 329 auto x1 = (x+1) * hr.w / lr.w; 330 auto y0 = y * hr.h / lr.h; 331 auto y1 = (y+1) * hr.h / lr.h; 332 333 // When upscaling (across one or two axes), 334 // fall back to nearest neighbor 335 if (x0 == x1) x1++; 336 if (y0 == y1) y1++; 337 338 foreach (j; y0 .. y1) 339 foreach (p; hr.scanline(j)[x0 .. x1]) 340 sum += p; 341 auto area = (x1 - x0) * (y1 - y0); 342 auto avg = sum / area; 343 lr[x, y] = cast(ViewColor!SRC)(avg); 344 } 345 } 346 } 347 348 auto perPixelArea = (hr.w / lr.w + 1) * (hr.h / lr.h + 1); 349 350 if (perPixelArea <= 0x100) 351 impl!1(); 352 else 353 if (perPixelArea <= 0x10000) 354 impl!2(); 355 else 356 if (perPixelArea <= 0x1000000) 357 impl!3(); 358 else 359 assert(false, "Downscaling too much"); 360 361 return target; 362 } 363 364 /// Downscales an image to a certain size. 365 auto downscaleTo(SRC)(auto ref SRC src, int w, int h) 366 if (isView!SRC) 367 { 368 ViewImage!SRC target; 369 target.size(w, h); 370 return src.downscaleTo(target); 371 } 372 373 unittest 374 { 375 onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2); 376 // onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2); 377 378 Image!ubyte i; 379 i.size(6, 1); 380 i.pixels[] = [1, 2, 3, 4, 5, 6]; 381 assert(i.downscaleTo(6, 1).pixels == [1, 2, 3, 4, 5, 6]); 382 assert(i.downscaleTo(3, 1).pixels == [1, 3, 5]); 383 assert(i.downscaleTo(2, 1).pixels == [2, 5]); 384 assert(i.downscaleTo(1, 1).pixels == [3]); 385 386 i.size(3, 3); 387 i.pixels[] = [ 388 1, 2, 3, 389 4, 5, 6, 390 7, 8, 9]; 391 assert(i.downscaleTo(2, 2).pixels == [1, 2, 5, 7]); 392 393 i.size(1, 1); 394 i.pixels = [1]; 395 assert(i.downscaleTo(2, 2).pixels == [1, 1, 1, 1]); 396 } 397 398 // *************************************************************************** 399 400 /// Copy the indicated row of src to a COLOR buffer. 401 void copyScanline(SRC, COLOR)(auto ref SRC src, int y, COLOR[] dst) 402 if (isView!SRC && is(COLOR == ViewColor!SRC)) 403 { 404 static if (isDirectView!SRC) 405 dst[] = src.scanline(y)[]; 406 else 407 { 408 assert(src.w == dst.length); 409 foreach (x; 0..src.w) 410 dst[x] = src[x, y]; 411 } 412 } 413 414 /// Copy a view's pixels (top-to-bottom) to a COLOR buffer. 415 void copyPixels(SRC, COLOR)(auto ref SRC src, COLOR[] dst) 416 if (isView!SRC && is(COLOR == ViewColor!SRC)) 417 { 418 assert(dst.length == src.w * src.h); 419 foreach (y; 0..src.h) 420 src.copyScanline(y, dst[y*src.w..(y+1)*src.w]); 421 } 422 423 // *************************************************************************** 424 425 import std.traits; 426 427 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433 428 429 struct InputColor {} 430 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR); 431 432 struct TargetColor {} 433 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET); 434 435 // *************************************************************************** 436 437 import ae.utils.graphics.color; 438 import ae.utils.meta : structFields; 439 440 private string[] readPBMHeader(ref const(ubyte)[] data) 441 { 442 import std.ascii; 443 444 string[] fields; 445 uint wordStart = 0; 446 uint p; 447 for (p=1; p<data.length && fields.length<4; p++) 448 if (!isWhite(data[p-1]) && isWhite(data[p])) 449 fields ~= cast(string)data[wordStart..p]; 450 else 451 if (isWhite(data[p-1]) && !isWhite(data[p])) 452 wordStart = p; 453 data = data[p..$]; 454 enforce(fields.length==4, "Header too short"); 455 enforce(fields[0].length==2 && fields[0][0]=='P', "Invalid signature"); 456 return fields; 457 } 458 459 private template PBMSignature(COLOR) 460 { 461 static if (structFields!COLOR == ["l"]) 462 enum PBMSignature = "P5"; 463 else 464 static if (structFields!COLOR == ["r", "g", "b"]) 465 enum PBMSignature = "P6"; 466 else 467 static assert(false, "Unsupported PBM color: " ~ 468 __traits(allMembers, COLOR.Fields).stringof); 469 } 470 471 /// Parses a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 472 auto parsePBM(C = TargetColor, TARGET)(const(void)[] vdata, auto ref TARGET target) 473 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 474 { 475 alias COLOR = ViewColor!TARGET; 476 477 auto data = cast(const(ubyte)[])vdata; 478 string[] fields = readPBMHeader(data); 479 enforce(fields[0]==PBMSignature!COLOR, "Invalid signature"); 480 enforce(to!uint(fields[3]) == COLOR.tupleof[0].max, "Channel depth mismatch"); 481 482 target.size(to!uint(fields[1]), to!uint(fields[2])); 483 enforce(data.length / COLOR.sizeof == target.w * target.h, 484 "Dimension / filesize mismatch"); 485 target.pixels[] = cast(COLOR[])data; 486 487 static if (COLOR.tupleof[0].sizeof > 1) 488 foreach (ref pixel; pixels) 489 pixel = COLOR.op!q{swapBytes(a)}(pixel); // TODO: proper endianness support 490 491 return target; 492 } 493 /// ditto 494 auto parsePBM(COLOR)(const(void)[] vdata) 495 { 496 Image!COLOR target; 497 return vdata.parsePBM(target); 498 } 499 500 unittest 501 { 502 import std.conv : hexString; 503 auto data = "P6\n2\n2\n255\n" ~ 504 hexString!"000000 FFF000" ~ 505 hexString!"000FFF FFFFFF"; 506 auto i = data.parsePBM!RGB(); 507 assert(i[0, 0] == RGB.fromHex("000000")); 508 assert(i[0, 1] == RGB.fromHex("000FFF")); 509 } 510 511 unittest 512 { 513 import std.conv : hexString; 514 auto data = "P5\n2\n2\n255\n" ~ 515 hexString!"00 55" ~ 516 hexString!"AA FF"; 517 auto i = data.parsePBM!L8(); 518 assert(i[0, 0] == L8(0x00)); 519 assert(i[0, 1] == L8(0xAA)); 520 } 521 522 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 523 ubyte[] toPBM(SRC)(auto ref SRC src) 524 if (isView!SRC) 525 { 526 alias COLOR = ViewColor!SRC; 527 528 auto length = src.w * src.h; 529 ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n" 530 .format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max); 531 ubyte[] data = new ubyte[header.length + length * COLOR.sizeof]; 532 533 data[0..header.length] = header; 534 src.copyPixels(cast(COLOR[])data[header.length..$]); 535 536 static if (ChannelType!COLOR.sizeof > 1) 537 foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$]) 538 p = swapBytes(p); // TODO: proper endianness support 539 540 return data; 541 } 542 543 unittest 544 { 545 import std.conv : hexString; 546 assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ hexString!"01 02 03"); 547 assert(onePixel(L8 (1) ).toPBM == "P5\n1 1 255\n" ~ hexString!"01" ); 548 } 549 550 // *************************************************************************** 551 552 /// Loads a raw COLOR[] into an image of the indicated size. 553 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h, 554 auto ref TARGET target) 555 if (isWritableView!TARGET 556 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET)) 557 { 558 alias COLOR = ViewColor!TARGET; 559 560 auto pixels = cast(COLOR[])input; 561 enforce(pixels.length == w*h, "Dimension / filesize mismatch"); 562 target.size(w, h); 563 target.pixels[] = pixels; 564 return target; 565 } 566 567 /// ditto 568 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h) 569 { 570 alias COLOR = GetInputColor!(C, INPUT); 571 Image!COLOR target; 572 return fromPixels!COLOR(input, w, h, target); 573 } 574 575 unittest 576 { 577 import std.conv : hexString; 578 Image!L8 i; 579 i = hexString!"42".fromPixels!L8(1, 1); 580 i = hexString!"42".fromPixels!L8(1, 1, i); 581 assert(i[0, 0].l == 0x42); 582 i = (cast(L8[])hexString!"42").fromPixels(1, 1); 583 i = (cast(L8[])hexString!"42").fromPixels(1, 1, i); 584 } 585 586 // *************************************************************************** 587 588 static import ae.utils.graphics.bitmap; 589 590 enum bitmapNeedV4Header(COLOR) = !is(COLOR == BGR) && !is(COLOR == BGRX); 591 592 uint[4] bitmapChannelMasks(COLOR)() 593 { 594 uint[4] result; 595 foreach (i, f; COLOR.init.tupleof) 596 { 597 enum channelName = __traits(identifier, COLOR.tupleof[i]); 598 static if (channelName != "x") 599 static assert((COLOR.tupleof[i].offsetof + COLOR.tupleof[i].sizeof) * 8 <= 32, 600 "Color " ~ COLOR.stringof ~ " (channel " ~ channelName ~ ") is too large for BMP"); 601 602 enum MASK = (cast(uint)typeof(COLOR.tupleof[i]).max) << (COLOR.tupleof[i].offsetof*8); 603 static if (channelName == "r") 604 result[0] |= MASK; 605 else 606 static if (channelName == "g") 607 result[1] |= MASK; 608 else 609 static if (channelName == "b") 610 result[2] |= MASK; 611 else 612 static if (channelName == "a") 613 result[3] |= MASK; 614 else 615 static if (channelName == "l") 616 { 617 result[0] |= MASK; 618 result[1] |= MASK; 619 result[2] |= MASK; 620 } 621 else 622 static if (channelName == "x") 623 { 624 } 625 else 626 static assert(false, "Don't know how to encode channelNamenel " ~ channelName); 627 } 628 return result; 629 } 630 631 @property int bitmapPixelStride(COLOR)(int w) 632 { 633 int pixelStride = w * cast(uint)COLOR.sizeof; 634 pixelStride = (pixelStride+3) & ~3; 635 return pixelStride; 636 } 637 638 /// Returns a view representing a BMP file. 639 /// Does not copy pixel data. 640 auto viewBMP(COLOR, V)(V data) 641 if (is(V : const(void)[])) 642 { 643 import ae.utils.graphics.bitmap; 644 alias BitmapHeader!3 Header; 645 enforce(data.length > Header.sizeof, "Not enough data for header"); 646 Header* header = cast(Header*) data.ptr; 647 enforce(header.bfType == "BM", "Invalid signature"); 648 enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)" 649 .format(header.bfSize, data.length)); 650 enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof); 651 652 static struct BMP 653 { 654 int w, h; 655 typeof(data.ptr) pixelData; 656 int pixelStride; 657 658 inout(COLOR)[] scanline(int y) inout // TODO constness 659 { 660 assert(y >= 0 && y < h, "BMP scanline out of bounds"); 661 return (cast(COLOR*)(pixelData + y * pixelStride))[0..w]; 662 } 663 664 mixin DirectView; 665 } 666 BMP bmp; 667 668 bmp.w = header.bcWidth; 669 bmp.h = header.bcHeight; 670 enforce(header.bcPlanes==1, "Multiplane BMPs not supported"); 671 672 enforce(header.bcBitCount == COLOR.sizeof * 8, 673 "Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image" 674 .format(header.bcBitCount, COLOR.sizeof * 8)); 675 676 static if (bitmapNeedV4Header!COLOR) 677 enforce(header.VERSION >= 4, "Need a V4+ header to load a %s image".format(COLOR.stringof)); 678 if (header.VERSION >= 4) 679 { 680 enforce(data.length > BitmapHeader!4.sizeof, "Not enough data for header"); 681 auto header4 = cast(BitmapHeader!4*) data.ptr; 682 uint[4] fileMasks = [ 683 header4.bV4RedMask, 684 header4.bV4GreenMask, 685 header4.bV4BlueMask, 686 header4.bV4AlphaMask]; 687 static immutable expectedMasks = bitmapChannelMasks!COLOR(); 688 enforce(fileMasks == expectedMasks, 689 "Channel format mask mismatch.\nExpected: [%(%32b, %)]\nIn file : [%(%32b, %)]" 690 .format(expectedMasks, fileMasks)); 691 } 692 693 bmp.pixelData = data[header.bfOffBits..$].ptr; 694 bmp.pixelStride = bitmapPixelStride!COLOR(bmp.w); 695 696 if (bmp.h < 0) 697 bmp.h = -bmp.h; 698 else 699 { 700 bmp.pixelData += bmp.pixelStride * (bmp.h - 1); 701 bmp.pixelStride = -bmp.pixelStride; 702 } 703 704 return bmp; 705 } 706 707 /// Parses a Windows bitmap (.bmp) file. 708 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target) 709 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 710 { 711 alias COLOR = ViewColor!TARGET; 712 viewBMP!COLOR(data).copy(target); 713 return target; 714 } 715 /// ditto 716 auto parseBMP(COLOR)(const(void)[] data) 717 { 718 Image!COLOR target; 719 return data.parseBMP(target); 720 } 721 722 unittest 723 { 724 alias parseBMP!BGR parseBMP24; 725 } 726 727 /// Creates a Windows bitmap (.bmp) file. 728 ubyte[] toBMP(SRC)(auto ref SRC src) 729 if (isView!SRC) 730 { 731 alias COLOR = ViewColor!SRC; 732 733 import ae.utils.graphics.bitmap; 734 static if (bitmapNeedV4Header!COLOR) 735 alias BitmapHeader!4 Header; 736 else 737 alias BitmapHeader!3 Header; 738 739 auto pixelStride = bitmapPixelStride!COLOR(src.w); 740 auto bitmapDataSize = src.h * pixelStride; 741 ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize]; 742 auto header = cast(Header*)data.ptr; 743 *header = Header.init; 744 header.bfSize = to!uint(data.length); 745 header.bfOffBits = Header.sizeof; 746 header.bcWidth = src.w; 747 header.bcHeight = -src.h; 748 header.bcPlanes = 1; 749 header.biSizeImage = bitmapDataSize; 750 header.bcBitCount = COLOR.sizeof * 8; 751 752 static if (header.VERSION >= 4) 753 { 754 header.biCompression = BI_BITFIELDS; 755 static immutable masks = bitmapChannelMasks!COLOR(); 756 header.bV4RedMask = masks[0]; 757 header.bV4GreenMask = masks[1]; 758 header.bV4BlueMask = masks[2]; 759 header.bV4AlphaMask = masks[3]; 760 } 761 762 auto pixelData = data[header.bfOffBits..$]; 763 auto ptr = pixelData.ptr; 764 size_t pos = 0; 765 766 foreach (y; 0..src.h) 767 { 768 src.copyScanline(y, (cast(COLOR*)ptr)[0..src.w]); 769 ptr += pixelStride; 770 } 771 772 return data; 773 } 774 775 unittest 776 { 777 Image!BGR output; 778 onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output); 779 } 780 781 // *************************************************************************** 782 783 enum ulong PNGSignature = 0x0a1a0a0d474e5089; 784 785 struct PNGChunk 786 { 787 char[4] type; 788 const(void)[] data; 789 790 uint crc32() 791 { 792 import std.digest.crc; 793 CRC32 crc; 794 crc.put(cast(ubyte[])(type[])); 795 crc.put(cast(ubyte[])data); 796 ubyte[4] hash = crc.finish(); 797 return *cast(uint*)hash.ptr; 798 } 799 800 this(string type, const(void)[] data) 801 { 802 this.type[] = type[]; 803 this.data = data; 804 } 805 } 806 807 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 } 808 enum PNGCompressionMethod : ubyte { DEFLATE } 809 enum PNGFilterMethod : ubyte { ADAPTIVE } 810 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 } 811 812 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH } 813 814 align(1) 815 struct PNGHeader 816 { 817 align(1): 818 ubyte[4] width, height; 819 ubyte colourDepth; 820 PNGColourType colourType; 821 PNGCompressionMethod compressionMethod; 822 PNGFilterMethod filterMethod; 823 PNGInterlaceMethod interlaceMethod; 824 static assert(PNGHeader.sizeof == 13); 825 } 826 827 /// Creates a PNG file. 828 /// Only basic PNG features are supported 829 /// (no filters, interlacing, palettes etc.) 830 ubyte[] toPNG(SRC)(auto ref SRC src, int compressionLevel = 5) 831 if (isView!SRC) 832 { 833 import std.zlib : compress; 834 import std.bitmanip : nativeToBigEndian, swapEndian; 835 836 alias COLOR = ViewColor!SRC; 837 static if (!is(COLOR == struct)) 838 enum COLOUR_TYPE = PNGColourType.G; 839 else 840 static if (structFields!COLOR == ["l"]) 841 enum COLOUR_TYPE = PNGColourType.G; 842 else 843 static if (structFields!COLOR == ["r","g","b"]) 844 enum COLOUR_TYPE = PNGColourType.RGB; 845 else 846 static if (structFields!COLOR == ["l","a"]) 847 enum COLOUR_TYPE = PNGColourType.GA; 848 else 849 static if (structFields!COLOR == ["r","g","b","a"]) 850 enum COLOUR_TYPE = PNGColourType.RGBA; 851 else 852 static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof); 853 854 PNGChunk[] chunks; 855 PNGHeader header = { 856 width : nativeToBigEndian(src.w), 857 height : nativeToBigEndian(src.h), 858 colourDepth : ChannelType!COLOR.sizeof * 8, 859 colourType : COLOUR_TYPE, 860 compressionMethod : PNGCompressionMethod.DEFLATE, 861 filterMethod : PNGFilterMethod.ADAPTIVE, 862 interlaceMethod : PNGInterlaceMethod.NONE, 863 }; 864 chunks ~= PNGChunk("IHDR", cast(void[])[header]); 865 uint idatStride = to!uint(src.w * COLOR.sizeof+1); 866 ubyte[] idatData = new ubyte[src.h * idatStride]; 867 for (uint y=0; y<src.h; y++) 868 { 869 idatData[y*idatStride] = PNGFilterAdaptive.NONE; 870 auto rowPixels = cast(COLOR[])idatData[y*idatStride+1..(y+1)*idatStride]; 871 src.copyScanline(y, rowPixels); 872 873 version (LittleEndian) 874 static if (ChannelType!COLOR.sizeof > 1) 875 foreach (ref p; cast(ChannelType!COLOR[])rowPixels) 876 p = swapEndian(p); 877 } 878 chunks ~= PNGChunk("IDAT", compress(idatData, compressionLevel)); 879 chunks ~= PNGChunk("IEND", null); 880 881 return makePNG(chunks); 882 } 883 884 ubyte[] makePNG(PNGChunk[] chunks) 885 { 886 import std.bitmanip : nativeToBigEndian; 887 888 uint totalSize = 8; 889 foreach (chunk; chunks) 890 totalSize += 8 + chunk.data.length + 4; 891 ubyte[] data = new ubyte[totalSize]; 892 893 *cast(ulong*)data.ptr = PNGSignature; 894 uint pos = 8; 895 foreach(chunk;chunks) 896 { 897 uint i = pos; 898 uint chunkLength = to!uint(chunk.data.length); 899 pos += 12 + chunkLength; 900 *cast(ubyte[4]*)&data[i] = nativeToBigEndian(chunkLength); 901 (cast(char[])data[i+4 .. i+8])[] = chunk.type[]; 902 data[i+8 .. i+8+chunk.data.length] = (cast(ubyte[])chunk.data)[]; 903 *cast(ubyte[4]*)&data[i+8+chunk.data.length] = nativeToBigEndian(chunk.crc32()); 904 assert(pos == i+12+chunk.data.length); 905 } 906 907 return data; 908 } 909 910 unittest 911 { 912 onePixel(RGB(1,2,3)).toPNG(); 913 onePixel(5).toPNG(); 914 }