/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Windows.Media.Imaging; using CommonUtil; namespace CommonWPF { /// /// Creates an animated GIF from a collection of bitmap frames. /// public class AnimatedGifEncoder { // GIF signature + version. private static readonly byte[] GIF89A_SIGNATURE = new byte[] { (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a' }; private static readonly byte[] NetscapeExtStart = new byte[] { UnpackedGif.EXTENSION_INTRODUCER, UnpackedGif.APP_EXTENSION_LABEL, 0x0b, // Block Size (byte)'N', // Application Identifier (8 bytes) (byte)'E', (byte)'T', (byte)'S', (byte)'C', (byte)'A', (byte)'P', (byte)'E', (byte)'2', // Appl. Authentication Code (3 bytes) (byte)'.', (byte)'0', 0x03, // size of block // followed by loop flag, 2-byte repetition count, and $00 to terminate }; private static readonly byte[] GraphicControlStart = new byte[] { UnpackedGif.EXTENSION_INTRODUCER, UnpackedGif.GRAPHIC_CONTROL_LABEL, 0x04, // Block Size // followed by flags, 2-byte delay, transparency color index, and $00 to terminate }; /// /// List of bitmap frames. /// private List Frames { get; set; } private class MetaData { public int DelayMsec { get; private set; } public MetaData(int delayMsec) { DelayMsec = delayMsec; } } /// /// Per-frame metadata. /// private List FrameData { get; set; } /// /// Constructor. /// public AnimatedGifEncoder() { Frames = new List(); FrameData = new List(); } public void AddFrame(BitmapFrame frame, int delayMsec) { Frames.Add(frame); FrameData.Add(new MetaData(delayMsec)); } /// /// Converts the list of frames into an animated GIF, and writes it to the stream. /// /// Output stream. public void Save(Stream stream, out int maxWidth, out int maxHeight) { maxWidth = maxHeight = -1; if (Frames.Count == 0) { // nothing to do Debug.Assert(false); return; } // // Step 1: convert all BitmapFrame objects to GIF. This lets the .NET GIF encoder // deal with the data compression. // List gifs = new List(Frames.Count); foreach (BitmapFrame bf in Frames) { GifBitmapEncoder encoder = new GifBitmapEncoder(); encoder.Frames.Add(bf); using (MemoryStream ms = new MemoryStream()) { encoder.Save(ms); // We're using GetBuffer() rather than ToArray() to avoid a copy. One // consequence of this choice is that the byte[] may be oversized. Since // GIFs are treated as streams with explicit termination this should not // pose a problem. gifs.Add(UnpackedGif.Create(ms.GetBuffer())); } } // // Step 2: determine the dimensions of the largest image. This will become the // logical size of the animated GIF. // // TODO(maybe): We have an opportunity to replace all of the local color tables with a // single global color table. This is only possible if all of the local tables are // identical and the transparency values in the GCE also match up. (Well, it's // otherwise *possible*, but we'd need to decode, update palettes and pixels, and // re-encode.) // TODO(maybe): add an arg to Save() that causes it to use the first bitmap's // palette as the global palette. // foreach (UnpackedGif gif in gifs) { //gif.DebugDump(); if (maxWidth < gif.LogicalScreenWidth) { maxWidth = gif.LogicalScreenWidth; } if (maxHeight < gif.LogicalScreenHeight) { maxHeight = gif.LogicalScreenHeight; } } if (maxWidth < 0 || maxHeight < 0) { Debug.WriteLine("Unable to determine correct width/height"); return; } // // Step 3: output data. // stream.Write(GIF89A_SIGNATURE, 0, GIF89A_SIGNATURE.Length); WriteLittleUshort(stream, (ushort)maxWidth); WriteLittleUshort(stream, (ushort)maxHeight); stream.WriteByte(0x70); // no GCT; max color resolution (does this matter?) stream.WriteByte(0); // BCI; not relevant stream.WriteByte(0); // no aspect ratio adjustment stream.Write(NetscapeExtStart, 0, NetscapeExtStart.Length); stream.WriteByte(1); // yes, we want to loop WriteLittleUshort(stream, 0); // loop forever stream.WriteByte(0); // end of block Debug.Assert(gifs.Count == FrameData.Count); for (int i = 0; i < Frames.Count; i++) { UnpackedGif gif = gifs[i]; MetaData md = FrameData[i]; // Just use the first image. UnpackedGif.GraphicRenderingBlock grb = gif.ImageBlocks[0]; byte colorTableSize; byte[] colorTable; if (grb.LocalColorTableFlag) { colorTableSize = grb.LocalColorTableSize; colorTable = grb.LocalColorTable; } else if (gif.GlobalColorTableFlag) { colorTableSize = gif.GlobalColorTableSize; colorTable = gif.GlobalColorTable; } else { Debug.Assert(false); colorTableSize = 0x07; colorTable = new byte[256 * 3]; // a whole lotta black } Debug.Assert(colorTable.Length == (1 << (colorTableSize + 1)) * 3); // If it has a GCE, use that. Otherwise supply default values. Either way // we use the frame delay from the meta-data. UnpackedGif.GraphicControlExtension gce = grb.GraphicControlExt; byte disposalMethod = (byte)UnpackedGif.GraphicControlExtension.DisposalMethods.RestoreBackground; bool userInputFlag = false; bool transparencyFlag = false; byte transparentColorIndex = 0; if (gce != null) { //disposalMethod = gce.DisposalMethod; userInputFlag = gce.UserInputFlag; transparencyFlag = gce.TransparencyFlag; transparentColorIndex = gce.TransparentColorIndex; } stream.Write(GraphicControlStart, 0, GraphicControlStart.Length); stream.WriteByte((byte)((disposalMethod << 2) | (userInputFlag ? 0x02 : 0) | (transparencyFlag ? 0x01 : 0))); WriteLittleUshort(stream, (ushort)Math.Round(md.DelayMsec / 10.0)); stream.WriteByte(transparentColorIndex); stream.WriteByte(0); // end of GCE // Output image descriptor. We can center the images in the animation or // just leave them in the top-left corner. stream.WriteByte(UnpackedGif.IMAGE_SEPARATOR); WriteLittleUshort(stream, 0); // left WriteLittleUshort(stream, 0); // top WriteLittleUshort(stream, gif.LogicalScreenWidth); WriteLittleUshort(stream, gif.LogicalScreenHeight); stream.WriteByte((byte)(0x80 | colorTableSize)); // local table, no sort/intrl // Local color table. stream.Write(colorTable, 0, colorTable.Length); // Image data. Trailing $00 is included. stream.Write(grb.ImageData, grb.ImageStartOffset, grb.ImageEndOffset - grb.ImageStartOffset + 1); } stream.WriteByte(UnpackedGif.GIF_TRAILER); } private static void WriteLittleUshort(Stream stream, ushort val) { stream.WriteByte((byte)val); stream.WriteByte((byte)(val >> 8)); } } }