using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace CamBooth.App.Features.Capture;
///
/// Creates an animated GIF from several frames using WPF's .
/// WPF writes the frames but not the per-frame delay or the loop flag, so the encoded bytes are
/// post-processed to inject a NETSCAPE2.0 looping block and a Graphic Control Extension per frame.
///
public static class GifEncoder
{
private const int TargetWidth = 480;
///
/// Builds an animated, looping GIF from and writes it to
/// . is the per-frame display time.
///
public static string CreateGif(IReadOnlyList framePaths, string outputPath, int frameDelayMs = 600)
{
if (framePaths.Count == 0)
throw new ArgumentException("No frames for GIF.", nameof(framePaths));
var (w, h) = TargetSize(framePaths[0]);
var encoder = new GifBitmapEncoder();
foreach (var path in framePaths)
encoder.Frames.Add(BitmapFrame.Create(ToUniformFrame(path, w, h)));
byte[] raw;
using (var ms = new MemoryStream())
{
encoder.Save(ms);
raw = ms.ToArray();
}
var delayCs = (byte)Math.Clamp(frameDelayMs / 10, 2, 255);
var bytes = TryAddAnimation(raw, delayCs);
File.WriteAllBytes(outputPath, bytes);
return outputPath;
}
///
/// Rewrites a static multi-frame GIF into a looping animation. On any parsing surprise it falls
/// back to the original bytes (still a valid GIF), so a capture is never lost.
///
private static byte[] TryAddAnimation(byte[] src, byte delayCs)
{
try
{
var outBytes = new List(src.Length + 64);
int pos = 0;
// Header (6) + Logical Screen Descriptor (7)
outBytes.AddRange(src[..13]);
pos = 13;
// Global Color Table (if present)
var lsdPacked = src[10];
if ((lsdPacked & 0x80) != 0)
{
int gctBytes = 3 * (1 << ((lsdPacked & 0x07) + 1));
outBytes.AddRange(src[pos..(pos + gctBytes)]);
pos += gctBytes;
}
// Insert the loop-forever application extension once.
outBytes.AddRange(NetscapeLoopBlock());
while (pos < src.Length)
{
byte block = src[pos];
if (block == 0x3B) // trailer
{
outBytes.Add(block);
pos++;
break;
}
if (block == 0x21) // extension
{
outBytes.Add(src[pos++]); // introducer
outBytes.Add(src[pos++]); // label
pos = CopySubBlocks(src, pos, outBytes);
}
else if (block == 0x2C) // image descriptor
{
outBytes.AddRange(GraphicControlExtension(delayCs)); // our delay, before the image
outBytes.AddRange(src[pos..(pos + 10)]); // descriptor (separator + 9)
byte imgPacked = src[pos + 9];
pos += 10;
if ((imgPacked & 0x80) != 0) // local color table
{
int lctBytes = 3 * (1 << ((imgPacked & 0x07) + 1));
outBytes.AddRange(src[pos..(pos + lctBytes)]);
pos += lctBytes;
}
outBytes.Add(src[pos++]); // LZW minimum code size
pos = CopySubBlocks(src, pos, outBytes); // image data
}
else
{
// Unknown — bail out and keep the original (safe fallback).
return src;
}
}
return outBytes.ToArray();
}
catch
{
return src;
}
}
private static int CopySubBlocks(byte[] src, int pos, List outBytes)
{
while (true)
{
byte size = src[pos];
outBytes.Add(size);
pos++;
if (size == 0) break;
outBytes.AddRange(src[pos..(pos + size)]);
pos += size;
}
return pos;
}
private static byte[] NetscapeLoopBlock()
{
var b = new List { 0x21, 0xFF, 0x0B };
b.AddRange(Encoding.ASCII.GetBytes("NETSCAPE2.0"));
b.AddRange(new byte[] { 0x03, 0x01, 0x00, 0x00, 0x00 }); // sub-block, id=1, loop count 0 = forever, terminator
return b.ToArray();
}
private static byte[] GraphicControlExtension(byte delayCs) =>
// introducer, label, block size, packed (disposal=1), delay lo/hi, transparent index, terminator
new byte[] { 0x21, 0xF9, 0x04, 0x04, delayCs, 0x00, 0x00, 0x00 };
private static (int w, int h) TargetSize(string firstFramePath)
{
var probe = new BitmapImage();
probe.BeginInit();
probe.UriSource = new Uri(firstFramePath);
probe.CacheOption = BitmapCacheOption.OnLoad;
probe.EndInit();
int h = (int)Math.Round(TargetWidth * (probe.PixelHeight / (double)probe.PixelWidth));
return (TargetWidth, Math.Max(1, h));
}
private static BitmapSource ToUniformFrame(string path, int w, int h)
{
var src = new BitmapImage();
src.BeginInit();
src.UriSource = new Uri(path);
src.CacheOption = BitmapCacheOption.OnLoad;
src.DecodePixelWidth = w;
src.EndInit();
src.Freeze();
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
dc.DrawImage(src, new Rect(0, 0, w, h));
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
rtb.Render(visual);
rtb.Freeze();
return rtb;
}
}