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 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 /// This is an uninitialized thing, you're responsible for filling in all data yourself. You probably don't want this. 431 this() { 432 433 } 434 435 /++ 436 If palette is null, it is a true color image. If it has data, it is indexed. 437 +/ 438 this(int width, int height, Color[] palette = null) { 439 header.type = (palette !is null) ? 3 : 6; 440 header.width = width; 441 header.height = height; 442 443 this.palette = palette; 444 } 445 446 /++ 447 Adds a frame with the given size and returns the object. You can change other values in the frameControlChunk on it 448 and get the data bytes out of there. 449 +/ 450 ApngFrame addFrame(int width, int height) { 451 assert(width <= header.width); 452 assert(height <= header.height); 453 auto f = new ApngFrame(this, width, height); 454 frames ~= f; 455 acc.num_frames++; 456 return f; 457 } 458 459 // call before writing or trying to render again 460 void resyncData() { 461 acc.num_frames = cast(int) frames.length; 462 foreach(frame; frames) 463 frame.resyncData(); 464 } 465 466 /// 467 ApngRenderBuffer renderer() { 468 return ApngRenderBuffer(this, new TrueColorImage(header.width, header.height), 0); 469 } 470 } 471 472 /// 473 enum APNG_DISPOSE_OP : byte { 474 NONE = 0, /// 475 BACKGROUND = 1, /// 476 PREVIOUS = 2 /// 477 } 478 479 /// 480 enum APNG_BLEND_OP : byte { 481 SOURCE = 0, /// 482 OVER = 1 /// 483 } 484 485 /++ 486 Loads an apng file. 487 488 Params: 489 data = the raw data bytes of the file 490 strictApng = if true, it will strictly interpret 491 the file as apng and ignore the default image. If there 492 are no animation chunks, it will return an empty ApngAnimation 493 object. 494 495 If false, it will use the default image as the first 496 (and only) frame of animation if there are no apng chunks. 497 498 History: 499 Parameter `strictApng` added February 27, 2021 500 +/ 501 ApngAnimation readApng(in ubyte[] data, bool strictApng = false) { 502 auto png = readPng(data); 503 auto header = PngHeader.fromChunk(png.chunks[0]); 504 505 auto obj = new ApngAnimation(); 506 obj.header = header; 507 508 if(header.type == 3) { 509 obj.palette = fetchPalette(png); 510 } 511 512 bool seenIdat = false; 513 bool seenFctl = false; 514 515 int frameNumber; 516 int expectedSequenceNumber = 0; 517 518 bool seenacTL = false; 519 520 foreach(chunk; png.chunks) { 521 switch(chunk.stype) { 522 case "IDAT": 523 524 if(!seenacTL && !strictApng) { 525 // acTL chunks must appear before IDAT per spec, 526 // so if there isn't one by now, it isn't an apng file. 527 // but unless we care about strictApng, we can salvage 528 // by making some dummy data. 529 530 { 531 AnimationControlChunk c; 532 c.num_frames = 1; 533 c.num_plays = 1; 534 535 obj.acc = c; 536 obj.frames = new ApngFrame[](c.num_frames); 537 538 seenacTL = true; 539 } 540 541 { 542 FrameControlChunk c; 543 c.sequence_number = 1; 544 c.width = header.width; 545 c.height = header.height; 546 c.x_offset = 0; 547 c.y_offset = 0; 548 c.delay_num = short.max; 549 c.delay_den = 1; 550 c.dispose_op = APNG_DISPOSE_OP.NONE; 551 c.blend_op = APNG_BLEND_OP.SOURCE; 552 553 seenFctl = true; 554 555 // not increasing expectedSequenceNumber since if something is present, this is malformed! 556 557 if(obj.frames[frameNumber] is null) 558 obj.frames[frameNumber] = new ApngFrame(obj); 559 obj.frames[frameNumber].frameControlChunk = c; 560 561 frameNumber++; 562 } 563 } 564 565 566 seenIdat = true; 567 // all I care about here are animation frames, 568 // so if this isn't after a control chunk, I'm 569 // just going to ignore it. Read the file with 570 // readPng if you want that. 571 if(!seenFctl) 572 continue; 573 574 assert(frameNumber == 1); // we work on frame 0 but fcTL advances it 575 assert(obj.frames[0]); 576 577 obj.frames[0].compressedDatastream ~= chunk.payload; 578 break; 579 case "acTL": 580 AnimationControlChunk c; 581 int offset = 0; 582 c.num_frames |= chunk.payload[offset++] << 24; 583 c.num_frames |= chunk.payload[offset++] << 16; 584 c.num_frames |= chunk.payload[offset++] << 8; 585 c.num_frames |= chunk.payload[offset++] << 0; 586 587 c.num_plays |= chunk.payload[offset++] << 24; 588 c.num_plays |= chunk.payload[offset++] << 16; 589 c.num_plays |= chunk.payload[offset++] << 8; 590 c.num_plays |= chunk.payload[offset++] << 0; 591 592 assert(offset == chunk.payload.length); 593 594 obj.acc = c; 595 obj.frames = new ApngFrame[](c.num_frames); 596 597 seenacTL = true; 598 break; 599 case "fcTL": 600 FrameControlChunk c; 601 int offset = 0; 602 603 seenFctl = true; 604 605 c.sequence_number |= chunk.payload[offset++] << 24; 606 c.sequence_number |= chunk.payload[offset++] << 16; 607 c.sequence_number |= chunk.payload[offset++] << 8; 608 c.sequence_number |= chunk.payload[offset++] << 0; 609 610 c.width |= chunk.payload[offset++] << 24; 611 c.width |= chunk.payload[offset++] << 16; 612 c.width |= chunk.payload[offset++] << 8; 613 c.width |= chunk.payload[offset++] << 0; 614 615 c.height |= chunk.payload[offset++] << 24; 616 c.height |= chunk.payload[offset++] << 16; 617 c.height |= chunk.payload[offset++] << 8; 618 c.height |= chunk.payload[offset++] << 0; 619 620 c.x_offset |= chunk.payload[offset++] << 24; 621 c.x_offset |= chunk.payload[offset++] << 16; 622 c.x_offset |= chunk.payload[offset++] << 8; 623 c.x_offset |= chunk.payload[offset++] << 0; 624 625 c.y_offset |= chunk.payload[offset++] << 24; 626 c.y_offset |= chunk.payload[offset++] << 16; 627 c.y_offset |= chunk.payload[offset++] << 8; 628 c.y_offset |= chunk.payload[offset++] << 0; 629 630 c.delay_num |= chunk.payload[offset++] << 8; 631 c.delay_num |= chunk.payload[offset++] << 0; 632 633 c.delay_den |= chunk.payload[offset++] << 8; 634 c.delay_den |= chunk.payload[offset++] << 0; 635 636 c.dispose_op = cast(APNG_DISPOSE_OP) chunk.payload[offset++]; 637 c.blend_op = cast(APNG_BLEND_OP) chunk.payload[offset++]; 638 639 assert(offset == chunk.payload.length); 640 641 import std.conv; 642 if(expectedSequenceNumber != c.sequence_number) 643 throw new Exception("malformed apng file expected fcTL seq " ~ to!string(expectedSequenceNumber) ~ " got " ~ to!string(c.sequence_number)); 644 645 expectedSequenceNumber++; 646 647 648 if(obj.frames[frameNumber] is null) 649 obj.frames[frameNumber] = new ApngFrame(obj); 650 obj.frames[frameNumber].frameControlChunk = c; 651 652 frameNumber++; 653 break; 654 case "fdAT": 655 uint sequence_number; 656 int offset; 657 658 sequence_number |= chunk.payload[offset++] << 24; 659 sequence_number |= chunk.payload[offset++] << 16; 660 sequence_number |= chunk.payload[offset++] << 8; 661 sequence_number |= chunk.payload[offset++] << 0; 662 663 import std.conv; 664 if(expectedSequenceNumber != sequence_number) 665 throw new Exception("malformed apng file expected fdAT seq " ~ to!string(expectedSequenceNumber) ~ " got " ~ to!string(sequence_number)); 666 667 expectedSequenceNumber++; 668 669 // and the rest of it is a datastream... 670 obj.frames[frameNumber - 1].compressedDatastream ~= chunk.payload[offset .. $]; 671 break; 672 default: 673 // ignore 674 } 675 676 } 677 678 return obj; 679 } 680 681 682 /++ 683 684 +/ 685 void writeApngToData(ApngAnimation apng, scope void delegate(in ubyte[] data) sink) { 686 687 apng.resyncData(); 688 689 PNG* p = blankPNG(apng.header); 690 if(apng.palette.length) 691 p.replacePalette(apng.palette); 692 693 // I want acTL first, then frames, then idat last. 694 695 ubyte[128] buffer; 696 697 p.chunks ~= *(Chunk.create("acTL", apng.acc.toChunkPayload(buffer[]).dup)); 698 699 // then IDAT is required 700 // FIXME: it might be better to just legit use the first frame but meh gotta check size and stuff too 701 auto render = apng.renderer(); 702 render.nextFrame(); 703 auto data = render.buffer.imageData.bytes; 704 addImageDatastreamToPng(data, p, false); 705 706 // then the frames 707 int sequenceNumber = 0; 708 foreach(frame; apng.frames) { 709 p.chunks ~= *(Chunk.create("fcTL", frame.frameControlChunk.toChunkPayload(sequenceNumber++, buffer[]).dup)); 710 // fdAT 711 712 import std.zlib; 713 714 size_t bytesPerLine; 715 switch(apng.header.type) { 716 case 0: 717 // FIXME: < 8 depth not supported here but should be 718 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 1 * apng.header.depth / 8; 719 break; 720 case 2: 721 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 3 * apng.header.depth / 8; 722 break; 723 case 3: 724 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 1 * apng.header.depth / 8; 725 break; 726 case 4: 727 // FIXME: < 8 depth not supported here but should be 728 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 2 * apng.header.depth / 8; 729 break; 730 case 6: 731 bytesPerLine = cast(size_t) frame.frameControlChunk.width * 4 * apng.header.depth / 8; 732 break; 733 default: assert(0); 734 735 } 736 737 Chunk dat; 738 dat.type = ['f', 'd', 'A', 'T']; 739 size_t pos = 0; 740 741 const(ubyte)[] output; 742 743 frame.populateData(); 744 745 while(pos+bytesPerLine <= frame.data.length) { 746 output ~= 0; 747 output ~= frame.data[pos..pos+bytesPerLine]; 748 pos += bytesPerLine; 749 } 750 751 auto com = cast(ubyte[]) compress(output); 752 dat.size = cast(int) com.length + 4; 753 754 buffer[0] = (sequenceNumber >> 24) & 0xff; 755 buffer[1] = (sequenceNumber >> 16) & 0xff; 756 buffer[2] = (sequenceNumber >> 8) & 0xff; 757 buffer[3] = (sequenceNumber >> 0) & 0xff; 758 759 sequenceNumber++; 760 761 762 dat.payload = buffer[0 .. 4] ~ com; 763 dat.checksum = crc("fdAT", dat.payload); 764 765 p.chunks ~= dat; 766 } 767 768 { 769 Chunk c; 770 771 c.size = 0; 772 c.type = ['I', 'E', 'N', 'D']; 773 c.checksum = crc("IEND", c.payload); 774 p.chunks ~= c; 775 } 776 777 sink(writePng(p)); 778 } 779 780 /// ditto 781 void writeApngToFile(ApngAnimation apng, string filename) { 782 import std.stdio; 783 auto file = File(filename, "wb"); 784 writeApngToData(apng, delegate(in ubyte[] data) { 785 file.rawWrite(data); 786 }); 787 } 788 789 /// ditto 790 ubyte[] getApngBytes(ApngAnimation apng) { 791 ubyte[] ret; 792 writeApngToData(apng, (in ubyte[] data) { ret ~= data; }); 793 return ret; 794 }