1 /* 2 * dimage - png.d 3 * by Laszlo Szeremi 4 * 5 * Copyright under Boost Software License. 6 */ 7 8 module dimage.png; 9 10 import dimage.base; 11 12 import zlib = etc.c.zlib; 13 import std.digest.crc; 14 import std.bitmanip; 15 import std.algorithm.mutation : reverse; 16 static import std.stdio; 17 static import std.string; 18 import std.string : fromStringz; 19 import core.stdc.stdint; 20 import bitleveld.datatypes; 21 import dimage.util; 22 import std.conv : to; 23 /** 24 * Implements the Portable Network Graphics file format as a class. 25 * 26 * Supports APNG extenstions. 27 */ 28 public class PNG : Image, MultiImage, CustomImageMetadata { 29 ///Chunk initializer IDs. 30 static enum ChunkInitializers : char[4] { 31 Header = "IHDR", ///Standard, for header, before image data 32 Palette = "PLTE", ///Standard, for palette, after header and before image data 33 Data = "IDAT", ///Standard, for image data 34 End = "IEND", ///Standard, file end marker 35 //Ancillary chunks: 36 Background = "bKGD", ///Background color 37 Chromaticies = "cHRM", ///Primary chromaticities and white point 38 Gamma = "gAMA", ///Image gamma 39 Histogram = "hIST", ///Image histogram 40 PixDim = "pHYs", ///Physical pixel dimensions 41 SignifBits = "sBIT", ///Significant bits 42 TextData = "tEXt", ///Textual data 43 Time = "tIME", ///Last modification date 44 Transparency = "tRNS", ///Transparency 45 CompTextData = "zTXt", ///Compressed textual data 46 IntTextData = "iTXt", ///International textual data 47 //Chunks for APNG: 48 AnimationControl = "acTL", ///Animation control chunk 49 FrameControl = "fcTL", ///Frame control chunk 50 FrameData = "fDAT", ///Frame data 51 } 52 /** 53 * Defines standard PNG filter types. 54 */ 55 static enum FilterType : ubyte { 56 None, 57 Sub, 58 Up, 59 Average, 60 Paeth, 61 } 62 /** 63 * Defines the flags in the flag field. 64 */ 65 static enum Flags : uint { 66 HasTransparency = 0x01, 67 HasAPNGext = 0x02, 68 } 69 //static enum HEADER_INIT = "IHDR"; ///Initializes header in the file 70 //static enum PALETTE_INIT = "PLTE"; ///Initializes palette in the file 71 //static enum DATA_INIT = "IDAT"; ///Initializes image data in the file 72 //static enum END_INIT = "IEND"; ///Initializes the end of the file 73 static enum ubyte[8] PNG_SIGNATURE = [0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]; ///Used for checking PNG files 74 static enum ubyte[4] PNG_CLOSER = [0xAE, 0x42, 0x60, 0x82]; ///Final checksum of IEND 75 /// Used for static function load. Selects checksum checking policy. 76 static enum ChecksumPolicy : ubyte{ 77 DisableAll, 78 DisableAncillary, 79 Enable 80 } 81 /** 82 * Stores ancillary data embedded into PNG files. Handling these are not vital for opening PNG files, but available for various purposes. 83 * Please be aware that certain readers might have issues with nonstandard chunks 84 */ 85 public class EmbeddedData{ 86 /** 87 * Describes where this data stream should go within the file. 88 * This describe the exact location, and configured when loading from PNG files. WithinIDAT will result in dumping all of them before the last theoretical IDAT chunk. 89 */ 90 public enum DataPosition{ 91 BeforePLTE, 92 BeforeIDAT, 93 WithinIDAT, 94 AfterIDAT 95 } 96 DataPosition pos; /// Describes the exact location of 97 char[4] identifier; /// Identifies the data of this chunk 98 ubyte[] data; /// Contains the data of this chunk 99 /** 100 * Creates an instance of this class. 101 */ 102 public this(DataPosition pos, char[4] identifier, ubyte[] data){ 103 this.pos = pos; 104 this.identifier = identifier; 105 this.data = data; 106 } 107 } 108 /** 109 * PNG Chunk identifier 110 */ 111 struct Chunk{ 112 uint dataLength; ///Length of chunk 113 char[4] identifier; ///Identifies the data of this chunk 114 /** 115 * Converts the struct to little endian on systems that need them. 116 */ 117 public void bigEndianToNative() { 118 version(LittleEndian) 119 dataLength = swapEndian(dataLength); 120 } 121 /** 122 * Returns a copy of the struct that is in big endian. 123 */ 124 public Chunk nativeToBigEndian() { 125 version(LittleEndian) 126 return Chunk(swapEndian(dataLength), identifier); 127 else 128 return this; 129 } 130 } 131 /** 132 * Represents the possible types for PNG 133 * While bitmasking could be used, not every combination are valid 134 */ 135 enum ColorType : ubyte { 136 Greyscale = 0, 137 TrueColor = 2, 138 Indexed = 3, 139 GreyscaleWithAlpha = 4, 140 TrueColorWithAlpha = 6, 141 } 142 /** 143 * Contains most data related to PNG files. 144 */ 145 align(1) struct Header{ 146 align(1): 147 uint width; /// Width of image in pixels 148 uint height; /// Height of image in pixels 149 ubyte bitDepth; /// Bits per pixel or per sample 150 ColorType colorType; /// Color interpretation indicator 151 ubyte compression; /// Compression type indicator 152 ubyte filter; /// Filter type indicator 153 ubyte interlace; /// Type of interlacing scheme used 154 /** 155 * Converts the struct to little endian on systems that need them. 156 */ 157 public void bigEndianToNative() @safe @nogc pure nothrow { 158 version(LittleEndian){ 159 width = swapEndian(width); 160 height = swapEndian(height); 161 } 162 } 163 /** 164 * Returns a copy of the struct that is in big endian. 165 */ 166 public Header nativeToBigEndian() @safe pure nothrow { 167 version(LittleEndian) 168 return Header(swapEndian(width), swapEndian(height), bitDepth, colorType, compression, filter, interlace); 169 else 170 return this; 171 } 172 /** 173 * For debugging purposes. 174 */ 175 public string toString() const { 176 import std.conv : to; 177 return "width: " ~ to!string(width) ~ "\n" ~ 178 "height: " ~ to!string(height) ~ "\n" ~ 179 "bitDepth: " ~ to!string(bitDepth) ~ "\n" ~ 180 "colorType: " ~ to!string(colorType) ~ "\n" ~ 181 "compression: " ~ to!string(compression) ~ "\n" ~ 182 "filter: " ~ to!string(filter) ~ "\n" ~ 183 "interlace: " ~ to!string(interlace) ~ "\n"; 184 } 185 } 186 /** 187 * Contains textual metadata embedded into the file. 188 */ 189 struct Text { 190 string keyword; ///The keyword, which the textual metadata is associated with. (Max 79 characters) 191 string text; ///The text itself. 192 string lang; ///Language in case of international textual data. 193 string ikeyword; ///Translated keyword if any. 194 ubyte compression; ///Compression method flag. (should be zero) 195 ubyte isCompressed; ///1 if field is compressed, 0 otherwise. 196 } 197 /** 198 * Animation control chunk. 199 * If found in a PNG file, it means it has the APNG extensions. 200 */ 201 struct AnimationControl { 202 uint numFrames; ///Number of animation frames. 203 uint numPlays; ///Number of repeats (0 means infinite loop). 204 /** 205 * Converts the struct to little endian on systems that need them. 206 */ 207 public void bigEndianToNative() @safe @nogc pure nothrow { 208 version(LittleEndian){ 209 numFrames = swapEndian(numFrames); 210 numPlays = swapEndian(numPlays); 211 } 212 } 213 /** 214 * Converts the struct to big endian for storage. 215 */ 216 public void nativeToBigEndian() @safe @nogc pure nothrow { 217 version(LittleEndian){ 218 numFrames = swapEndian(numFrames); 219 numPlays = swapEndian(numPlays); 220 } 221 } 222 } 223 /** 224 * Frame disposal operator. 225 */ 226 enum FrameDisposalOperator : ubyte { 227 None, ///no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is. 228 Background, ///the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. 229 Previous, ///the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame. 230 } 231 /** 232 * Frame blend operator. 233 */ 234 enum FrameBlendOperator : ubyte { 235 Source, ///all color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region 236 Over, ///the frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification 237 } 238 /** 239 * Frame control chunk. 240 */ 241 struct FrameControl { 242 uint seqNum; 243 uint width; 244 uint height; 245 uint xOffset; 246 uint yOffset; 247 ushort delayNum; 248 ushort delayDen; 249 FrameDisposalOperator disposeOp; 250 FrameBlendOperator blendOp; 251 /** 252 * Converts the struct to little endian on systems that need them. 253 */ 254 public void bigEndianToNative() @safe @nogc pure nothrow { 255 version(LittleEndian){ 256 seqNum = swapEndian(seqNum); 257 width = swapEndian(width); 258 height = swapEndian(height); 259 xOffset = swapEndian(xOffset); 260 yOffset = swapEndian(yOffset); 261 delayNum = swapEndian(delayNum); 262 delayDen = swapEndian(delayDen); 263 } 264 } 265 /** 266 * Converts the struct to big endian for storage. 267 */ 268 public void nativeToBigEndian() @safe @nogc pure nothrow { 269 version(LittleEndian){ 270 seqNum = swapEndian(seqNum); 271 width = swapEndian(width); 272 height = swapEndian(height); 273 xOffset = swapEndian(xOffset); 274 yOffset = swapEndian(yOffset); 275 delayNum = swapEndian(delayNum); 276 delayDen = swapEndian(delayDen); 277 } 278 } 279 } 280 protected Header header; 281 protected IImageData baseImage; ///Base image if APNG chunks present. 282 protected IImageData[] frames; ///Extra frames for the APNG extension 283 public EmbeddedData[] ancillaryChunks; ///Stores ancilliary chunks that are not essential for image processing 284 public ubyte[] filterBytes; ///Filterbytes for each scanline 285 public ubyte[][] frameFilterBytes; ///Filterbytes for each frame (might be jagged) 286 public Text[] textData; ///Textual metadata 287 protected int bkgIndex = -1; ///Background index 288 protected uint flags; ///Stores property flags 289 protected RGB16_16_16BE bkgColor; ///Background color 290 protected RGB16_16_16BE trns; ///Transparency 291 protected size_t pitch; 292 /** 293 * Creates an empty PNG file in memory 294 */ 295 public this(IImageData imgDat, IPalette pal = null) @safe pure { 296 //header = Header(width, height, bitDepth, colorType, compression, 0, 0); 297 _imageData = imgDat; 298 _palette = pal; 299 header.bitDepth = imgDat.bitDepth; 300 header.width = imgDat.width; 301 header.height = imgDat.height; 302 filterBytes.length = imgDat.height; 303 pitch = (header.width * imgDat.bitDepth) / 8; 304 switch (imgDat.pixelFormat) { 305 case PixelFormat.Indexed1Bit: .. case PixelFormat.Indexed8Bit: 306 header.colorType = ColorType.Indexed; 307 header.bitDepth = imgDat.bitDepth; 308 break; 309 case PixelFormat.Grayscale1Bit: .. case PixelFormat.Grayscale8Bit: case PixelFormat.Grayscale16Bit: 310 header.colorType = ColorType.Greyscale; 311 header.bitDepth = imgDat.bitDepth; 312 break; 313 case PixelFormat.YA88 | PixelFormat.BigEndian: 314 header.colorType = ColorType.GreyscaleWithAlpha; 315 header.bitDepth = imgDat.bitDepth / 2; 316 break; 317 case PixelFormat.RGB888 | PixelFormat.BigEndian, PixelFormat.RGBX5551 | PixelFormat.BigEndian, 318 PixelFormat.RGB16_16_16 | PixelFormat.BigEndian: 319 header.colorType = ColorType.TrueColor; 320 header.bitDepth = imgDat.bitDepth / 3; 321 break; 322 case PixelFormat.RGBA8888 | PixelFormat.BigEndian, PixelFormat.RGBA16_16_16_16 | PixelFormat.BigEndian: 323 header.colorType = ColorType.TrueColorWithAlpha; 324 header.bitDepth = imgDat.bitDepth / 4; 325 break; 326 default: throw new ImageFormatException("Image format currently not supported!"); 327 } 328 switch (header.bitDepth) { 329 case 1: 330 if (header.width & 7) pitch++; 331 break; 332 case 2: 333 if (header.width & 3) pitch++; 334 break; 335 case 4: 336 if (header.width & 1) pitch++; 337 break; 338 default: 339 break; 340 } 341 } 342 protected this(){ 343 344 } 345 /** 346 * Loads a PNG file. 347 * Currently interlaced mode is unsupported. 348 */ 349 static PNG load(F = std.stdio.File, ChecksumPolicy chksmTest = ChecksumPolicy.DisableAncillary)(ref F file){ 350 class PNGImgDecoder { 351 zlib.z_stream strm; 352 ubyte[] output; 353 ubyte[] filterBytes; 354 ~this() { 355 zlib.inflateEnd(&strm); 356 } 357 } 358 //import std.zlib : UnCompress; 359 RGB16_16_16BE beToNative(RGB16_16_16BE val) { 360 version(LittleEndian) { 361 val.r = swapEndian(val.r); 362 val.g = swapEndian(val.g); 363 val.b = swapEndian(val.b); 364 } 365 return val; 366 } 367 ///Decompresses a text chunk. 368 ///`extstrm` is used in case of an error during decompression. It'll fold it, to avoid memory leakage issues. 369 string decompressText(ubyte[] src, zlib.z_streamp extstrm) { 370 import std.math : nextPow2; 371 ubyte[] output, workpad; 372 zlib.z_stream strm; 373 strm.zalloc = null; 374 strm.zfree = null; 375 strm.opaque = null; 376 int ret = zlib.inflateInit(&strm); 377 strm.next_in = src.ptr; 378 strm.avail_in = cast(uint)src.length; 379 output.length = nextPow2(src.length); 380 strm.next_out = output.ptr; 381 strm.avail_out = cast(uint)output.length; 382 while (strm.avail_in) { 383 ret = zlib.inflate(&strm, zlib.Z_FULL_FLUSH); 384 if(!(ret == zlib.Z_OK || ret == zlib.Z_STREAM_END)){ 385 /* version(unittest) std.stdio.writeln(ret); */ 386 zlib.inflateEnd(&strm); 387 zlib.inflateEnd(extstrm); 388 throw new ImageFileException("Text data decompression error"); 389 } 390 if (!strm.avail_out) {//decompress more data 391 strm.next_out = output.ptr; 392 strm.avail_out = cast(uint)output.length; 393 workpad ~= output; 394 } 395 } 396 workpad ~= output; 397 workpad.length = cast(size_t)strm.total_out; 398 zlib.inflateEnd(&strm); 399 return reinterpretCast!char(workpad).idup; 400 } 401 /+ubyte[] decompressAnimChunk(ubyte[] src, AnimChunk context) { 402 403 }+/ 404 PNG result = new PNG(); 405 bool iend; 406 EmbeddedData.DataPosition pos = EmbeddedData.DataPosition.BeforePLTE; 407 version (unittest) uint scanlineCounter; 408 //initialize decompressor 409 int ret; 410 //uint have; 411 zlib.z_stream strm; 412 strm.zalloc = null; 413 strm.zfree = null; 414 strm.opaque = null; 415 ret = zlib.inflateInit(&strm); 416 if(ret != zlib.Z_OK) 417 throw new Exception("Decompressor initialization error"); 418 419 ubyte[] readBuffer, imageBuffer, imageTemp, paletteTemp, paletteTemp0; 420 readBuffer.length = 8; 421 file.rawRead(readBuffer); 422 for(int i ; i < 8 ; i++) 423 if(readBuffer[i] != PNG_SIGNATURE[i]) 424 throw new ImageFileException("Invalid PNG file signature"); 425 do{ 426 ubyte[4] crc; 427 file.rawRead(readBuffer); 428 Chunk curChunk = (cast(Chunk[])(cast(void[])readBuffer))[0]; 429 curChunk.bigEndianToNative(); 430 readBuffer.length = curChunk.dataLength; 431 if(curChunk.dataLength) 432 file.rawRead(readBuffer); 433 //std.stdio.writeln("pos:", file.tell); 434 switch(curChunk.identifier){ 435 case ChunkInitializers.Header: 436 result.header = reinterpretGet!Header(readBuffer); 437 result.header.bigEndianToNative; 438 /* version (unittest) std.stdio.writeln(result.header); */ 439 result.pitch = (result.header.width * result.getBitdepth) / 8; 440 /* version (unittest) std.stdio.writeln("Pitch length: ", result.pitch); */ 441 imageBuffer.length = result.pitch + 1; 442 /* version (unittest) std.stdio.writeln(result.getPixelFormat); */ 443 strm.next_out = imageBuffer.ptr; 444 strm.avail_out = cast(uint)imageBuffer.length; 445 //result.pitch = result.header.width; 446 break; 447 case ChunkInitializers.Palette: 448 paletteTemp = readBuffer.dup; 449 pos = EmbeddedData.DataPosition.BeforeIDAT; 450 break; 451 case ChunkInitializers.Transparency: 452 453 switch (result.header.colorType){ 454 case ColorType.Indexed: 455 if(readBuffer.length == paletteTemp.length) paletteTemp0 = readBuffer.dup; 456 else if (readBuffer.length == RGB16_16_16BE.sizeof) goto case ColorType.TrueColor; 457 break; 458 case ColorType.TrueColor: 459 result.flags = 1; 460 result.trns = reinterpretGet!RGB16_16_16BE(readBuffer); 461 result.trns = beToNative(result.trns); 462 break; 463 case ColorType.Greyscale: 464 result.flags = 1; 465 const ushort value = reinterpretGet!ushort(readBuffer); 466 result.trns = RGB16_16_16BE(value, value, value); 467 result.trns = beToNative(result.trns); 468 break; 469 default: 470 break; 471 } 472 break; 473 case ChunkInitializers.Data: 474 //if(result.header.compression) 475 //imageBuffer ~= cast(ubyte[])decompressor.uncompress(cast(void[])readBuffer); 476 //else 477 // imageBuffer ~= readBuffer.dup; 478 pos = EmbeddedData.DataPosition.WithinIDAT; 479 strm.next_in = readBuffer.ptr; 480 strm.avail_in = cast(uint)readBuffer.length; 481 while (strm.avail_in) { 482 ret = zlib.inflate(&strm, zlib.Z_FULL_FLUSH); 483 if(!(ret == zlib.Z_OK || ret == zlib.Z_STREAM_END)){ 484 /* version(unittest) std.stdio.writeln(ret); */ 485 zlib.inflateEnd(&strm); 486 throw new ImageFileException("Decompression error"); 487 }else if(imageBuffer.length == strm.total_out){ 488 pos = EmbeddedData.DataPosition.AfterIDAT; 489 } 490 if (!strm.avail_out) {//flush scanline into imagedata 491 version (unittest) scanlineCounter++; 492 version (unittest) assert (scanlineCounter <= result.header.height, "Scanline overflow!"); 493 imageTemp ~= imageBuffer[1..$]; 494 result.filterBytes ~= imageBuffer[0]; 495 strm.next_out = imageBuffer.ptr; 496 strm.avail_out = cast(uint)imageBuffer.length; 497 } 498 } 499 500 break; 501 case ChunkInitializers.Background: 502 switch (result.header.colorType) { 503 case ColorType.Indexed: 504 result.bkgIndex = readBuffer[0]; 505 break; 506 case ColorType.Greyscale, ColorType.GreyscaleWithAlpha: 507 result.bkgIndex = -2; 508 const ushort value = reinterpretGet!ushort(readBuffer); 509 result.bkgColor = RGB16_16_16BE(value, value, value); 510 result.bkgColor = beToNative(result.bkgColor); 511 break; 512 case ColorType.TrueColor, ColorType.TrueColorWithAlpha: 513 result.bkgIndex = -2; 514 result.bkgColor = reinterpretGet!RGB16_16_16BE(readBuffer); 515 result.bkgColor = beToNative(result.bkgColor); 516 break; 517 default: 518 break; 519 } 520 break; 521 case ChunkInitializers.TextData: 522 ubyte[] arr = readBuffer.dup; 523 string keyword = getFirstString(arr); 524 Text t = Text(keyword, reinterpretCast!char(arr).idup, null, null, 0, 0); 525 result.textData ~= t; 526 break; 527 case ChunkInitializers.CompTextData: 528 ubyte[] arr = readBuffer.dup; 529 string keyword = getFirstString(arr); 530 Text t = Text(keyword, decompressText(arr[1..$], &strm), null, null, 0, 1); 531 result.textData ~= t; 532 break; 533 case ChunkInitializers.IntTextData: 534 ubyte[] arr = readBuffer.dup; 535 string keyword = getFirstString(arr); 536 const ubyte cmprflag = arr[0]; 537 arr = arr[2..$]; 538 539 string lngTag; 540 if (arr[0] != '\n') lngTag = getFirstString(arr); 541 else arr = arr[1..$]; 542 543 string intrntKeyword; 544 if (arr[0] != '\n') intrntKeyword = getFirstString(arr); 545 else arr = arr[1..$]; 546 547 if (cmprflag) { 548 result.textData ~= Text(keyword, decompressText(arr, &strm), lngTag, intrntKeyword, 0, 1); 549 } else { 550 result.textData ~= Text(keyword, reinterpretCast!char(arr).idup, lngTag, intrntKeyword, 0, 0); 551 } 552 break; 553 case ChunkInitializers.End: 554 version (unittest) assert(result.header.height == scanlineCounter, "Scanline count mismatch"); 555 if(imageTemp.length + result.filterBytes.length > strm.total_out){ 556 zlib.inflateEnd(&strm); 557 //version(unittest) std.stdio.writeln() 558 throw new ImageFileCompressionException("Decompression error! Image ended at: " ~ to!string(strm.total_out) ~ 559 "; Required length: " ~ to!string(imageTemp.length)); 560 } 561 imageTemp.reserve(result.height * result.pitch); 562 result.filterBytes.reserve(result.height); 563 iend = true; 564 break; 565 default: 566 //Process any unknown chunk as embedded data 567 //EmbeddedData chnk = new EmbeddedData(pos, curChunk.identifier, readBuffer.dup); 568 result.addAncilliaryChunk(pos, curChunk.identifier, readBuffer.dup); 569 /* version (unittest) { 570 std.stdio.writeln ("Acilliary chunk found!"); 571 std.stdio.writeln ("ID: " , curChunk.identifier, " size: ", readBuffer.length, " pos: ", pos); 572 } */ 573 break; 574 575 } 576 if (paletteTemp.length) { 577 if(paletteTemp0.length) result._palette = new PaletteWithSepA!RGB888BE(reinterpretCast!RGB888BE(paletteTemp), 578 paletteTemp0, PixelFormat.RGB888 | PixelFormat.ValidAlpha | PixelFormat.BigEndian, 24); 579 else result._palette = new Palette!RGB888BE(reinterpretCast!RGB888BE(paletteTemp), PixelFormat.RGB888 | 580 PixelFormat.BigEndian, 24); 581 } 582 //calculate crc 583 //if(curChunk.dataLength){ 584 crc = crc32Of((cast(ubyte[])curChunk.identifier) ~ readBuffer); 585 readBuffer.length = 4; 586 file.rawRead(readBuffer); 587 readBuffer.reverse; 588 static if(chksmTest == ChecksumPolicy.Enable){ 589 if(readBuffer != crc) 590 throw new ChecksumMismatchException("Checksum error"); 591 }else static if(chksmTest == ChecksumPolicy.DisableAncillary) { 592 if(readBuffer != crc && (curChunk.identifier == ChunkInitializers.Header || curChunk.identifier == 593 ChunkInitializers.Palette || curChunk.identifier == ChunkInitializers.Data || curChunk.identifier == 594 ChunkInitializers.End)) 595 throw new ChecksumMismatchException("Checksum error"); 596 } 597 //} 598 readBuffer.length = 8; 599 } while(!iend); 600 zlib.inflateEnd(&strm);//should have no data remaining 601 assert(imageTemp.length == result.pitch * result.header.height, "Image size mismatch. Expected size: " ~ 602 to!string(result.pitch * result.header.height) ~ "; Actual size: " ~ to!string(imageTemp.length) ~ 603 "; Pitch length: " ~ to!string(result.pitch) ~ "; N of scanlines: " ~ to!string(result.header.height)); 604 //reconstruct image from filtering 605 ubyte[] scanline, prevScanline; 606 int wordlength = result.getBitdepth > 8 ? result.getBitdepth / 8 : 1; 607 for (uint y ; y < result.filterBytes.length ; y++) { 608 scanline = imageTemp[(y * result.pitch)..((y + 1) * result.pitch)]; 609 switch (result.filterBytes[y]) { 610 case FilterType.Sub: 611 reconstructScanlineSub(scanline, wordlength); 612 break; 613 case FilterType.Up: 614 reconstructScanlineUp(scanline, prevScanline); 615 break; 616 case FilterType.Average: 617 reconstructScanlineAverage(scanline, prevScanline, wordlength); 618 break; 619 case FilterType.Paeth: 620 reconstructScanlinePaeth(scanline, prevScanline, wordlength); 621 break; 622 default: 623 /* version (unittest) { 624 if(result.filterBytes[y]) std.stdio.writeln("Irregular filter type value of \"", result.filterBytes[y] 625 ,"\" found at scanline ", y ); 626 } */ 627 break; 628 } 629 prevScanline = scanline; 630 } 631 //setup imagedata 632 if (result.header.bitDepth == 16) { 633 ushort[] arr = nativeStreamToBigEndian!ushort(reinterpretCast!ushort(imageTemp)); 634 imageTemp = reinterpretCast!ubyte(arr); 635 } 636 switch (result.getPixelFormat & ~PixelFormat.BigEndian) { 637 case PixelFormat.Indexed1Bit: 638 result._imageData = new IndexedImageData1Bit(imageTemp, result._palette, result.width, result.height); 639 break; 640 case PixelFormat.Indexed2Bit: 641 result._imageData = new IndexedImageData2Bit(imageTemp, result._palette, result.width, result.height); 642 break; 643 case PixelFormat.Indexed4Bit: 644 result._imageData = new IndexedImageData4Bit(imageTemp, result._palette, result.width, result.height); 645 break; 646 case PixelFormat.Indexed8Bit: 647 result._imageData = new IndexedImageData!ubyte(imageTemp, result._palette, result.width, result.height); 648 break; 649 case PixelFormat.YA88: 650 result._imageData = new ImageData!YA88BE(reinterpretCast!YA88BE(imageTemp), result.width, result.height, 651 result.getPixelFormat, result.getBitdepth); 652 break; 653 case PixelFormat.YA16_16: 654 //imageTemp = bigEndianStreamToNative(imageTemp); 655 result._imageData = new ImageData!YA16_16BE(reinterpretCast!YA16_16BE(imageTemp), result.width, result.height, 656 result.getPixelFormat, result.getBitdepth); 657 break; 658 case PixelFormat.RGB888: 659 result._imageData = new ImageData!RGB888BE(reinterpretCast!RGB888BE(imageTemp), result.width, result.height, 660 result.getPixelFormat, result.getBitdepth); 661 break; 662 case PixelFormat.RGB16_16_16: 663 //imageTemp = bigEndianStreamToNative(imageTemp); 664 result._imageData = new ImageData!RGB16_16_16BE(reinterpretCast!RGB16_16_16BE(imageTemp), result.width, result.height, 665 result.getPixelFormat, result.getBitdepth); 666 break; 667 case PixelFormat.RGBX5551: 668 result._imageData = new ImageData!RGBA5551(reinterpretCast!RGBA5551(imageTemp), result.width, result.height, 669 result.getPixelFormat, result.getBitdepth); 670 break; 671 case PixelFormat.RGBA8888: 672 result._imageData = new ImageData!RGBA8888BE(reinterpretCast!RGBA8888BE(imageTemp), result.width, result.height, 673 result.getPixelFormat, result.getBitdepth); 674 break; 675 case PixelFormat.RGBA16_16_16_16: 676 //imageTemp = bigEndianStreamToNative(imageTemp); 677 result._imageData = new ImageData!RGBA16_16_16_16BE(reinterpretCast!RGBA16_16_16_16BE(imageTemp), result.width, 678 result.height, result.getPixelFormat, result.getBitdepth); 679 break; 680 case PixelFormat.Grayscale1Bit: 681 result._imageData = new MonochromeImageData1Bit(imageTemp, result.width, result.height); 682 break; 683 case PixelFormat.Grayscale2Bit: 684 result._imageData = new MonochromeImageData2Bit(imageTemp, result.width, result.height); 685 break; 686 case PixelFormat.Grayscale4Bit: 687 result._imageData = new MonochromeImageData4Bit(imageTemp, result.width, result.height); 688 break; 689 case PixelFormat.Grayscale8Bit: 690 result._imageData = new MonochromeImageData!ubyte(reinterpretCast!ubyte(imageTemp), result.width, result.height, 691 result.getPixelFormat, result.getBitdepth); 692 break; 693 case PixelFormat.Grayscale16Bit: 694 //imageTemp = bigEndianStreamToNative(imageTemp); 695 result._imageData = new MonochromeImageData!ushort(reinterpretCast!ushort(imageTemp), result.width, result.height, 696 result.getPixelFormat, result.getBitdepth); 697 break; 698 default: throw new ImageFileException("Unsupported image format!"); 699 } 700 return result; 701 } 702 /** 703 * Decompresses a block of graphics data. 704 */ 705 /** 706 * Reconstructs a scanline from `Sub` filtering. 707 */ 708 protected static ubyte[] reconstructScanlineSub(ubyte[] target, int bytedepth) @safe nothrow { 709 for (size_t i ; i < target.length ; i++) { 710 const ubyte a = i >= bytedepth ? target[i - bytedepth] : 0; 711 target[i] += a; 712 } 713 return target; 714 } 715 /** 716 * Reconstructs a scanline from `Up` filtering. 717 */ 718 protected static ubyte[] reconstructScanlineUp(ubyte[] target, ubyte[] prevScanline) @safe nothrow { 719 assert(target.length == prevScanline.length); 720 for (size_t i ; i < target.length ; i++) { 721 target[i] += prevScanline[i]; 722 } 723 return target; 724 } 725 /** 726 * Reconstructs a scanline from `Average` filtering 727 */ 728 protected static ubyte[] reconstructScanlineAverage(ubyte[] target, ubyte[] prevScanline, int bytedepth) @safe nothrow { 729 assert(target.length == prevScanline.length); 730 for (size_t i ; i < target.length ; i++) { 731 const uint a = i >= bytedepth ? target[i - bytedepth] : 0; 732 target[i] += cast(ubyte)((a + prevScanline[i])>>>1); 733 734 } 735 return target; 736 } 737 /** 738 * Paeth function for filtering and reconstruction. 739 */ 740 protected static ubyte paethFunc(const ubyte a, const ubyte b, const ubyte c) @safe nothrow { 741 import std.math : abs; 742 743 const int p = a + b - c; 744 const int pa = abs(p - a); 745 const int pb = abs(p - b); 746 const int pc = abs(p - c); 747 748 if (pa <= pb && pa <= pc) return a; 749 else if (pb <= pc) return b; 750 else return c; 751 } 752 /** 753 * Reconstructs a scanline from `Paeth` filtering. 754 */ 755 protected static ubyte[] reconstructScanlinePaeth(ubyte[] target, ubyte[] prevScanline, int bytedepth) @safe nothrow { 756 assert(target.length == prevScanline.length); 757 for (size_t i ; i < target.length ; i++) { 758 const ubyte a = i >= bytedepth ? target[i - bytedepth] : 0, b = prevScanline[i], 759 c = i >= bytedepth ? prevScanline[i - bytedepth] : 0; 760 target[i] += paethFunc(a, b, c); 761 762 } 763 return target; 764 } 765 /** 766 * Saves the file to the disk. 767 * Currently interlaced mode is unsupported. 768 */ 769 public void save(F = std.stdio.File)(ref F file, int compLevel = 6) { 770 RGB16_16_16BE fromNativeToBE(RGB16_16_16BE val) @nogc { 771 version(LittleEndian) { 772 val.r = swapEndian(val.r); 773 val.g = swapEndian(val.g); 774 val.b = swapEndian(val.b); 775 } 776 return val; 777 } 778 ubyte[] crc; 779 //ubyte[] paletteData; 780 //if (_palette) paletteData = _palette.raw; 781 ubyte[] imageTemp0 = _imageData.raw, imageTemp; 782 ubyte[] scanline, prevScanline; 783 prevScanline.length = pitch; //First filter should be either none or sub, but not all writers are obeying the standard 784 int wordlength = getBitdepth > 8 ? getBitdepth / 8 : 1; 785 for (uint y ; y < height ; y++) { 786 scanline = imageTemp0[(y * pitch)..((y + 1) * pitch)]; 787 //version (unittest) std.stdio.writeln(scanline.ptr,";",prevScanline.ptr); 788 switch(filterBytes[y]) { 789 case FilterType.Sub: 790 imageTemp ~= filterScanlineSub(scanline, wordlength); 791 break; 792 case FilterType.Up: 793 imageTemp ~= filterScanlineUp(scanline, prevScanline); 794 break; 795 case FilterType.Average: 796 imageTemp ~= filterScanlineAverage(scanline, prevScanline, wordlength); 797 break; 798 case FilterType.Paeth: 799 imageTemp ~= filterScanlinePaeth(scanline, prevScanline, wordlength); 800 break; 801 default: 802 imageTemp ~= scanline; 803 break; 804 } 805 prevScanline = scanline; 806 } 807 assert(imageTemp0.length == imageTemp.length, "Image processing buffer error!"); 808 if (header.bitDepth == 16) { 809 ushort[] arr = nativeStreamToBigEndian!ushort(reinterpretCast!ushort(imageTemp)); 810 imageTemp = reinterpretCast!ubyte(arr); 811 } 812 //write PNG signature into file 813 file.rawWrite(PNG_SIGNATURE); 814 //write Header into file 815 void[] writeBuffer; 816 //writeBuffer.length = 8; 817 writeBuffer = cast(void[])[Chunk(header.sizeof, ChunkInitializers.Header).nativeToBigEndian]; 818 file.rawWrite(writeBuffer); 819 //writeBuffer.length = 0; 820 writeBuffer = cast(void[])[header.nativeToBigEndian]; 821 file.rawWrite(writeBuffer); 822 crc = crc32Of((cast(ubyte[])ChunkInitializers.Header) ~ writeBuffer).dup.reverse; 823 file.rawWrite(crc); 824 //write any ancilliary chunks into the file that needs to stand before the palette 825 foreach (curChunk; ancillaryChunks){ 826 if (curChunk.pos == EmbeddedData.DataPosition.BeforePLTE){ 827 writeBuffer = cast(void[])[Chunk(cast(uint)curChunk.data.length, curChunk.identifier).nativeToBigEndian]; 828 file.rawWrite(writeBuffer); 829 file.rawWrite(curChunk.data); 830 crc = crc32Of((cast(ubyte[])curChunk.identifier) ~ curChunk.data).dup.reverse; 831 file.rawWrite(crc); 832 } 833 } 834 //write palette into file if exists 835 if (_palette) { 836 ubyte[] paletteData, transparencyData; 837 if (_palette.paletteFormat & PixelFormat.SeparateAlphaField) { 838 paletteData = _palette.raw[0.._palette.length * 3]; 839 transparencyData = _palette.raw[_palette.length * 3..$]; 840 } else { 841 paletteData = _palette.raw; 842 } 843 writeBuffer = cast(void[])[Chunk(cast(uint)paletteData.length, ChunkInitializers.Palette).nativeToBigEndian]; 844 file.rawWrite(writeBuffer); 845 file.rawWrite(paletteData); 846 crc = crc32Of((cast(ubyte[])ChunkInitializers.Palette) ~ paletteData).dup.reverse; 847 file.rawWrite(crc); 848 if (transparencyData.length) { 849 writeBuffer = cast(void[])[Chunk(cast(uint)transparencyData.length, ChunkInitializers.Transparency).nativeToBigEndian]; 850 file.rawWrite(writeBuffer); 851 file.rawWrite(transparencyData); 852 crc = crc32Of((cast(ubyte[])ChunkInitializers.Transparency) ~ transparencyData).dup.reverse; 853 file.rawWrite(crc); 854 } 855 } 856 if (flags & 1) { 857 ubyte[] transparencyData; 858 RGB16_16_16BE nativeTrns = fromNativeToBE(trns); 859 if(header.colorType == ColorType.Greyscale) { 860 transparencyData = reinterpretAsArray!ubyte(nativeTrns.r); 861 } else { 862 transparencyData = reinterpretAsArray!ubyte(nativeTrns); 863 } 864 writeBuffer = cast(void[])[Chunk(cast(uint)transparencyData.length, ChunkInitializers.Transparency).nativeToBigEndian]; 865 file.rawWrite(writeBuffer); 866 file.rawWrite(transparencyData); 867 crc = crc32Of((cast(ubyte[])ChunkInitializers.Transparency) ~ transparencyData).dup.reverse; 868 file.rawWrite(crc); 869 } 870 if (bkgIndex != -1) { 871 if (bkgIndex == -2) { 872 ubyte[] transparencyData; 873 RGB16_16_16BE nativeTrns = fromNativeToBE(trns); 874 if(header.colorType == ColorType.Greyscale) { 875 transparencyData = reinterpretAsArray!ubyte(nativeTrns.r); 876 } else { 877 transparencyData = reinterpretAsArray!ubyte(nativeTrns); 878 } 879 writeBuffer = cast(void[])[Chunk(cast(uint)transparencyData.length, ChunkInitializers.Background).nativeToBigEndian]; 880 file.rawWrite(writeBuffer); 881 file.rawWrite(transparencyData); 882 crc = crc32Of((cast(ubyte[])ChunkInitializers.Transparency) ~ transparencyData).dup.reverse; 883 file.rawWrite(crc); 884 } else { 885 writeBuffer = cast(void[])[Chunk(cast(uint)ubyte.sizeof, ChunkInitializers.Background).nativeToBigEndian]; 886 file.rawWrite(writeBuffer); 887 writeBuffer = reinterpretAsArray!void(cast(ubyte)bkgIndex); 888 file.rawWrite(writeBuffer); 889 crc = crc32Of((cast(ubyte[])ChunkInitializers.Background) ~ cast(ubyte[])writeBuffer).dup.reverse; 890 file.rawWrite(crc); 891 } 892 } 893 //write any ancilliary chunks into the file that needs to stand before the imagedata 894 foreach (curChunk; ancillaryChunks){ 895 if (curChunk.pos == EmbeddedData.DataPosition.BeforeIDAT){ 896 writeBuffer = cast(void[])[Chunk(cast(uint)curChunk.data.length, curChunk.identifier).nativeToBigEndian]; 897 file.rawWrite(writeBuffer); 898 file.rawWrite(curChunk.data); 899 crc = crc32Of((cast(ubyte[])curChunk.identifier) ~ curChunk.data).dup.reverse; 900 file.rawWrite(crc); 901 } 902 } 903 //compress imagedata if needed, then write it into the file 904 { 905 int ret; 906 ubyte[] input, output; 907 input.reserve(imageTemp.length + filterBytes.length); 908 for (int line ; line < height ; line++){ 909 const size_t offset = line * pitch; 910 input ~= filterBytes[line]; 911 input ~= imageTemp[offset..offset+pitch]; 912 } 913 914 zlib.z_stream strm; 915 ret = zlib.deflateInit(&strm, compLevel); 916 if (ret != zlib.Z_OK) 917 throw new Exception("Compressor initialization error"); 918 output.length = 32 * 1024;//cast(uint)imageTemp.length; 919 strm.next_in = input.ptr; 920 strm.avail_in = cast(uint)input.length; 921 strm.next_out = output.ptr; 922 strm.avail_out = cast(uint)output.length; 923 do { 924 //std.stdio.writeln(ret, ";", strm.avail_in, ";", strm.total_in, ";", strm.total_out); 925 if (!strm.avail_out) { 926 writeBuffer = cast(void[])[Chunk(cast(uint)output.length, ChunkInitializers.Data).nativeToBigEndian]; 927 file.rawWrite(writeBuffer); 928 file.rawWrite(output); 929 crc = crc32Of((cast(ubyte[])ChunkInitializers.Data) ~ output).dup.reverse; 930 file.rawWrite(crc); 931 strm.next_out = output.ptr; 932 strm.avail_out = cast(uint)output.length; 933 } 934 ret = zlib.deflate(&strm, zlib.Z_FINISH); 935 //std.stdio.writeln(ret, ";", strm.avail_out); 936 if(ret < 0){ 937 zlib.deflateEnd(&strm); 938 throw new Exception("Compressor output error: " ~ cast(string)std..string.fromStringz(strm.msg)); 939 } 940 } while (ret != zlib.Z_STREAM_END); 941 //write any ancilliary chunks into the file that needs to stand within the imagedata 942 foreach (curChunk; ancillaryChunks){ 943 if (curChunk.pos == EmbeddedData.DataPosition.WithinIDAT){ 944 writeBuffer = cast(void[])[Chunk(cast(uint)curChunk.data.length, curChunk.identifier).nativeToBigEndian]; 945 file.rawWrite(writeBuffer); 946 file.rawWrite(curChunk.data); 947 crc = crc32Of((cast(ubyte[])curChunk.identifier) ~ curChunk.data).dup.reverse; 948 file.rawWrite(crc); 949 } 950 } 951 if (strm.avail_out != output.length) { 952 writeBuffer = cast(void[])[Chunk(cast(uint)(output.length - strm.avail_out), ChunkInitializers.Data).nativeToBigEndian]; 953 file.rawWrite(writeBuffer); 954 file.rawWrite(output[0..$-strm.avail_out]); 955 crc = crc32Of((cast(ubyte[])ChunkInitializers.Data) ~ output[0..$-strm.avail_out]).dup.reverse; 956 file.rawWrite(crc); 957 } 958 zlib.deflateEnd(&strm); 959 } 960 //write any ancilliary chunks into the file that needs to stand after the imagedata 961 foreach (curChunk; ancillaryChunks){ 962 if (curChunk.pos == EmbeddedData.DataPosition.AfterIDAT){ 963 writeBuffer = cast(void[])[Chunk(cast(uint)curChunk.data.length, curChunk.identifier).nativeToBigEndian]; 964 file.rawWrite(writeBuffer); 965 file.rawWrite(curChunk.data); 966 crc = crc32Of((cast(ubyte[])curChunk.identifier) ~ curChunk.data).dup.reverse; 967 file.rawWrite(crc); 968 } 969 } 970 //write IEND chunk 971 //writeBuffer.length = 0; 972 writeBuffer = cast(void[])[Chunk(0, ChunkInitializers.End).nativeToBigEndian]; 973 file.rawWrite(writeBuffer); 974 file.rawWrite(PNG_CLOSER); 975 } 976 /** 977 * Encodes a scanline using `Sub` filter. 978 */ 979 protected static ubyte[] filterScanlineSub(ubyte[] target, int bytedepth) @safe nothrow { 980 ubyte[] result; 981 result.length = target.length; 982 for (size_t i ; i < target.length ; i++) { 983 const ubyte a = i >= bytedepth ? target[i - bytedepth] : 0; 984 result[i] = cast(ubyte)(target[i] - a); 985 } 986 return result; 987 } 988 /** 989 * Encodes a scanline using `Up` filter. 990 */ 991 protected static ubyte[] filterScanlineUp(ubyte[] target, ubyte[] prevScanline) @safe nothrow { 992 ubyte[] result; 993 result.length = target.length; 994 for (size_t i ; i < target.length ; i++) { 995 result[i] = cast(ubyte)(target[i] - prevScanline[i]); 996 } 997 return result; 998 } 999 /** 1000 * Encodes a scanline using `Average` filter. 1001 */ 1002 protected static ubyte[] filterScanlineAverage(ubyte[] target, ubyte[] prevScanline, int bytedepth) @safe nothrow { 1003 ubyte[] result; 1004 result.length = target.length; 1005 for (size_t i ; i < target.length ; i++) { 1006 const uint a = i >= bytedepth ? target[i - bytedepth] : 0; 1007 result[i] = cast(ubyte)(target[i] - ((a + prevScanline[i])>>>1)); 1008 } 1009 return result; 1010 } 1011 /** 1012 * Encodes a scanline using `Paeth` filter. 1013 */ 1014 protected static ubyte[] filterScanlinePaeth(ubyte[] target, ubyte[] prevScanline, int bytedepth) @safe nothrow { 1015 ubyte[] result; 1016 result.length = target.length; 1017 for (size_t i ; i < target.length ; i++) { 1018 const ubyte a = i >= bytedepth ? target[i - bytedepth] : 0, b = prevScanline[i], 1019 c = i >= bytedepth ? prevScanline[i - bytedepth] : 0; 1020 result[i] = cast(ubyte)(target[i] - paethFunc(a, b, c)); 1021 1022 } 1023 return result; 1024 } 1025 /** 1026 * Adds an ancillary chunk to the PNG file 1027 */ 1028 public void addAncilliaryChunk(EmbeddedData.DataPosition pos, char[4] identifier, ubyte[] data){ 1029 ancillaryChunks ~= new EmbeddedData(pos, identifier, data); 1030 } 1031 override uint width() @nogc @safe @property const pure{ 1032 return header.width; 1033 } 1034 override uint height() @nogc @safe @property const pure{ 1035 return header.height; 1036 } 1037 override bool isIndexed() @nogc @safe @property const pure{ 1038 return header.colorType == ColorType.Indexed; 1039 } 1040 override ubyte getBitdepth() @nogc @safe @property const pure{ 1041 switch(header.colorType){ 1042 case ColorType.GreyscaleWithAlpha: 1043 return cast(ubyte)(header.bitDepth * 2); 1044 case ColorType.TrueColor: 1045 return cast(ubyte)(header.bitDepth * 3); 1046 case ColorType.TrueColorWithAlpha: 1047 return cast(ubyte)(header.bitDepth * 4); 1048 default: 1049 return header.bitDepth; 1050 } 1051 } 1052 override ubyte getPaletteBitdepth() @nogc @safe @property const pure{ 1053 if (_palette) return _palette.bitDepth; 1054 else return 0; 1055 } 1056 override uint getPixelFormat() @nogc @safe @property const pure{ 1057 switch (header.colorType){ 1058 case ColorType.Indexed: 1059 switch (header.bitDepth) { 1060 case 1: return PixelFormat.Indexed1Bit; 1061 case 2: return PixelFormat.Indexed2Bit; 1062 case 4: return PixelFormat.Indexed4Bit; 1063 case 8: return PixelFormat.Indexed8Bit; 1064 default: return PixelFormat.Undefined; 1065 } 1066 case ColorType.GreyscaleWithAlpha: 1067 if (header.bitDepth) return PixelFormat.YA88 | PixelFormat.BigEndian; 1068 else return PixelFormat.YA16_16 | PixelFormat.BigEndian; 1069 case ColorType.Greyscale: 1070 switch (header.bitDepth) { 1071 case 1: return PixelFormat.Grayscale1Bit; 1072 case 2: return PixelFormat.Grayscale2Bit; 1073 case 4: return PixelFormat.Grayscale4Bit; 1074 case 8: return PixelFormat.Grayscale8Bit; 1075 case 16: return PixelFormat.Grayscale16Bit; 1076 default: return PixelFormat.Undefined; 1077 } 1078 case ColorType.TrueColor: 1079 switch (header.bitDepth) { 1080 case 8: return PixelFormat.RGB888 | PixelFormat.BigEndian; 1081 case 16: return PixelFormat.RGB16_16_16 | PixelFormat.BigEndian; 1082 default: return PixelFormat.RGBX5551; 1083 } 1084 case ColorType.TrueColorWithAlpha: 1085 switch (header.bitDepth) { 1086 case 8: return PixelFormat.RGBA8888 | PixelFormat.BigEndian | PixelFormat.ValidAlpha; 1087 case 16: return PixelFormat.RGBA16_16_16_16 | PixelFormat.BigEndian | PixelFormat.ValidAlpha; 1088 default: return PixelFormat.Undefined; 1089 } 1090 default: return PixelFormat.Undefined; 1091 } 1092 } 1093 override uint getPalettePixelFormat() @nogc @safe @property const pure{ 1094 if (_palette) return _palette.paletteFormat; 1095 else return PixelFormat.Undefined; 1096 } 1097 /** 1098 * Returns the header. 1099 */ 1100 public ref Header getHeader() @nogc @safe pure{ 1101 return header; 1102 } 1103 1104 public uint getCurrentImage() @safe pure { 1105 return uint.init; // TODO: implement 1106 } 1107 1108 public uint setCurrentImage(uint frame) @safe pure { 1109 return uint.init; // TODO: implement 1110 } 1111 ///Sets the current image to the static if available 1112 public void setStaticImage() @safe pure { 1113 1114 } 1115 public uint nOfImages() @property @safe @nogc pure const { 1116 return cast(uint)(frames.length + 1); 1117 } 1118 1119 public uint frameTime() @property @safe @nogc pure const { 1120 return uint.init; // TODO: implement 1121 } 1122 1123 public bool isAnimation() @property @safe @nogc pure const { 1124 return frames.length ? true : false; 1125 } 1126 1127 public string getMetadata(string id) @safe pure { 1128 foreach (Text key; textData) { 1129 if (key.keyword == id) 1130 return key.text; 1131 } 1132 return null; 1133 } 1134 1135 public string setMetadata(string id, string val) @safe pure { 1136 foreach (ref Text key; textData) { 1137 if (key.keyword == id) 1138 return key.text = val; 1139 } 1140 textData ~= Text (id, val, null, null, 0, 0); 1141 return val; 1142 } 1143 1144 public string getID() @safe pure { 1145 return getMetadata("Title"); 1146 } 1147 1148 public string getAuthor() @safe pure { 1149 return getMetadata("Author"); 1150 } 1151 1152 public string getComment() @safe pure { 1153 return getMetadata("Comment"); 1154 } 1155 1156 public string getJobName() @safe pure { 1157 return getMetadata("Job Name"); 1158 } 1159 1160 public string getSoftwareInfo() @safe pure { 1161 return getMetadata("Software"); 1162 } 1163 1164 public string getSoftwareVersion() @safe pure { 1165 return getMetadata("Software Version"); 1166 } 1167 1168 public string getDescription() @safe pure { 1169 return getMetadata("Description"); 1170 } 1171 1172 public string getSource() @safe pure { 1173 return getMetadata("Source"); 1174 } 1175 1176 public string getCopyright() @safe pure { 1177 return getMetadata("Copyright"); 1178 } 1179 1180 public string getCreationTimeStr() @safe pure { 1181 return getMetadata("Creation Time"); 1182 } 1183 1184 public string setID(string val) @safe pure { 1185 return setMetadata("Title", val); 1186 } 1187 1188 public string setAuthor(string val) @safe pure { 1189 return setMetadata("Author", val); 1190 } 1191 1192 public string setComment(string val) @safe pure { 1193 return setMetadata("Comment", val); 1194 } 1195 1196 public string setJobName(string val) @safe pure { 1197 return setMetadata("Job Name", val); 1198 } 1199 1200 public string setSoftwareInfo(string val) @safe pure { 1201 return setMetadata("Software", val); 1202 } 1203 1204 public string setSoftwareVersion(string val) @safe pure { 1205 return setMetadata("Software Version", val); 1206 } 1207 1208 public string setDescription(string val) @safe pure { 1209 return setMetadata("Description", val); 1210 } 1211 1212 public string setSource(string val) @safe pure { 1213 return setMetadata("Source", val); 1214 } 1215 1216 public string setCopyright(string val) @safe pure { 1217 return setMetadata("Copyright", val); 1218 } 1219 1220 public string setCreationTime(string val) @safe pure { 1221 return setMetadata("Creation Time", val); 1222 } 1223 1224 1225 1226 } 1227 1228 unittest{ 1229 import vfile; 1230 import dimage.tga; 1231 { 1232 std.stdio.File indexedPNGFile = std.stdio.File("./test/png/MARBLE24.PNG"); 1233 std.stdio.writeln("Loading ", indexedPNGFile.name); 1234 PNG a = PNG.load(indexedPNGFile); 1235 std.stdio.writeln("File `", indexedPNGFile.name, "` successfully loaded"); 1236 assert(a.getBitdepth == 24, "Bitdepth error!"); 1237 //std.stdio.File output = std.stdio.File("./test/png/output.png", "wb"); 1238 //a.save(output); 1239 VFile virtualIndexedPNGFile; 1240 a.save(virtualIndexedPNGFile); 1241 std.stdio.writeln("Successfully saved to virtual file ", virtualIndexedPNGFile.size); 1242 virtualIndexedPNGFile.seek(0); 1243 PNG b = PNG.load(virtualIndexedPNGFile); 1244 std.stdio.writeln("Image restored from virtual file"); 1245 compareImages(a, b); 1246 std.stdio.writeln("The two images' output match"); 1247 } 1248 /+{ 1249 std.stdio.File indexedPNGFile = std.stdio.File("./test/png/MARBLE8.png"); 1250 std.stdio.writeln("Loading ", indexedPNGFile.name); 1251 PNG a = PNG.load(indexedPNGFile); 1252 std.stdio.writeln("File `", indexedPNGFile.name, "` successfully loaded"); 1253 //std.stdio.writeln(a.filterBytes); 1254 //std.stdio.File output = std.stdio.File("./test/png/output.png", "wb"); 1255 //a.save(output); 1256 VFile virtualIndexedPNGFile; 1257 a.save(virtualIndexedPNGFile); 1258 std.stdio.writeln("Successfully saved to virtual file ", virtualIndexedPNGFile.size); 1259 virtualIndexedPNGFile.seek(0); 1260 PNG b = PNG.load(virtualIndexedPNGFile); 1261 std.stdio.writeln("Image restored from virtual file"); 1262 compareImages(a, b); 1263 std.stdio.writeln("The two images' output match"); 1264 }+/ 1265 //test indexed png files and their palettes 1266 { 1267 std.stdio.File indexedPNGFile = std.stdio.File("./test/png/sci-fi-tileset.png"); 1268 std.stdio.writeln("Loading ", indexedPNGFile.name); 1269 PNG a = PNG.load(indexedPNGFile); 1270 std.stdio.writeln("File `", indexedPNGFile.name, "` successfully loaded"); 1271 //std.stdio.File output = std.stdio.File("./test/png/output.png", "wb"); 1272 //a.save(output); 1273 IPalette p = a.palette; 1274 assert(p.convTo(PixelFormat.ARGB8888).length == p.length); 1275 assert(p.convTo(PixelFormat.ARGB8888 | PixelFormat.BigEndian).length == p.length); 1276 } 1277 //test loading and saving multiple images 1278 const string[] filenames = ["basn0g01.png", "basn0g02.png", "basn0g04.png", "basn0g08.png", "basn0g16.png", 1279 "basn2c08.png", "basn2c16.png", "basn3p01.png", "basn3p02.png", "basn3p04.png", "basn3p08.png", 1280 "basn4a08.png", "basn4a16.png", "basn6a08.png", "basn6a16.png"]; 1281 foreach (fn ; filenames) { 1282 std.stdio.File sourceFile = std.stdio.File("./test/png/" ~ fn); 1283 std.stdio.writeln("Loading ", sourceFile.name); 1284 PNG a = PNG.load(sourceFile); 1285 std.stdio.writeln("File `", sourceFile.name, "` successfully loaded"); 1286 //std.stdio.writeln(a.filterBytes); 1287 //std.stdio.File output = std.stdio.File("./test/png/output_" ~ fn, "wb"); 1288 //a.save(output); 1289 VFile virtualPNGFile; 1290 a.save(virtualPNGFile); 1291 std.stdio.writeln("Successfully saved to virtual file ", virtualPNGFile.size); 1292 virtualPNGFile.seek(0); 1293 PNG b = PNG.load(virtualPNGFile); 1294 std.stdio.writeln("Image restored from virtual file"); 1295 compareImages(a, b); 1296 std.stdio.writeln("The two images' output match"); 1297 std.stdio.writeln("Reconstructing PNG from image data"); 1298 PNG c = new PNG(a.imageData, a.palette); 1299 compareImages(a, c); 1300 virtualPNGFile = VFile.init; 1301 c.save(virtualPNGFile); 1302 //output.rewind(); 1303 virtualPNGFile.seek(0); 1304 b = PNG.load(virtualPNGFile); 1305 std.stdio.writeln("Image restored from virtual file"); 1306 compareImages(a, b); 1307 std.stdio.writeln("The two images' output match"); 1308 } 1309 //test against TGA versions of the same images 1310 /* { 1311 std.stdio.File tgaSource = std.stdio.File("./test/tga/mapped_8.tga"); 1312 std.stdio.File pngSource = std.stdio.File("./test/png/mapped_8.png"); 1313 std.stdio.writeln("Loading ", tgaSource.name); 1314 TGA tgaImage = TGA.load(tgaSource); 1315 std.stdio.writeln("Loading ", pngSource.name); 1316 PNG pngImage = PNG.load(pngSource); 1317 compareImages!true(tgaImage, pngImage); 1318 } */ 1319 { 1320 std.stdio.File tgaSource = std.stdio.File("./test/tga/truecolor_24.tga"); 1321 std.stdio.File pngSource = std.stdio.File("./test/png/truecolor_24.png"); 1322 std.stdio.writeln("Loading ", tgaSource.name); 1323 TGA tgaImage = TGA.load(tgaSource); 1324 std.stdio.writeln("Loading ", pngSource.name); 1325 PNG pngImage = PNG.load(pngSource); 1326 compareImages!true(tgaImage, pngImage); 1327 } 1328 { 1329 std.stdio.File tgaSource = std.stdio.File("./test/tga/truecolor_32.tga"); 1330 std.stdio.File pngSource = std.stdio.File("./test/png/truecolor_32.png"); 1331 std.stdio.writeln("Loading ", tgaSource.name); 1332 TGA tgaImage = TGA.load(tgaSource); 1333 std.stdio.writeln("Loading ", pngSource.name); 1334 PNG pngImage = PNG.load(pngSource); 1335 compareImages!true(tgaImage, pngImage); 1336 } 1337 }