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.math : abs; 20 import std.range; 21 import std.string : format; 22 23 public import ae.utils.graphics.view; 24 25 /// Represents a reference to COLOR data 26 /// already existing elsewhere in memory. 27 /// Assumes that pixels are stored row-by-row, 28 /// with a known distance between each row. 29 struct ImageRef(COLOR, StorageType = PlainStorageUnit!COLOR) 30 { 31 xy_t w, h; 32 size_t pitch; /// In bytes, not COLORs 33 StorageType* pixels; 34 35 /// Returns an array for the pixels at row y. 36 inout(StorageType)[] scanline(xy_t y) inout 37 { 38 assert(y>=0 && y<h, "Scanline out-of-bounds"); 39 assert(pitch, "Pitch not set"); 40 auto row = cast(StorageType*)(cast(ubyte*)pixels + y*pitch); 41 return row[0..w]; 42 } 43 44 mixin DirectView; 45 } 46 47 unittest 48 { 49 static assert(isDirectView!(ImageRef!ubyte)); 50 } 51 52 /// Convert a direct view to an ImageRef. 53 /// Assumes that the rows are evenly spaced. 54 ImageRef!(ViewColor!SRC) toRef(SRC)(auto ref SRC src) 55 if (isDirectView!SRC) 56 { 57 return ImageRef!(ViewColor!SRC)(src.w, src.h, 58 src.h > 1 ? cast(ubyte*)src.scanline(1) - cast(ubyte*)src.scanline(0) : src.w, 59 src.scanline(0).ptr); 60 } 61 62 unittest 63 { 64 auto i = Image!ubyte(1, 1); 65 auto r = i.toRef(); 66 assert(r.scanline(0).ptr is i.scanline(0).ptr); 67 } 68 69 // *************************************************************************** 70 71 /// An in-memory image. 72 /// Pixels are stored in a flat array. 73 struct Image(COLOR, StorageType = PlainStorageUnit!COLOR) 74 { 75 xy_t w, h; 76 StorageType[] pixels; 77 78 /// Returns an array for the pixels at row y. 79 inout(StorageType)[] scanline(xy_t y) inout 80 { 81 assert(y>=0 && y<h, "Scanline out-of-bounds"); 82 auto start = w*y; 83 return pixels[start..start+w]; 84 } 85 86 mixin DirectView; 87 88 this(xy_t w, xy_t h) 89 { 90 size(w, h); 91 } 92 93 /// Does not scale image 94 void size(xy_t w, xy_t h) 95 { 96 this.w = w; 97 this.h = h; 98 if (pixels.length < w*h) 99 pixels.length = w*h; 100 } 101 } 102 103 unittest 104 { 105 static assert(isDirectView!(Image!ubyte)); 106 } 107 108 // *************************************************************************** 109 110 // Functions which need a target image to operate on are currenty declared 111 // as two overloads. The code might be simplified if some of these get fixed: 112 // https://d.puremagic.com/issues/show_bug.cgi?id=8074 113 // https://d.puremagic.com/issues/show_bug.cgi?id=12386 114 // https://d.puremagic.com/issues/show_bug.cgi?id=12425 115 // https://d.puremagic.com/issues/show_bug.cgi?id=12426 116 // https://d.puremagic.com/issues/show_bug.cgi?id=12433 117 118 alias ViewImage(V) = Image!(ViewColor!V); 119 120 /// Copy the given view into the specified target. 121 auto copy(SRC, TARGET)(auto ref SRC src, auto ref TARGET target) 122 if (isView!SRC && isWritableView!TARGET) 123 { 124 target.size(src.w, src.h); 125 src.blitTo(target); 126 return target; 127 } 128 129 /// Copy the given view into a newly-allocated image. 130 auto copy(SRC)(auto ref SRC src) 131 if (isView!SRC) 132 { 133 ViewImage!SRC target; 134 return src.copy(target); 135 } 136 137 unittest 138 { 139 auto v = onePixel(0); 140 auto i = v.copy(); 141 v.copy(i); 142 143 auto c = i.crop(0, 0, 1, 1); 144 v.copy(c); 145 } 146 147 alias ElementViewImage(R) = ViewImage!(ElementType!R); 148 149 /// Splice multiple images horizontally. 150 auto hjoin(R, TARGET)(R images, auto ref TARGET target) 151 if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET) 152 { 153 xy_t w, h; 154 foreach (ref image; images) 155 w += image.w, 156 h = max(h, image.h); 157 target.size(w, h); 158 xy_t x; 159 foreach (ref image; images) 160 image.blitTo(target, x, 0), 161 x += image.w; 162 return target; 163 } 164 /// ditto 165 auto hjoin(R)(R images) 166 if (isInputRange!R && isView!(ElementType!R)) 167 { 168 ElementViewImage!R target; 169 return images.hjoin(target); 170 } 171 172 /// Splice multiple images vertically. 173 auto vjoin(R, TARGET)(R images, auto ref TARGET target) 174 if (isInputRange!R && isView!(ElementType!R) && isWritableView!TARGET) 175 { 176 xy_t w, h; 177 foreach (ref image; images) 178 w = max(w, image.w), 179 h += image.h; 180 target.size(w, h); 181 xy_t y; 182 foreach (ref image; images) 183 image.blitTo(target, 0, y), 184 y += image.h; 185 return target; 186 } 187 /// ditto 188 auto vjoin(R)(R images) 189 if (isInputRange!R && isView!(ElementType!R)) 190 { 191 ElementViewImage!R target; 192 return images.vjoin(target); 193 } 194 195 unittest 196 { 197 auto h = 10 198 .iota 199 .retro 200 .map!onePixel 201 .retro 202 .hjoin(); 203 204 foreach (i; 0..10) 205 assert(h[i, 0] == i); 206 207 auto v = 10.iota.map!onePixel.vjoin(); 208 foreach (i; 0..10) 209 assert(v[0, i] == i); 210 } 211 212 // *************************************************************************** 213 214 /// Performs linear downscale by a constant factor 215 template downscale(int HRX, int HRY=HRX) 216 { 217 auto downscale(SRC, TARGET)(auto ref SRC src, auto ref TARGET target) 218 if (isDirectView!SRC && isWritableView!TARGET) 219 { 220 alias lr = target; 221 alias hr = src; 222 alias COLOR = ViewColor!SRC; 223 224 assert(hr.w % HRX == 0 && hr.h % HRY == 0, "Size mismatch"); 225 226 lr.size(hr.w / HRX, hr.h / HRY); 227 228 foreach (y; 0..lr.h) 229 foreach (x; 0..lr.w) 230 { 231 static if (HRX*HRY <= 0x100) 232 enum EXPAND_BYTES = 1; 233 else 234 static if (HRX*HRY <= 0x10000) 235 enum EXPAND_BYTES = 2; 236 else 237 static assert(0); 238 static if (is(typeof(COLOR.init.a))) // downscale with alpha 239 { 240 version (none) // TODO: broken 241 { 242 ExpandChannelType!(COLOR, EXPAND_BYTES+COLOR.init.a.sizeof) sum; 243 ExpandChannelType!(typeof(COLOR.init.a), EXPAND_BYTES) alphaSum; 244 auto start = y*HRY*hr.stride + x*HRX; 245 foreach (j; 0..HRY) 246 { 247 foreach (p; hr.pixels[start..start+HRX]) 248 { 249 foreach (i, f; p.tupleof) 250 static if (p.tupleof[i].stringof != "p.a") 251 { 252 enum FIELD = p.tupleof[i].stringof[2..$]; 253 mixin("sum."~FIELD~" += cast(typeof(sum."~FIELD~"))p."~FIELD~" * p.a;"); 254 } 255 alphaSum += p.a; 256 } 257 start += hr.stride; 258 } 259 if (alphaSum) 260 { 261 auto result = cast(COLOR)(sum / alphaSum); 262 result.a = cast(typeof(result.a))(alphaSum / (HRX*HRY)); 263 lr[x, y] = result; 264 } 265 else 266 { 267 static assert(COLOR.init.a == 0); 268 lr[x, y] = COLOR.init; 269 } 270 } 271 else 272 static assert(false, "Downscaling with alpha is not implemented"); 273 } 274 else 275 { 276 ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum; 277 auto x0 = x*HRX; 278 auto x1 = x0+HRX; 279 foreach (j; y*HRY..(y+1)*HRY) 280 foreach (s; hr.scanline(j)[x0..x1]) 281 foreach (p; s) 282 sum += p; 283 lr[x, y] = cast(ViewColor!SRC)(sum / (HRX*HRY)); 284 } 285 } 286 287 return target; 288 } 289 290 auto downscale(SRC)(auto ref SRC src) 291 if (isView!SRC) 292 { 293 ViewImage!SRC target; 294 return src.downscale(target); 295 } 296 } 297 298 unittest 299 { 300 onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)(); 301 // onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscale!(2, 2)(); 302 303 Image!ubyte i; 304 i.size(4, 1); 305 i.pixels[] = [[1], [3], [5], [7]]; 306 auto d = i.downscale!(2, 1); 307 assert(d.pixels == [[2], [6]]); 308 } 309 310 // *************************************************************************** 311 312 /// Downscaling copy (averages colors in source per one pixel in target). 313 auto downscaleTo(SRC, TARGET)(auto ref SRC src, auto ref TARGET target) 314 if (isDirectView!SRC && isWritableView!TARGET) 315 { 316 alias lr = target; 317 alias hr = src; 318 alias COLOR = ViewColor!SRC; 319 320 void impl(uint EXPAND_BYTES)() 321 { 322 foreach (y; 0..lr.h) 323 foreach (x; 0..lr.w) 324 { 325 static if (is(typeof(COLOR.init.a))) // downscale with alpha 326 static assert(false, "Downscaling with alpha is not implemented"); 327 else 328 { 329 ExpandChannelType!(ViewColor!SRC, EXPAND_BYTES) sum; 330 auto x0 = x * hr.w / lr.w; 331 auto x1 = (x+1) * hr.w / lr.w; 332 auto y0 = y * hr.h / lr.h; 333 auto y1 = (y+1) * hr.h / lr.h; 334 335 // When upscaling (across one or two axes), 336 // fall back to nearest neighbor 337 if (x0 == x1) x1++; 338 if (y0 == y1) y1++; 339 340 foreach (j; y0 .. y1) 341 foreach (s; hr.scanline(j)[x0 .. x1]) 342 foreach (p; s) 343 sum += p; 344 auto area = (x1 - x0) * (y1 - y0); 345 auto avg = sum / cast(uint)area; 346 lr[x, y] = cast(ViewColor!SRC)(avg); 347 } 348 } 349 } 350 351 auto perPixelArea = (hr.w / lr.w + 1) * (hr.h / lr.h + 1); 352 353 if (perPixelArea <= 0x100) 354 impl!1(); 355 else 356 if (perPixelArea <= 0x10000) 357 impl!2(); 358 else 359 if (perPixelArea <= 0x1000000) 360 impl!3(); 361 else 362 assert(false, "Downscaling too much"); 363 364 return target; 365 } 366 367 /// Downscales an image to a certain size. 368 auto downscaleTo(SRC)(auto ref SRC src, xy_t w, xy_t h) 369 if (isView!SRC) 370 { 371 ViewImage!SRC target; 372 target.size(w, h); 373 return src.downscaleTo(target); 374 } 375 376 unittest 377 { 378 onePixel(RGB.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2); 379 // onePixel(RGBA.init).nearestNeighbor(4, 4).copy.downscaleTo(2, 2); 380 381 Image!ubyte i; 382 i.size(6, 1); 383 i.pixels[] = [[1], [2], [3], [4], [5], [6]]; 384 assert(i.downscaleTo(6, 1).pixels == [[1], [2], [3], [4], [5], [6]]); 385 assert(i.downscaleTo(3, 1).pixels == [[1], [3], [5]]); 386 assert(i.downscaleTo(2, 1).pixels == [[2], [5]]); 387 assert(i.downscaleTo(1, 1).pixels == [[3]]); 388 389 i.size(3, 3); 390 i.pixels[] = [ 391 [1], [2], [3], 392 [4], [5], [6], 393 [7], [8], [9]]; 394 assert(i.downscaleTo(2, 2).pixels == [[1], [2], [5], [7]]); 395 396 i.size(1, 1); 397 i.pixels = [[1]]; 398 assert(i.downscaleTo(2, 2).pixels == [[1], [1], [1], [1]]); 399 } 400 401 // *************************************************************************** 402 403 /// Copy the indicated row of src to a StorageType buffer. 404 void copyScanline(SRC, StorageType)(auto ref SRC src, xy_t y, StorageType[] dst) 405 if (isView!SRC && is(StorageColor!StorageType == ViewColor!SRC)) 406 { 407 static if (isDirectView!SRC && is(ViewStorageType!SRC == StorageType)) 408 dst[] = src.scanline(y)[]; 409 else 410 { 411 auto storageUnitsPerRow = (src.w + StorageType.length - 1) / StorageType.length; 412 assert(storageUnitsPerRow == dst.length); 413 foreach (x; 0..src.w) 414 dst[x / StorageType.length][x % StorageType.length] = src[x, y]; 415 } 416 } 417 418 /// Copy a view's pixels (top-to-bottom) to a StorageType buffer. 419 /// Rows are assumed to be StorageType.sizeof-aligned. 420 void copyPixels(SRC, StorageType)(auto ref SRC src, StorageType[] dst) 421 if (isView!SRC && is(StorageColor!StorageType == ViewColor!SRC)) 422 { 423 auto storageUnitsPerRow = src.w + (StorageType.length - 1) / StorageType.length; 424 assert(dst.length == storageUnitsPerRow * src.h); 425 foreach (y; 0..src.h) 426 src.copyScanline(y, dst[y*storageUnitsPerRow..(y+1)*storageUnitsPerRow]); 427 } 428 429 // *************************************************************************** 430 431 import std.traits; 432 433 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433 434 435 struct InputColor {} 436 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR); 437 438 struct TargetColor {} 439 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET); 440 441 // *************************************************************************** 442 443 import ae.utils.graphics.color; 444 import ae.utils.meta : structFields; 445 446 private string[] readPBMHeader(ref const(ubyte)[] data) 447 { 448 import std.ascii; 449 450 string[] fields; 451 uint wordStart = 0; 452 uint p; 453 for (p=1; p<data.length && fields.length<4; p++) 454 if (!isWhite(data[p-1]) && isWhite(data[p])) 455 fields ~= cast(string)data[wordStart..p]; 456 else 457 if (isWhite(data[p-1]) && !isWhite(data[p])) 458 wordStart = p; 459 data = data[p..$]; 460 enforce(fields.length==4, "Header too short"); 461 enforce(fields[0].length==2 && fields[0][0]=='P', "Invalid signature"); 462 return fields; 463 } 464 465 private template PBMSignature(COLOR) 466 { 467 static if (structFields!COLOR == ["l"]) 468 enum PBMSignature = "P5"; 469 else 470 static if (structFields!COLOR == ["r", "g", "b"]) 471 enum PBMSignature = "P6"; 472 else 473 static assert(false, "Unsupported PBM color: " ~ 474 __traits(allMembers, COLOR.Fields).stringof); 475 } 476 477 /// Parses a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 478 auto parsePBM(C = TargetColor, TARGET)(const(void)[] vdata, auto ref TARGET target) 479 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 480 { 481 alias COLOR = ViewColor!TARGET; 482 483 auto data = cast(const(ubyte)[])vdata; 484 string[] fields = readPBMHeader(data); 485 enforce(fields[0]==PBMSignature!COLOR, "Invalid signature"); 486 enforce(to!uint(fields[3]) == COLOR.tupleof[0].max, "Channel depth mismatch"); 487 488 target.size(to!uint(fields[1]), to!uint(fields[2])); 489 enforce(data.length / COLOR.sizeof == target.w * target.h, 490 "Dimension / filesize mismatch"); 491 target.pixels[] = cast(PlainStorageUnit!COLOR[])data; 492 493 static if (COLOR.tupleof[0].sizeof > 1) 494 foreach (ref pixel; pixels) 495 pixel = COLOR.op!q{swapBytes(a)}(pixel); // TODO: proper endianness support 496 497 return target; 498 } 499 /// ditto 500 auto parsePBM(COLOR)(const(void)[] vdata) 501 { 502 Image!COLOR target; 503 return vdata.parsePBM(target); 504 } 505 506 unittest 507 { 508 import std.conv : hexString; 509 auto data = "P6\n2\n2\n255\n" ~ 510 hexString!"000000 FFF000" ~ 511 hexString!"000FFF FFFFFF"; 512 auto i = data.parsePBM!RGB(); 513 assert(i[0, 0] == RGB.fromHex("000000")); 514 assert(i[0, 1] == RGB.fromHex("000FFF")); 515 } 516 517 unittest 518 { 519 import std.conv : hexString; 520 auto data = "P5\n2\n2\n255\n" ~ 521 hexString!"00 55" ~ 522 hexString!"AA FF"; 523 auto i = data.parsePBM!L8(); 524 assert(i[0, 0] == L8(0x00)); 525 assert(i[0, 1] == L8(0xAA)); 526 } 527 528 /// Creates a binary Netpbm monochrome (.pgm) or RGB (.ppm) file. 529 ubyte[] toPBM(SRC)(auto ref SRC src) 530 if (isView!SRC) 531 { 532 alias COLOR = ViewColor!SRC; 533 alias StorageType = PlainStorageUnit!COLOR; 534 535 auto length = src.w * src.h; 536 ubyte[] header = cast(ubyte[])"%s\n%d %d %d\n" 537 .format(PBMSignature!COLOR, src.w, src.h, ChannelType!COLOR.max); 538 ubyte[] data = new ubyte[header.length + length * COLOR.sizeof]; 539 540 data[0..header.length] = header; 541 src.copyPixels(cast(StorageType[])data[header.length..$]); 542 543 static if (ChannelType!COLOR.sizeof > 1) 544 foreach (ref p; cast(ChannelType!COLOR[])data[header.length..$]) 545 p = swapBytes(p); // TODO: proper endianness support 546 547 return data; 548 } 549 550 unittest 551 { 552 import std.conv : hexString; 553 assert(onePixel(RGB(1,2,3)).toPBM == "P6\n1 1 255\n" ~ hexString!"01 02 03"); 554 assert(onePixel(L8 (1) ).toPBM == "P5\n1 1 255\n" ~ hexString!"01" ); 555 } 556 557 // *************************************************************************** 558 559 /// Loads a raw COLOR[] into an image of the indicated size. 560 auto fromPixels(C = InputColor, INPUT, TARGET)(INPUT[] input, uint w, uint h, 561 auto ref TARGET target) 562 if (isWritableView!TARGET 563 && is(GetInputColor!(C, INPUT) == ViewColor!TARGET)) 564 { 565 alias COLOR = ViewColor!TARGET; 566 567 auto pixels = cast(PlainStorageUnit!COLOR[])input; 568 enforce(pixels.length == w*h, "Dimension / filesize mismatch"); 569 target.size(w, h); 570 target.pixels[] = pixels; 571 return target; 572 } 573 574 /// ditto 575 auto fromPixels(C = InputColor, INPUT)(INPUT[] input, uint w, uint h) 576 { 577 alias COLOR = GetInputColor!(C, INPUT); 578 Image!COLOR target; 579 return fromPixels!COLOR(input, w, h, target); 580 } 581 582 unittest 583 { 584 import std.conv : hexString; 585 Image!L8 i; 586 i = hexString!"42".fromPixels!L8(1, 1); 587 i = hexString!"42".fromPixels!L8(1, 1, i); 588 assert(i[0, 0].l == 0x42); 589 i = (cast(L8[])hexString!"42").fromPixels(1, 1); 590 i = (cast(L8[])hexString!"42").fromPixels(1, 1, i); 591 } 592 593 // *************************************************************************** 594 595 static import ae.utils.graphics.bitmap; 596 597 // Different software have different standards regarding alpha without a V4 header. 598 // ImageMagick will write BMPs with alpha without a V4 header, but not all software will read them. 599 enum bitmapNeedV4HeaderForWrite(COLOR) = is(COLOR == struct) && !is(COLOR == BGR) && !is(COLOR == BGRX); 600 enum bitmapNeedV4HeaderForRead (COLOR) = is(COLOR == struct) && !is(COLOR == BGR) && !is(COLOR == BGRX) && !is(COLOR == BGRA); 601 602 uint[4] bitmapChannelMasks(COLOR)() 603 { 604 uint[4] result; 605 foreach (i, f; COLOR.init.tupleof) 606 { 607 enum channelName = __traits(identifier, COLOR.tupleof[i]); 608 static if (channelName != "x") 609 static assert((COLOR.tupleof[i].offsetof + COLOR.tupleof[i].sizeof) * 8 <= 32, 610 "Color " ~ COLOR.stringof ~ " (channel " ~ channelName ~ ") is too large for BMP"); 611 612 enum MASK = (cast(uint)typeof(COLOR.tupleof[i]).max) << (COLOR.tupleof[i].offsetof*8); 613 static if (channelName == "r") 614 result[0] |= MASK; 615 else 616 static if (channelName == "g") 617 result[1] |= MASK; 618 else 619 static if (channelName == "b") 620 result[2] |= MASK; 621 else 622 static if (channelName == "a") 623 result[3] |= MASK; 624 else 625 static if (channelName == "l") 626 { 627 result[0] |= MASK; 628 result[1] |= MASK; 629 result[2] |= MASK; 630 } 631 else 632 static if (channelName == "x") 633 { 634 } 635 else 636 static assert(false, "Don't know how to encode channelNamenel " ~ channelName); 637 } 638 return result; 639 } 640 641 @property size_t bitmapPixelStride(StorageType)(xy_t w) 642 { 643 auto rowBits = w * storageColorBits!StorageType; 644 rowBits = (rowBits + 0x1f) & ~0x1f; 645 return rowBits / 8; 646 } 647 648 template BMPStorageType(COLOR) 649 { 650 static if (is(COLOR == bool)) 651 alias BMPStorageType = OneBitStorageBE; 652 else 653 alias BMPStorageType = PlainStorageUnit!COLOR; 654 } 655 656 /// Returns a view representing a BMP file. 657 /// Does not copy pixel data. 658 auto viewBMP(COLOR, V)(V data) 659 if (is(V : const(void)[])) 660 { 661 import ae.utils.graphics.bitmap; 662 alias BitmapHeader!3 Header; 663 enforce(data.length > Header.sizeof, "Not enough data for header"); 664 Header* header = cast(Header*) data.ptr; 665 enforce(header.bfType == "BM", "Invalid signature"); 666 enforce(header.bfSize == data.length, "Incorrect file size (%d in header, %d in file)" 667 .format(header.bfSize, data.length)); 668 enforce(header.bcSize >= Header.sizeof - header.bcSize.offsetof); 669 670 alias StorageType = BMPStorageType!COLOR; 671 672 static struct BMP 673 { 674 xy_t w, h; 675 typeof(data.ptr) pixelData; 676 sizediff_t pixelStride; 677 678 inout(StorageType)[] scanline(xy_t y) inout 679 { 680 assert(y >= 0 && y < h, "BMP scanline out of bounds"); 681 auto row = cast(void*)pixelData + y * pixelStride; 682 auto storageUnitsPerRow = (w + StorageType.length - 1) / StorageType.length; 683 return (cast(inout(StorageType)*)row)[0 .. storageUnitsPerRow]; 684 } 685 686 mixin DirectView; 687 } 688 BMP bmp; 689 690 bmp.w = header.bcWidth; 691 bmp.h = header.bcHeight; 692 enforce(header.bcPlanes==1, "Multiplane BMPs not supported"); 693 694 enum storageBits = StorageType.sizeof * 8 / StorageType.length; 695 enforce(header.bcBitCount == storageBits, 696 "Mismatching BMP bcBitCount - trying to load a %d-bit .BMP file to a %d-bit Image" 697 .format(header.bcBitCount, storageBits)); 698 699 static if (bitmapNeedV4HeaderForRead!COLOR) 700 enforce(header.VERSION >= 4, "Need a V4+ header to load a %s image".format(COLOR.stringof)); 701 if (header.VERSION >= 4) 702 { 703 enforce(data.length > BitmapHeader!4.sizeof, "Not enough data for header"); 704 auto header4 = cast(BitmapHeader!4*) data.ptr; 705 static if (is(COLOR == struct)) 706 { 707 uint[4] fileMasks = [ 708 header4.bV4RedMask, 709 header4.bV4GreenMask, 710 header4.bV4BlueMask, 711 header4.bV4AlphaMask]; 712 static immutable expectedMasks = bitmapChannelMasks!COLOR(); 713 enforce(fileMasks == expectedMasks, 714 "Channel format mask mismatch.\nExpected: [%(%32b, %)]\nIn file : [%(%32b, %)]" 715 .format(expectedMasks, fileMasks)); 716 } 717 else 718 throw new Exception("Unexpected V4 header with basic COLOR type " ~ COLOR.stringof); 719 } 720 721 auto pixelData = data[header.bfOffBits..$]; 722 bmp.pixelData = pixelData.ptr; 723 bmp.pixelStride = bitmapPixelStride!StorageType(bmp.w); 724 enforce(bmp.pixelStride * abs(bmp.h) <= pixelData.length, "Insufficient data for pixels"); 725 726 if (bmp.h < 0) 727 bmp.h = -bmp.h; 728 else 729 { 730 bmp.pixelData += bmp.pixelStride * (bmp.h - 1); 731 bmp.pixelStride = -bmp.pixelStride; 732 } 733 734 return bmp; 735 } 736 737 /// Parses a Windows bitmap (.bmp) file. 738 auto parseBMP(C = TargetColor, TARGET)(const(void)[] data, auto ref TARGET target) 739 if (isWritableView!TARGET && isTargetColor!(C, TARGET)) 740 { 741 alias COLOR = ViewColor!TARGET; 742 viewBMP!COLOR(data).copy(target); 743 return target; 744 } 745 /// ditto 746 auto parseBMP(COLOR)(const(void)[] data) 747 { 748 Image!(COLOR, BMPStorageType!COLOR) target; 749 return data.parseBMP(target); 750 } 751 752 unittest 753 { 754 alias parseBMP!BGR parseBMP24; 755 if (false) 756 { 757 auto b = viewBMP!BGRA((void[]).init); 758 BGRA c = b[1, 2]; 759 } 760 alias parseBMP!bool parseBMP1; 761 } 762 763 /// Creates a Windows bitmap (.bmp) file. 764 ubyte[] toBMP(SRC)(auto ref SRC src) 765 if (isView!SRC) 766 { 767 alias COLOR = ViewColor!SRC; 768 alias StorageType = BMPStorageType!COLOR; 769 770 import ae.utils.graphics.bitmap; 771 static if (bitmapNeedV4HeaderForWrite!COLOR) 772 alias BitmapHeader!4 Header; 773 else 774 alias BitmapHeader!3 Header; 775 776 auto pixelStride = bitmapPixelStride!StorageType(src.w); 777 auto bitmapDataSize = src.h * pixelStride; 778 ubyte[] data = new ubyte[Header.sizeof + bitmapDataSize]; 779 auto header = cast(Header*)data.ptr; 780 *header = Header.init; 781 header.bfSize = data.length.to!uint; 782 header.bfOffBits = Header.sizeof; 783 header.bcWidth = src.w.to!int; 784 header.bcHeight = -src.h.to!int; 785 header.bcPlanes = 1; 786 header.biSizeImage = bitmapDataSize.to!uint; 787 enum storageBits = StorageType.sizeof * 8 / StorageType.length; 788 header.bcBitCount = storageBits; 789 790 static if (header.VERSION >= 4) 791 { 792 header.biCompression = BI_BITFIELDS; 793 static immutable masks = bitmapChannelMasks!COLOR(); 794 header.bV4RedMask = masks[0]; 795 header.bV4GreenMask = masks[1]; 796 header.bV4BlueMask = masks[2]; 797 header.bV4AlphaMask = masks[3]; 798 } 799 800 auto pixelData = data[header.bfOffBits..$]; 801 auto ptr = pixelData.ptr; 802 size_t pos = 0; 803 auto storageUnitsPerRow = (src.w + StorageType.length - 1) / StorageType.length; 804 805 foreach (y; 0..src.h) 806 { 807 src.copyScanline(y, (cast(StorageType*)ptr)[0..storageUnitsPerRow]); 808 ptr += pixelStride; 809 } 810 811 return data; 812 } 813 814 unittest 815 { 816 Image!BGR output; 817 onePixel(BGR(1,2,3)).toBMP().parseBMP!BGR(output); 818 } 819 820 // *************************************************************************** 821 822 static immutable ubyte[8] pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; // \211 P N G \r \n \032 \n 823 824 struct PNGChunk 825 { 826 char[4] type; 827 const(void)[] data; 828 829 uint crc32() 830 { 831 import std.digest.crc; 832 CRC32 crc; 833 crc.put(cast(ubyte[])(type[])); 834 crc.put(cast(ubyte[])data); 835 ubyte[4] hash = crc.finish(); 836 return *cast(uint*)hash.ptr; 837 } 838 839 this(string type, const(void)[] data) 840 { 841 this.type[] = type[]; 842 this.data = data; 843 } 844 } 845 846 enum PNGColourType : ubyte { G, RGB=2, PLTE, GA, RGBA=6 } 847 enum PNGCompressionMethod : ubyte { DEFLATE } 848 enum PNGFilterMethod : ubyte { ADAPTIVE } 849 enum PNGInterlaceMethod : ubyte { NONE, ADAM7 } 850 851 enum PNGFilterAdaptive : ubyte { NONE, SUB, UP, AVERAGE, PAETH } 852 853 align(1) 854 struct PNGHeader 855 { 856 align(1): 857 ubyte[4] width, height; 858 ubyte colourDepth; 859 PNGColourType colourType; 860 PNGCompressionMethod compressionMethod; 861 PNGFilterMethod filterMethod; 862 PNGInterlaceMethod interlaceMethod; 863 static assert(PNGHeader.sizeof == 13); 864 } 865 866 struct PNGChunkHeader { ubyte[4] length; char[4] type; } 867 struct PNGChunkFooter { ubyte[4] crc32; } 868 869 /// Creates a PNG file. 870 /// Only basic PNG features are supported 871 /// (no filters, interlacing, palettes etc.) 872 ubyte[] toPNG(SRC)(auto ref SRC src, int compressionLevel = 5) 873 if (isView!SRC) 874 { 875 import std.zlib : compress; 876 import std.bitmanip : nativeToBigEndian, swapEndian; 877 878 alias COLOR = ViewColor!SRC; 879 static if (!is(COLOR == struct)) 880 enum COLOUR_TYPE = PNGColourType.G; 881 else 882 static if (structFields!COLOR == ["l"]) 883 enum COLOUR_TYPE = PNGColourType.G; 884 else 885 static if (structFields!COLOR == ["r","g","b"]) 886 enum COLOUR_TYPE = PNGColourType.RGB; 887 else 888 static if (structFields!COLOR == ["l","a"]) 889 enum COLOUR_TYPE = PNGColourType.GA; 890 else 891 static if (structFields!COLOR == ["r","g","b","a"]) 892 enum COLOUR_TYPE = PNGColourType.RGBA; 893 else 894 static assert(0, "Unsupported PNG color type: " ~ COLOR.stringof); 895 896 static if (is(COLOR == bool)) 897 alias StorageType = OneBitStorageBE; 898 else 899 alias StorageType = PlainStorageUnit!COLOR; 900 901 static if (is(COLOR == struct)) 902 enum numChannels = structFields!COLOR.length; 903 else 904 enum numChannels = 1; 905 906 PNGChunk[] chunks; 907 PNGHeader header = { 908 width : nativeToBigEndian(src.w.to!uint), 909 height : nativeToBigEndian(src.h.to!uint), 910 colourDepth : StorageType.sizeof * 8 / StorageType.length / numChannels, 911 colourType : COLOUR_TYPE, 912 compressionMethod : PNGCompressionMethod.DEFLATE, 913 filterMethod : PNGFilterMethod.ADAPTIVE, 914 interlaceMethod : PNGInterlaceMethod.NONE, 915 }; 916 chunks ~= PNGChunk("IHDR", cast(void[])[header]); 917 auto storageUnitsPerRow = (src.w + StorageType.length - 1) / StorageType.length; 918 size_t idatStride = 1 + (storageUnitsPerRow * StorageType.sizeof); 919 ubyte[] idatData = new ubyte[src.h * idatStride]; 920 for (uint y=0; y<src.h; y++) 921 { 922 idatData[y * idatStride] = PNGFilterAdaptive.NONE; 923 auto rowPixels = cast(StorageType[])idatData[1 + (y * idatStride) .. (y + 1) * idatStride]; 924 src.copyScanline(y, rowPixels); 925 926 version (LittleEndian) 927 static if (ChannelType!COLOR.sizeof > 1) 928 foreach (ref p; cast(ChannelType!COLOR[])rowPixels) 929 p = swapEndian(p); 930 } 931 chunks ~= PNGChunk("IDAT", compress(idatData, compressionLevel)); 932 chunks ~= PNGChunk("IEND", null); 933 934 return makePNG(chunks); 935 } 936 937 ubyte[] makePNG(PNGChunk[] chunks) 938 { 939 import std.bitmanip : nativeToBigEndian; 940 941 size_t totalSize = pngSignature.length; 942 foreach (chunk; chunks) 943 totalSize += PNGChunkHeader.sizeof + chunk.data.length + PNGChunkFooter.sizeof; 944 ubyte[] data = new ubyte[totalSize]; 945 946 data[0 .. pngSignature.length] = pngSignature; 947 size_t pos = pngSignature.length; 948 foreach (chunk; chunks) 949 { 950 auto header = cast(PNGChunkHeader*)data[pos .. $].ptr; 951 header.length = chunk.data.length.to!uint.nativeToBigEndian; 952 header.type = chunk.type; 953 pos += PNGChunkHeader.sizeof; 954 955 data[pos .. pos + chunk.data.length] = cast(ubyte[])chunk.data; 956 pos += chunk.data.length; 957 958 auto footer = cast(PNGChunkFooter*)data[pos .. $].ptr; 959 footer.crc32 = chunk.crc32.nativeToBigEndian; 960 pos += PNGChunkFooter.sizeof; 961 } 962 963 return data; 964 } 965 966 unittest 967 { 968 onePixel(RGB(1,2,3)).toPNG(); 969 onePixel(5).toPNG(); 970 onePixel(true).toPNG(); 971 }