1 /++ 2 Support for [https://wiki.mozilla.org/APNG_Specification|animated png] files. 3 4 $(WARNING Please note this interface is not exactly stable and may break with minimum notice.) 5 6 History: 7 Originally written March 2019 with read support. 8 9 Render support added December 28, 2020. 10 11 Write support added February 27, 2021. 12 +/ 13 module arsd.apng; 14 15 /// Demo creating one from scratch 16 unittest { 17 import arsd.apng; 18 19 void main() { 20 auto apng = new ApngAnimation(50, 50); 21 22 auto frame = apng.addFrame(25, 25); 23 frame.data[] = 255; 24 25 frame = apng.addFrame(25, 25); 26 frame.data[] = 255; 27 frame.frameControlChunk.delay_num = 10; 28 29 frame = apng.addFrame(25, 25); 30 frame.data[] = 255; 31 frame.frameControlChunk.x_offset = 25; 32 frame.frameControlChunk.delay_num = 10; 33 34 frame = apng.addFrame(25, 25); 35 frame.data[] = 255; 36 frame.frameControlChunk.y_offset = 25; 37 frame.frameControlChunk.delay_num = 10; 38 39 frame = apng.addFrame(25, 25); 40 frame.data[] = 255; 41 frame.frameControlChunk.x_offset = 25; 42 frame.frameControlChunk.y_offset = 25; 43 frame.frameControlChunk.delay_num = 10; 44 45 46 writeApngToFile(apng, "/home/me/test.apng"); 47 } 48 49 version(Demo) main(); // exclude from docs 50 } 51 52 /// Demo reading and rendering 53 unittest { 54 import arsd.simpledisplay; 55 import arsd.game; 56 import arsd.apng; 57 58 void main(string[] args) { 59 import std.file; 60 auto a = readApng(cast(ubyte[]) std.file.read(args[1])); 61 62 auto window = create2dWindow("Animated PNG viewer", a.header.width, a.header.height); 63 64 auto render = a.renderer(); 65 OpenGlTexture[] frames; 66 int[] waits; 67 foreach(frame; a.frames) { 68 waits ~= render.nextFrame(); 69 // this would be the raw data for the frame 70 //frames ~= new OpenGlTexture(frame.frameData.getAsTrueColorImage); 71 // or the current rendered ersion 72 frames ~= new OpenGlTexture(render.buffer); 73 } 74 75 int pos; 76 int currentWait; 77 78 void update() { 79 currentWait += waits[pos]; 80 pos++; 81 if(pos == frames.length) 82 pos = 0; 83 } 84 85 window.redrawOpenGlScene = () { 86 glClear(GL_COLOR_BUFFER_BIT); 87 frames[pos].draw(0, 0); 88 }; 89 90 auto tick = 50; 91 window.eventLoop(tick, delegate() { 92 currentWait -= tick; 93 auto updateNeeded = currentWait <= 0; 94 while(currentWait <= 0) 95 update(); 96 if(updateNeeded) 97 window.redrawOpenGlSceneNow(); 98 //}, 99 //(KeyEvent ev) { 100 //if(ev.pressed) 101 }); 102 103 // writeApngToFile(a, "/home/me/test.apng"); 104 } 105 106 version(Demo) main(["", "/home/me/test.apng"]); // exclude from docs 107 //version(Demo) main(["", "/home/me/small-clouds.png"]); // exclude from docs 108 } 109 110 import arsd.png; 111 112 // must be in the file before the IDAT 113 /// acTL chunk direct representation 114 struct AnimationControlChunk { 115 uint num_frames; 116 uint num_plays; 117 118 /// Adds it to a chunk payload buffer, returning the slice of `buffer` actually used 119 /// Used internally by the [writeApngToFile] family of functions. 120 ubyte[] toChunkPayload(ubyte[] buffer) 121 in { assert(buffer.length >= 8); } 122 do { 123 int offset = 0; 124 buffer[offset++] = (num_frames >> 24) & 0xff; 125 buffer[offset++] = (num_frames >> 16) & 0xff; 126 buffer[offset++] = (num_frames >> 8) & 0xff; 127 buffer[offset++] = (num_frames >> 0) & 0xff; 128 129 buffer[offset++] = (num_plays >> 24) & 0xff; 130 buffer[offset++] = (num_plays >> 16) & 0xff; 131 buffer[offset++] = (num_plays >> 8) & 0xff; 132 buffer[offset++] = (num_plays >> 0) & 0xff; 133 134 return buffer[0 .. offset]; 135 } 136 } 137 138 /// fcTL chunk direct representation 139 struct FrameControlChunk { 140 align(1): 141 // this should go up each time, for frame control AND for frame data, each increases. 142 uint sequence_number; 143 uint width; 144 uint height; 145 uint x_offset; 146 uint y_offset; 147 ushort delay_num; 148 ushort delay_den; 149 APNG_DISPOSE_OP dispose_op; 150 APNG_BLEND_OP blend_op; 151 152 static assert(dispose_op.offsetof == 24); 153 static assert(blend_op.offsetof == 25); 154 155 ubyte[] toChunkPayload(int sequenceNumber, ubyte[] buffer) 156 in { assert(buffer.length >= typeof(this).sizeof); } 157 do { 158 int offset = 0; 159 160 sequence_number = sequenceNumber; 161 162 buffer[offset++] = (sequence_number >> 24) & 0xff; 163 buffer[offset++] = (sequence_number >> 16) & 0xff; 164 buffer[offset++] = (sequence_number >> 8) & 0xff; 165 buffer[offset++] = (sequence_number >> 0) & 0xff; 166 167 buffer[offset++] = (width >> 24) & 0xff; 168 buffer[offset++] = (width >> 16) & 0xff; 169 buffer[offset++] = (width >> 8) & 0xff; 170 buffer[offset++] = (width >> 0) & 0xff; 171 172 buffer[offset++] = (height >> 24) & 0xff; 173 buffer[offset++] = (height >> 16) & 0xff; 174 buffer[offset++] = (height >> 8) & 0xff; 175 buffer[offset++] = (height >> 0) & 0xff; 176 177 buffer[offset++] = (x_offset >> 24) & 0xff; 178 buffer[offset++] = (x_offset >> 16) & 0xff; 179 buffer[offset++] = (x_offset >> 8) & 0xff; 180 buffer[offset++] = (x_offset >> 0) & 0xff; 181 182 buffer[offset++] = (y_offset >> 24) & 0xff; 183 buffer[offset++] = (y_offset >> 16) & 0xff; 184 buffer[offset++] = (y_offset >> 8) & 0xff; 185 buffer[offset++] = (y_offset >> 0) & 0xff; 186 187 buffer[offset++] = (delay_num >> 8) & 0xff; 188 buffer[offset++] = (delay_num >> 0) & 0xff; 189 190 buffer[offset++] = (delay_den >> 8) & 0xff; 191 buffer[offset++] = (delay_den >> 0) & 0xff; 192 193 buffer[offset++] = cast(ubyte) dispose_op; 194 buffer[offset++] = cast(ubyte) blend_op; 195 196 return buffer[0 .. offset]; 197 } 198 } 199 200 /++ 201 Represents a single frame from the file, directly corresponding to the fcTL and fdAT data from the file. 202 +/ 203 class ApngFrame { 204 205 ApngAnimation parent; 206 207 this(ApngAnimation parent) { 208 this.parent = parent; 209 } 210 211 this(ApngAnimation parent, int width, int height) { 212 this.parent = parent; 213 frameControlChunk.width = width; 214 frameControlChunk.height = height; 215 216 if(parent.header.type == 3) { // FIXME: other types?! 217 auto ii = new IndexedImage(width, height); 218 ii.palette = parent.palette; 219 frameData = ii; 220 data = ii.data; 221 } else { 222 auto tci = new TrueColorImage(width, height); 223 frameData = tci; 224 data = tci.imageData.bytes; 225 } 226 } 227 228 void resyncData() { 229 if(frameData is null) 230 populateData(); 231 232 assert(frameData !is null); 233 assert(frameData.width == frameControlChunk.width); 234 assert(frameData.height == frameControlChunk.height); 235 236 if(auto tci = cast(TrueColorImage) frameData) { 237 data = tci.imageData.bytes; 238 assert(parent.header.type == 6); 239 } else if(auto ii = cast(IndexedImage) frameData) { 240 data = ii.data; 241 assert(parent.header.type == 3); 242 assert(ii.palette == parent.palette); 243 } 244 } 245 246 /++ 247 You're allowed to edit these values but remember it is your responsibility to keep 248 it consistent with the rest of the file (at least for now, I might change this in the future). 249 +/ 250 FrameControlChunk frameControlChunk; 251 252 private ubyte[] compressedDatastream; /// Raw datastream from the file. 253 254 /++ 255 A reference to frameData's bytes. May be 8 bit if indexed or 32 bit rgba if not. 256 257 Do not replace this reference but you may edit the content. 258 +/ 259 ubyte[] data; 260 261 /++ 262 Processed frame data as an image. only set after you call populateData. 263 264 You are allowed to edit the bytes on this but don't change the width/height or palette. Also don't replace the object. 265 266 This also means `getAsTrueColorImage` is not that useful, instead cast to [IndexedImage] or [TrueColorImage] depending 267 on your type. 268 +/ 269 MemoryImage frameData; 270 /++ 271 Loads the raw [compressedDatastream] into raw uncompressed [data] and processed [frameData] 272 +/ 273 void populateData() { 274 if(data !is null) 275 return; 276 277 import std.zlib; 278 279 auto raw = cast(ubyte[]) uncompress(compressedDatastream); 280 auto bpp = bytesPerPixel(parent.header); 281 282 auto width = frameControlChunk.width; 283 auto height = frameControlChunk.height; 284 285 auto bytesPerLine = bytesPerLineOfPng(parent.header.depth, parent.header.type, width); 286 bytesPerLine--; // removing filter byte from this calculation since we handle separately 287 288 size_t idataIdx; 289 ubyte[] idata; 290 291 MemoryImage img; 292 if(parent.header.type == 3) { 293 auto i = new IndexedImage(width, height); 294 img = i; 295 i.palette = parent.palette; 296 idata = i.data; 297 } else { // FIXME: other types?! 298 auto i = new TrueColorImage(width, height); 299 img = i; 300 idata = i.imageData.bytes; 301 } 302 303 immutable(ubyte)[] previousLine; 304 foreach(y; 0 .. height) { 305 auto filter = raw[0]; 306 raw = raw[1 .. $]; 307 auto line = raw[0 .. bytesPerLine]; 308 raw = raw[bytesPerLine .. $]; 309 310 auto unfiltered = unfilter(filter, line, previousLine, bpp); 311 previousLine = unfiltered; 312 313 convertPngData(parent.header.type, parent.header.depth, unfiltered, width, idata, idataIdx); 314 } 315 316 this.data = idata; 317 this.frameData = img; 318 } 319 } 320 321 /++ 322 323 +/ 324 struct ApngRenderBuffer { 325 /// Load this yourself 326 ApngAnimation animation; 327 328 /// Then these are populated when you call [nextFrame] 329 public TrueColorImage buffer; 330 /// ditto 331 public int frameNumber; 332 333 private FrameControlChunk prevFcc; 334 private TrueColorImage[] convertedFrames; 335 private TrueColorImage previousFrame; 336 337 /++ 338 Returns number of millisecond to wait until the next frame and populates [buffer] and [frameNumber]. 339 +/ 340 int nextFrame() { 341 if(frameNumber == animation.frames.length) { 342 frameNumber = 0; 343 prevFcc = FrameControlChunk.init; 344 } 345 346 auto frame = animation.frames[frameNumber]; 347 auto fcc = frame.frameControlChunk; 348 if(convertedFrames is null) { 349 convertedFrames = new TrueColorImage[](animation.frames.length); 350 } 351 if(convertedFrames[frameNumber] is null) { 352 frame.populateData(); 353 convertedFrames[frameNumber] = frame.frameData.getAsTrueColorImage(); 354 } 355 356 final switch(prevFcc.dispose_op) { 357 case APNG_DISPOSE_OP.NONE: 358 break; 359 case APNG_DISPOSE_OP.BACKGROUND: 360 // clear area to 0 361 foreach(y; prevFcc.y_offset .. prevFcc.y_offset + prevFcc.height) 362 buffer.imageData.bytes[ 363 4 * (prevFcc.x_offset + y * buffer.width) 364 .. 365 4 * (prevFcc.x_offset + prevFcc.width + y * buffer.width) 366 ] = 0; 367 break; 368 case APNG_DISPOSE_OP.PREVIOUS: 369 // put the buffer back in 370 371 // this could prolly be more efficient, it only really cares about the prevFcc bounding box 372 buffer.imageData.bytes[] = previousFrame.imageData.bytes[]; 373 break; 374 } 375 376 prevFcc = fcc; 377 // should copy the buffer at this point for a PREVIOUS case happening 378 if(fcc.dispose_op == APNG_DISPOSE_OP.PREVIOUS) { 379 // this could prolly be more efficient, it only really cares about the prevFcc bounding box 380 if(previousFrame is null){ 381 previousFrame = buffer.clone(); 382 } else { 383 previousFrame.imageData.bytes[] = buffer.imageData.bytes[]; 384 } 385 } 386 387 size_t foff; 388 foreach(y; fcc.y_offset .. fcc.y_offset + fcc.height) { 389 final switch(fcc.blend_op) { 390 case APNG_BLEND_OP.SOURCE: 391 buffer.imageData.bytes[ 392 4 * (fcc.x_offset + y * buffer.width) 393 .. 394 4 * (fcc.x_offset + y * buffer.width + fcc.width) 395 ] = convertedFrames[frameNumber].imageData.bytes[foff .. foff + fcc.width * 4]; 396 foff += fcc.width * 4; 397 break; 398 case APNG_BLEND_OP.OVER: 399 foreach(x; fcc.x_offset .. fcc.x_offset + fcc.width) { 400 buffer.imageData.colors[y * buffer.width + x] = 401 alphaBlend( 402 convertedFrames[frameNumber].imageData.colors[foff], 403 buffer.imageData.colors[y * buffer.width + x] 404 ); 405 foff++; 406 } 407 break; 408 } 409 } 410 411 frameNumber++; 412 413 if(fcc.delay_den == 0) 414 return fcc.delay_num * 1000 / 100; 415 else 416 return fcc.delay_num * 1000 / fcc.delay_den; 417 } 418 } 419 420 /++ 421 Class that represents an apng file. 422 +/ 423 class ApngAnimation { 424 PngHeader header; 425 AnimationControlChunk acc; 426 Color[] palette; 427 ApngFrame[] frames; 428 // default image? tho i can just load it as a png for that too. 429 430 /++ 431 This is an uninitialized thing, you're responsible for filling in all data yourself. You probably don't want to 432 use this except for use in the `factory` function you pass to [readApng]. 433 +/ 434 this() { 435 436 } 437 438 /++ 439 If palette is null, it is a true color image. If it has data, it is indexed. 440 +/ 441 this(int width, int height, Color[] palette = null) { 442 header.type = (palette !is null) ? 3 : 6; 443 header.width = width; 444 header.height = height; 445 446 this.palette = palette; 447 } 448 449 /++ 450 Adds a frame with the given size and returns the object. You can change other values in the frameControlChunk on it 451 and get the data bytes out of there. 452 +/ 453 ApngFrame addFrame(int width, int height) { 454 assert(width <= header.width); 455 assert(height <= header.height); 456 auto f = new ApngFrame(this, width, height); 457 frames ~= f; 458 acc.num_frames++; 459 return f; 460 } 461 462 // call before writing or trying to render again 463 void resyncData() { 464 acc.num_frames = cast(int) frames.length; 465 foreach(frame; frames) 466 frame.resyncData(); 467 } 468 469 /// 470 ApngRenderBuffer renderer() { 471 return ApngRenderBuffer(this, new TrueColorImage(header.width, header.height), 0); 472 } 473 474 /++ 475 Hook for subclasses to handle custom chunks in the png file as it is loaded by [readApng]. 476 477 Examples: 478 --- 479 override void handleOtherChunkWhenLoading(Chunk chunk) { 480 if(chunk.stype == "mine") { 481 ubyte[] data = chunk.payload; 482 // process it 483 } 484 } 485 --- 486 487 History: 488 Added December 26, 2021 (dub v10.5) 489 +/ 490 protected void handleOtherChunkWhenLoading(Chunk chunk) { 491 // intentionally blank to ignore it since the main function does the whole base functionality 492 } 493 494 /++ 495 Hook for subclasses to add custom chunks to the png file as it is written by [writeApngToData] and [writeApngToFile]. 496 497 Standards: 498 See the png spec for guidelines on how to create non-essential, private chunks in a file: 499 500 http://www.libpng.org/pub/png/spec/1.2/PNG-Encoders.html#E.Use-of-private-chunks 501 502 Examples: 503 --- 504 override createOtherChunksWhenSaving(scope void delegate(Chunk c) sink) { 505 sink(*Chunk.create("mine", [payload, bytes, here])); 506 } 507 --- 508 509 History: 510 Added December 26, 2021 (dub v10.5) 511 +/ 512 protected void createOtherChunksWhenSaving(scope void delegate(Chunk c) sink) { 513 // no other chunks by default 514 515 // I can now do the repeat frame thing for start / cycle / end bits of the animation in the game! 516 } 517 } 518 519 /// 520 enum APNG_DISPOSE_OP : byte { 521 NONE = 0, /// 522 BACKGROUND = 1, /// 523 PREVIOUS = 2 /// 524 } 525 526 /// 527 enum APNG_BLEND_OP : byte { 528 SOURCE = 0, /// 529 OVER = 1 /// 530 } 531 532 /++ 533 Loads an apng file. 534 535 Params: 536 data = the raw data bytes of the file 537 strictApng = if true, it will strictly interpret 538 the file as apng and ignore the default image. If there 539 are no animation chunks, it will return an empty ApngAnimation 540 object. 541 542 If false, it will use the default image as the first 543 (and only) frame of animation if there are no apng chunks. 544 545 factory = factory function for constructing the [ApngAnimation] 546 object the function returns. You can use this to override the 547 allocation pattern or to return a subclass instead, which can handle 548 custom chunks and other things. 549 550 History: 551 Parameter `strictApng` added February 27, 2021 552 Parameter `factory` added December 26, 2021 553 +/ 554 ApngAnimation readApng(in ubyte[] data, bool strictApng = false, scope ApngAnimation delegate() factory = null) { 555 auto png = readPng(data); 556 auto header = PngHeader.fromChunk(png.chunks[0]); 557 558 ApngAnimation obj; 559 if(factory) 560 obj = factory(); 561 else 562 obj = new ApngAnimation(); 563 564 obj.header = header; 565 566 if(header.type == 3) { 567 obj.palette = fetchPalette(png); 568 } 569 570 bool seenIdat = false; 571 bool seenFctl = false; 572 573 int frameNumber; 574 int expectedSequenceNumber = 0; 575 576 bool seenacTL = false; 577 578 foreach(chunk; png.chunks) { 579 switch(chunk.stype) { 580 case "IDAT": 581 582 if(!seenacTL && !strictApng) { 583 // acTL chunks must appear before IDAT per spec, 584 // so if there isn't one by now, it isn't an apng file. 585 // but unless we care about strictApng, we can salvage 586 // by making some dummy data. 587 588 { 589 AnimationControlChunk c; 590 c.num_frames = 1; 591 c.num_plays = 1; 592 593 obj.acc = c; 594 obj.frames = new ApngFrame[](c.num_frames); 595 596 seenacTL = true; 597 } 598 599 { 600 FrameControlChunk c; 601 c.sequence_number = 1; 602 c.width = header.width; 603 c.height = header.height; 604 c.x_offset = 0; 605 c.y_offset = 0; 606 c.delay_num = short.max; 607 c.delay_den = 1; 608 c.dispose_op = APNG_DISPOSE_OP.NONE; 609 c.blend_op = APNG_BLEND_OP.SOURCE; 610 611 seenFctl = true; 612 613 // not increasing expectedSequenceNumber since if something is present, this is malformed! 614 615 if(obj.frames[frameNumber] is null) 616 obj.frames[frameNumber] = new ApngFrame(obj); 617 obj.frames[frameNumber].frameControlChunk = c; 618 619 frameNumber++; 620 } 621 } 622 623 624 seenIdat = true; 625 // all I care about here are animation frames, 626 // so if this isn't after a control chunk, I'm 627 // just going to ignore it. Read the file with 628 // readPng if you want that. 629 if(!seenFctl) 630 continue; 631 632 assert(frameNumber == 1); // we work on frame 0 but fcTL advances it 633 assert(obj.frames[0]); 634 635 obj.frames[0].compressedDatastream ~= chunk.payload; 636 break; 637 case "acTL": 638 AnimationControlChunk c; 639 int offset = 0; 640 c.num_frames |= chunk.payload[offset++] << 24; 641 c.num_frames |= chunk.payload[offset++] << 16; 642 c.num_frames |= chunk.payload[offset++] << 8; 643 c.num_frames |= chunk.payload[offset++] << 0; 644 645 c.num_plays |= chunk.payload[offset++] << 24; 646 c.num_plays |= chunk.payload[offset++] << 16; 647 c.num_plays |= chunk.payload[offset++] << 8; 648 c.num_plays |= chunk.payload[offset++] << 0; 649 650 assert(offset == chunk.payload.length); 651 652 obj.acc = c; 653 obj.frames = new ApngFrame[](c.num_frames); 654 655 seenacTL = true; 656 break; 657 case "fcTL": 658 FrameControlChunk c; 659 int offset = 0; 660 661 seenFctl = true; 662 663 c.sequence_number |= chunk.payload[offset++] << 24; 664 c.sequence_number |= chunk.payload[offset++] << 16; 665 c.sequence_number |= chunk.payload[offset++] << 8; 666 c.sequence_number |= chunk.payload[offset++] << 0; 667 668 c.width |= chunk.payload[offset++] << 24; 669 c.width |= chunk.payload[offset++] << 16; 670 c.width |= chunk.payload[offset++] << 8; 671 c.width |= chunk.payload[offset++] << 0; 672 673 c.height |= chunk.payload[offset++] << 24; 674 c.height |= chunk.payload[offset++] << 16; 675 c.height |= chunk.payload[offset++] << 8; 676 c.height |= chunk.payload[offset++] << 0; 677 678 c.x_offset |= chunk.payload[offset++] << 24; 679 c.x_offset |= chunk.payload[offset++] << 16; 680 c.x_offset |= chunk.payload[offset++] << 8; 681 c.x_offset |= chunk.payload[offset++] << 0; 682 683 c.y_offset |= chunk.payload[offset++] << 24; 684 c.y_offset |= chunk.payload[offset++] << 16; 685 c.y_offset |= chunk.payload[offset++] << 8; 686 c.y_offset |= chunk.payload[offset++] << 0; 687 688 c.delay_num |= chunk.payload[offset++] << 8; 689 c.delay_num |= chunk.payload[offset++] << 0; 690 691 c.delay_den |= chunk.payload[offset++] << 8; 692 c.delay_den |= chunk.payload[offset++] << 0; 693 694 c.dispose_op = cast(APNG_DISPOSE_OP) chunk.payload[offset++]; 695 c.blend_op = cast(APNG_BLEND_OP) chunk.payload[offset++]; 696 697 assert(offset == chunk.payload.length); 698 699 import std.conv; 700 if(expectedSequenceNumber != c.sequence_number) 701 throw new Exception("malformed apng file expected fcTL seq " ~ to!string(expectedSequenceNumber) ~ " got " ~ to!string(c.sequence_number)); 702 703 expectedSequenceNumber++; 704 705 706 if(obj.frames[frameNumber] is null) 707 obj.frames[frameNumber] = new ApngFrame(obj); 708 obj.frames[frameNumber].frameControlChunk = c; 709 710 frameNumber++; 711 break; 712 case "fdAT": 713 uint sequence_number; 714 int offset; 715 716 sequence_number |= chunk.payload[offset++] << 24; 717 sequence_number |= chunk.payload[offset++] << 16; 718 sequence_number |= chunk.payload[offset++] << 8; 719 sequence_number |= chunk.payload[offset++] << 0; 720 721 import std.conv; 722 if(expectedSequenceNumber != sequence_number) 723 throw new Exception("malformed apng file expected fdAT seq " ~ to!string(expectedSequenceNumber) ~ " got " ~ to!string(sequence_number)); 724 725 expectedSequenceNumber++; 726 727 // and the rest of it is a datastream... 728 obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $]; 729 break; 730 default: 731 obj.handleOtherChunkWhenLoading(chunk); 732 } 733 734 } 735 736 return obj; 737 } 738 739 740 /++ 741 It takes the apng file and feeds the file data to your `sink` delegate, the given file, 742 or simply returns it as an in-memory array. 743 +/ 744 void writeApngToData(ApngAnimation apng, scope void delegate(in ubyte[] data) sink) { 745 746 apng.resyncData(); 747 748 PNG* p = blankPNG(apng.header); 749 if(apng.palette.length) 750 p.replacePalette(apng.palette); 751 752 // I want acTL first, then frames, then idat last. 753 754 ubyte[128] buffer; 755 756 p.chunks ~= *(Chunk.create("acTL", apng.acc.toChunkPayload(buffer[]).dup)); 757 758 // then IDAT is required 759 // FIXME: it might be better to just legit use the first frame but meh gotta check size and stuff too 760 auto render = apng.renderer(); 761 render.nextFrame(); 762 auto data = render.buffer.imageData.bytes; 763 addImageDatastreamToPng(data, p, false); 764 765 // then the frames 766 int sequenceNumber = 0; 767 foreach(frame; apng.frames) { 768 p.chunks ~= *(Chunk.create("fcTL", frame.frameControlChunk.toChunkPayload(sequenceNumber++, buffer[]).dup)); 769 // fdAT 770 771 import std.zlib; 772 773 size_t bytesPerLine; 774 switch(apng.header.type) { 775 case 0: 776 // FIXME: < 8 depth not supported here but should be 777 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 1 * apng.header.depth / 8; 778 break; 779 case 2: 780 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 3 * apng.header.depth / 8; 781 break; 782 case 3: 783 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 1 * apng.header.depth / 8; 784 break; 785 case 4: 786 // FIXME: < 8 depth not supported here but should be 787 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 2 * apng.header.depth / 8; 788 break; 789 case 6: 790 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 4 * apng.header.depth / 8; 791 break; 792 default: assert(0); 793 794 } 795 796 Chunk dat; 797 dat.type = ['f', 'd', 'A', 'T']; 798 size_t pos = 0; 799 800 const(ubyte)[] output; 801 802 frame.populateData(); 803 804 while(pos+bytesPerLine <= frame.data.length) { 805 output ~= 0; 806 output ~= frame.data[pos..pos+bytesPerLine]; 807 pos += bytesPerLine; 808 } 809 810 auto com = cast(ubyte[]) compress(output); 811 dat.size = cast(int) com.length + 4; 812 813 buffer[0] = (sequenceNumber >> 24) & 0xff; 814 buffer[1] = (sequenceNumber >> 16) & 0xff; 815 buffer[2] = (sequenceNumber >> 8) & 0xff; 816 buffer[3] = (sequenceNumber >> 0) & 0xff; 817 818 sequenceNumber++; 819 820 821 dat.payload = buffer[0 .. 4] ~ com; 822 dat.checksum = crc("fdAT", dat.payload); 823 824 p.chunks ~= dat; 825 } 826 827 { 828 Chunk c; 829 830 c.size = 0; 831 c.type = ['I', 'E', 'N', 'D']; 832 c.checksum = crc("IEND", c.payload); 833 p.chunks ~= c; 834 } 835 836 sink(writePng(p)); 837 } 838 839 /// ditto 840 void writeApngToFile(ApngAnimation apng, string filename) { 841 import std.stdio; 842 auto file = File(filename, "wb"); 843 writeApngToData(apng, delegate(in ubyte[] data) { 844 file.rawWrite(data); 845 }); 846 } 847 848 /// ditto 849 ubyte[] getApngBytes(ApngAnimation apng) { 850 ubyte[] ret; 851 writeApngToData(apng, (in ubyte[] data) { ret ~= data; }); 852 return ret; 853 }