private void WriteVariableBitsChunked(uint value, int bits, ref PendingBitsAndAndChunks pending) { // NOTE: GIF uses LSB-first packing Debug.Assert(pending.bits >= 0 && pending.bits < 8); Debug.Assert(value < (1u << bits)); while (bits > 0) { int takeBits = System.Math.Min(bits, 8 - pending.bits); uint takeMask = (1u << takeBits) - 1u; pending.value |= ((value & takeMask) << pending.bits); pending.bits += takeBits; bits -= takeBits; value >>= takeBits; if (pending.bits == 8) { WriteChunked((byte)(pending.value & 0xFF), ref pending); pending.value = 0; pending.bits = 0; } } }
private void FinishVariableBitsChunked(ref PendingBitsAndAndChunks pending) { if (pending.bits > 0) { WriteChunked((byte)(pending.value & 0xFF), ref pending); pending.value = 0; pending.bits = 0; } }
private void FinishChunked(ref PendingBitsAndAndChunks pending) { if (pending.chunkSize > 0) // If we're right on the chunk boundary (0 bytes in current chunk), just use the placeholder { Debug.Assert(pending.chunkSize <= byte.MaxValue); buffer[bufferPosition - pending.chunkSize - 1] = (byte)pending.chunkSize; pending.chunkSize = 0; WriteByte(0); // Terminator } }
private void WriteChunked(byte byteToWrite, ref PendingBitsAndAndChunks pending) { WriteByte(byteToWrite); pending.chunkSize++; if (pending.chunkSize == 255) { buffer[bufferPosition - 256] = 255; pending.chunkSize = 0; WriteByte(0); // Placeholder for chunk size } }
// TODO: Factor this out such that frames can be appended on-the-fly (for capturing "animated screenshots"). // // May want to do threading (outside this class). Could thread the following: // - Pulling GPU surfaces (want to return surfaces quikcly) // - GIF encoding (maybe fast enough to combine, rather than take buffer re-cache hit and thread costs) // - File I/O (want to avoid blocking useful work while writing to file, and built-in async allocates, uses a thread pool anyway??) // Debatable about what, if anything, to combine. public void WriteAnimation(Animation animation) { if (animation == null) { throw new ArgumentNullException("animation"); } if (animation.FrameCount == 0) { return; } if (stream == null) { throw new InvalidOperationException("No stream set"); } Rectangle animationBounds = animation.GetSoftRenderBounds(true); Position accumulatedGameplayMotion = Position.Zero; bool animated = animation.FrameCount > 1; // Preconditions: if (animationBounds.Width > ushort.MaxValue || animationBounds.Height > ushort.MaxValue) { return; } bool transparency = true; // GIF: Header { WriteBuffer(gifHeader); } // GIF: Logical Screen Descriptor { WriteUShort((ushort)animationBounds.Width); WriteUShort((ushort)animationBounds.Height); byte packed = 0; packed |= 1 << 7; // Global colour table present packed |= 7 << 4; // Bits-per-channel minus 1, in source data packed |= 0 << 3; // Is GCT priority-sorted? packed |= 7 << 0; // Global colour table size = 2^(N+1) WriteByte(packed); WriteByte(0); // Background colour index into GCT (or 0 if not present) WriteByte(0); // Pixel aspect ratio, or 0 for "not-specified" } // GIF: Global Color Table // TODO: Consider using pre-built NES palette? (Especially for game export) // TODO: Add support for local color tables // Reserve and clear space in output buffer: const int colorTableBytes = 256 * 3; // 256 RGB Colors int gctStart = bufferPosition; EnsureBufferSpace(colorTableBytes); bufferPosition += colorTableBytes; Array.Clear(buffer, gctStart, colorTableBytes); int gctCount = 1; // <- first value is always pure black, and transparent if transparency is enabled // GIF: NETSCAPE2.0 Application Extension Block if (animated) { WriteBuffer(netscapeBlock); } foreach (var frame in animation.Frames) { var data = frame.SoftRender(); // HACK: If the image has zero size (blank, cropped to nothing), replace it with 1px of transparency // (Because I'm not fiddling around trying to figure out how to encode a zero-sized frame, or how compatible that is, right now -AR) if (data.Width == 0 || data.Height == 0) { data = new Data2D <Color>(animationBounds.X, animationBounds.Y, 1, 1); } accumulatedGameplayMotion += frame.positionDelta; Point motion = accumulatedGameplayMotion.ToWorldZero.FlipY(); // Convert to texture space data.OffsetX += motion.X; data.OffsetY += motion.Y; int delayTime = (frame.delay * 100) / 60; // Convert ticks at 60FPS to ticks at 100FPS (the GIF standard) // NOTE: Browsers do retarded things with timings of 0 or 1. Some even do stupid things with 2-5. // See http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser for details delayTime = System.Math.Max(2, System.Math.Min(delayTime, ushort.MaxValue)); // GIF: Graphic Control Extension { WriteByte(0x21); // extension block WriteByte(0xF9); // graphics control label WriteByte(0x04); // block size in bytes (fixed) byte packed = 0; packed |= (byte)((animated ? 2u : 0u) << 2); // Disposal method (0 = none specified, 1 = do not clear, 2 = clear to BG) packed |= 0 << 1; // User input flag packed |= (byte)((transparency ? 1u : 0) << 0); // Transparent colour flag WriteByte(packed); WriteUShort((ushort)(animated ? delayTime : 0)); // Delay time WriteByte(0); // Transparent colour index // TODO: Put this outside the table, so that non-transparent images don't have fixed black entry in palette. WriteByte(0); // Block terminator } // GIF: Image descriptor { WriteByte(0x2C); // Image separator WriteUShort((ushort)(data.OffsetX - animationBounds.X)); // Image left WriteUShort((ushort)(data.OffsetY - animationBounds.Y)); // Image top WriteUShort((ushort)data.Width); // Image width WriteUShort((ushort)data.Height); // Image height byte packed = 0; packed |= 0 << 7; // Local colour table packed |= 0 << 6; // Interlace packed |= 0 << 5; // Is LCT priority sorted? packed |= 0 << 0; // Local colour table size = 2^(N+1) WriteByte(packed); // NOTE: Local colour table would folow if specified } // GIF: Image Data (with LZW compression) { WriteByte(8); // LZW minimum code size (hard-coding for 256-colour images for now) WriteByte(0); // Placeholder for chunk size PendingBitsAndAndChunks pending = new PendingBitsAndAndChunks(); // Code table is a series of unbalanced binary trees of available suffixes, one for each prefix LZWTableEntry[] codeTable; ushort[] treeRoots; if (codeTableStorage == null) { codeTable = codeTableStorage = new LZWTableEntry[4096 - codeStart]; // maximum 12-bit codes treeRoots = treeRootsStorage = new ushort[4096]; } else { codeTable = codeTableStorage; treeRoots = treeRootsStorage; Array.Clear(treeRoots, 0, treeRoots.Length); } int codeCount = 0; // <- number of used entries in the (stored) code table int codeBitsUsed = 9; // Always start with clear code: WriteVariableBitsChunked(clearCode, codeBitsUsed, ref pending); // Setup active prefix: uint activePrefix = (uint)GetColorTableIndex(buffer, gctStart, ref gctCount, data.Data[0], transparency); Debug.Assert(activePrefix < 256); // Loop through remaining indicies: int imageSize = data.Width * data.Height; for (int i = 1; i < imageSize; i++) { uint activeSuffix = (uint)GetColorTableIndex(buffer, gctStart, ref gctCount, data.Data[i], transparency); Debug.Assert(activeSuffix < 256); uint lastTreePosition = 0; // <- sentinal value for the root uint foundSuffix = 0; // Search for matching entry: { uint treePosition = treeRoots[activePrefix]; while (treePosition != 0) { lastTreePosition = treePosition; foundSuffix = codeTable[treePosition - codeStart].SuffixValue; if (activeSuffix == foundSuffix) { // FOUND: activePrefix = treePosition; goto nextIndex; } else if (activeSuffix < foundSuffix) { treePosition = codeTable[treePosition - codeStart].LowerIndex; } else // activeSuffix > foundSuffix { treePosition = codeTable[treePosition - codeStart].HigherIndex; } } } // NOT FOUND: // Write the code we do know about WriteVariableBitsChunked(activePrefix, codeBitsUsed, ref pending); // If we fill up the code table, reset it if (codeCount == codeTable.Length) { WriteVariableBitsChunked(clearCode, codeBitsUsed, ref pending); Array.Clear(treeRoots, 0, treeRoots.Length); codeCount = 0; codeBitsUsed = 9; } else // Add to the code table { // Insert into unbalanced binary tree (prefix index is implicit in the tree itself) if (lastTreePosition == 0) { treeRoots[activePrefix] = (ushort)(codeStart + codeCount); } else if (activeSuffix < foundSuffix) { codeTable[lastTreePosition - codeStart].LowerIndex = (uint)(codeStart + codeCount); } else // activeSuffix > foundSuffix { codeTable[lastTreePosition - codeStart].HigherIndex = (uint)(codeStart + codeCount); } // If the new code is past the maximum representable at this code width, increase the code width if ((codeStart + codeCount) == (1 << codeBitsUsed)) { codeBitsUsed++; // NOTE: GIF does *not* do an early change of the code-width } // Fill table entry: codeTable[codeCount++] = new LZWTableEntry(activeSuffix); } // Active prefix has been written, start searching from the suffix activePrefix = activeSuffix; nextIndex :; } WriteVariableBitsChunked(activePrefix, codeBitsUsed, ref pending); WriteVariableBitsChunked(endCode, codeBitsUsed, ref pending); FinishVariableBitsChunked(ref pending); FinishChunked(ref pending); } } // GIF: Trailer WriteByte(0x3B); // Finished creating gif data at this point, write it out stream.Write(buffer, 0, bufferPosition); bufferPosition = 0; }