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