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