Skip to content

Commit

Permalink
fix some animation edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
saucecontrol committed Oct 9, 2023
1 parent 7fef17e commit 11e96cc
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 175 deletions.
11 changes: 10 additions & 1 deletion src/MagicScaler/Codecs/CodecOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ public interface IMultiFrameDecoderOptions : IDecoderOptions
Range FrameRange { get; }
}

/// <summary>Describes a decoder that supports animation properies in a container.</summary>
public interface IAnimationDecoderOptions : IDecoderOptions
{
/// <summary>True to honor the background color given in the container metadata, otherwise false.</summary>
/// <remarks>Most animation viewers, including web browsers, override the background.</remarks>
bool UseBackgroundColor { get; }
}

/// <summary>JPEG decoder options.</summary>
/// <param name="AllowPlanar"><inheritdoc cref="IPlanarDecoderOptions.AllowPlanar" path="/summary/node()" /></param>
public readonly record struct JpegDecoderOptions(bool AllowPlanar) : IPlanarDecoderOptions
Expand All @@ -123,7 +131,8 @@ public readonly record struct JpegDecoderOptions(bool AllowPlanar) : IPlanarDeco

/// <summary>GIF decoder options.</summary>
/// <param name="FrameRange"><inheritdoc cref="IMultiFrameDecoderOptions.FrameRange" path="/summary/node()" /></param>
public readonly record struct GifDecoderOptions(Range FrameRange) : IMultiFrameDecoderOptions
/// <param name="UseBackgroundColor"><inheritdoc cref="IAnimationDecoderOptions.UseBackgroundColor" path="/summary/node()" /></param>
public readonly record struct GifDecoderOptions(Range FrameRange, bool UseBackgroundColor = false) : IMultiFrameDecoderOptions, IAnimationDecoderOptions
{
/// <summary>Default GIF decoder options.</summary>
public static GifDecoderOptions Default => new(..);
Expand Down
32 changes: 15 additions & 17 deletions src/MagicScaler/Core/AnimationPipelineContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright © Clinton Ingram and Contributors. Licensed under the MIT License.

using System;
using System.Runtime.InteropServices;

using PhotoSauce.MagicScaler.Transforms;

Expand All @@ -14,21 +13,16 @@ internal sealed class AnimationPipelineContext : IDisposable

public FrameBufferSource? ScreenBuffer;
public FrameDisposalMethod LastDisposal = FrameDisposalMethod.RestoreBackground;
public PixelArea LastArea;

public unsafe void UpdateFrameBuffer(IPixelSource src, in AnimationContainer anicnt, in AnimationFrame anifrm)
{
int width = Math.Min(src.Width, anicnt.ScreenWidth - anifrm.OffsetLeft);
int height = Math.Min(src.Height, anicnt.ScreenHeight - anifrm.OffsetTop);

if (ScreenBuffer is null)
{
ScreenBuffer = new(anicnt.ScreenWidth, anicnt.ScreenHeight, PixelFormat.Bgra32, true);
ScreenBuffer.Span.Clear();
ScreenBuffer.Clear(ScreenBuffer.Area, (uint)anicnt.BackgroundColor);
}

var fbuff = ScreenBuffer;
var bspan = fbuff.Span;

if (src.Format != PixelFormat.Bgra32.FormatGuid)
{
var psrc = src.AsPixelSource();
Expand All @@ -44,35 +38,39 @@ public unsafe void UpdateFrameBuffer(IPixelSource src, in AnimationContainer ani
}
}

var fbuff = ScreenBuffer;
fbuff.Profiler.ResumeTiming();

// Most GIF viewers clear the background to transparent instead of the background color when the next frame has transparency
bool fullScreen = width == anicnt.ScreenWidth && height == anicnt.ScreenHeight;
if (!fullScreen && LastDisposal == FrameDisposalMethod.RestoreBackground)
MemoryMarshal.Cast<byte, uint>(bspan).Fill(anifrm.HasAlpha ? 0 : (uint)anicnt.BackgroundColor);

var fspan = bspan.Slice(anifrm.OffsetTop * fbuff.Stride + anifrm.OffsetLeft * fbuff.Format.BytesPerPixel);
var farea = new PixelArea(anifrm.OffsetLeft, anifrm.OffsetTop, src.Width, src.Height).Intersect(fbuff.Area);
var fspan = fbuff.Span.Slice(anifrm.OffsetTop * fbuff.Stride + anifrm.OffsetLeft * fbuff.Format.BytesPerPixel);
fixed (byte* buff = fspan)
{
if (anifrm.Blend == AlphaBlendMethod.Source)
{
src.CopyPixels(new(0, 0, width, height), fbuff.Stride, fspan);
src.CopyPixels(PixelArea.FromSize(farea.Width, farea.Height), fbuff.Stride, fspan);
}
else
{
var area = new PixelArea(anifrm.OffsetLeft, anifrm.OffsetTop, width, height);
if (overlay is null)
overlay = new OverlayTransform(fbuff, src.AsPixelSource(), anifrm.OffsetLeft, anifrm.OffsetTop, anifrm.HasAlpha, anifrm.Blend);
else
overlay.SetOver(src.AsPixelSource(), anifrm.OffsetLeft, anifrm.OffsetTop, anifrm.HasAlpha, anifrm.Blend);

overlay.CopyPixels(area, fbuff.Stride, fspan.Length, buff);
overlay.CopyPixels(farea, fbuff.Stride, fspan.Length, buff);
}
}

fbuff.Profiler.PauseTiming();
}

public void ClearFrameBuffer(uint color)
{
var fbuff = ScreenBuffer!;
fbuff.Profiler.ResumeTiming();
fbuff.Clear(LastArea, color);
fbuff.Profiler.PauseTiming();
}

public void Dispose()
{
converter?.Dispose();
Expand Down
18 changes: 18 additions & 0 deletions src/MagicScaler/Core/PipelineContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ public T AddProfiler<T>(T source) where T : IProfileSource
return source;
}

public bool TryGetAnimationMetadata(out AnimationContainer anicnt, out AnimationFrame anifrm)
{
anicnt = default;
anifrm = default;

bool nocntmeta = ImageContainer is not IMetadataSource cmsrc || !cmsrc.TryGetMetadata(out anicnt);
bool nofrmmeta = ImageFrame is not IMetadataSource fmsrc || !fmsrc.TryGetMetadata(out anifrm);
if (nocntmeta && nofrmmeta)
return false;

if (nocntmeta || anicnt.ScreenWidth is 0 || anicnt.ScreenHeight is 0)
anicnt = new(Source.Width, Source.Height, ImageContainer.FrameCount);
if (nofrmmeta)
anifrm = AnimationFrame.Default;

return true;
}

public void FinalizeSettings()
{
Orientation = Settings.OrientationMode == OrientationMode.Normalize ? ImageFrame.GetOrientation() : Orientation.Normal;
Expand Down
4 changes: 3 additions & 1 deletion src/MagicScaler/Core/PixelArea.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ public PixelArea SliceMax(int y, int height)
return new(X, Y + y, Width, height);
}

public static PixelArea FromSize(int width, int height) => new(0, 0, width, height);

public static implicit operator PixelArea(in Rectangle r) => new(r.X, r.Y, r.Width, r.Height);
public static implicit operator Rectangle(in PixelArea a) => Unsafe.As<PixelArea, Rectangle>(ref Unsafe.AsRef(a));

public static implicit operator PixelArea(Size s) => new(0, 0, s.Width, s.Height);
public static implicit operator PixelArea(Size s) => FromSize(s.Width, s.Height);
}
52 changes: 30 additions & 22 deletions src/MagicScaler/Magic/AnimationEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public bool TryGetMetadata<T>([NotNullWhen(true)] out T? metadata) where T : IMe
private readonly PixelSource lastSource;
private readonly ChainedPixelSource? convertSource;
private readonly AnimationBufferFrame[] frames = new AnimationBufferFrame[3];
private readonly AnimationContainer anicnt;
private readonly int lastFrame;

private int currentFrame;
Expand All @@ -59,6 +60,9 @@ public bool TryGetMetadata<T>([NotNullWhen(true)] out T? metadata) where T : IMe

public AnimationEncoder(PipelineContext ctx, IAnimatedImageEncoder enc)
{
if (!ctx.TryGetAnimationMetadata(out anicnt, out var anifrm))
throw new InvalidOperationException("Not an animation source.");

context = ctx;
encoder = enc;

Expand All @@ -68,9 +72,9 @@ public AnimationEncoder(PipelineContext ctx, IAnimatedImageEncoder enc)
lastSource = ctx.Source;
lastFrame = ctx.ImageContainer.FrameCount - 1;

EncodeFrame = new AnimationBufferFrame(context);
EncodeFrame = new AnimationBufferFrame(ctx);
for (int i = 0; i < Math.Min(frames.Length, lastFrame + 1); i++)
frames[i] = new AnimationBufferFrame(context);
frames[i] = new AnimationBufferFrame(ctx);

if (ctx.Settings.EncoderInfo is IImageEncoderInfo encinfo && !encinfo.SupportsPixelFormat(lastSource.Format.FormatGuid))
{
Expand All @@ -81,32 +85,32 @@ public AnimationEncoder(PipelineContext ctx, IAnimatedImageEncoder enc)
convertSource = ctx.AddProfiler(new ConversionTransform(EncodeFrame.Source, fmt));
}

loadFrame(Current);
loadFrame(Current, anifrm);
Current.Source.Span.CopyTo(EncodeFrame.Source.Span);

moveToFrame(1);
loadFrame(Next!);
moveToFrame(1, out anifrm);
loadFrame(Next!, anifrm);
}

public void WriteGlobalMetadata() => encoder.WriteAnimationMetadata(context.Metadata);

public void WriteFrames()
{
uint bgColor = context.Metadata.TryGetMetadata<AnimationContainer>(out var anicnt) ? (uint)anicnt.BackgroundColor : default;

var ppt = context.AddProfiler(nameof(TemporalFilters));
var ppq = context.AddProfiler($"{nameof(OctreeQuantizer)}: {nameof(OctreeQuantizer.CreatePalette)}");

var encopt = context.Settings.EncoderOptions is GifEncoderOptions gifopt ? gifopt : GifEncoderOptions.Default;
writeFrame(Current, encopt, ppq);
var frame = Current;
writeFrame(frame, encopt, ppq);

while (moveNext())
{
ppt.ResumeTiming(Current.Source.Area);
TemporalFilters.Dedupe(this, bgColor, Current.Blend != AlphaBlendMethod.Source);
frame = Current;
ppt.ResumeTiming(frame.Source.Area);
TemporalFilters.Dedupe(this, frame.Disposal, (uint)anicnt.BackgroundColor, frame.Blend != AlphaBlendMethod.Source);
ppt.PauseTiming();

writeFrame(Current, encopt, ppq);
writeFrame(frame, encopt, ppq);
}
}

Expand All @@ -124,16 +128,20 @@ private bool moveNext()
if (currentFrame == lastFrame)
return false;

var frame = Current;
if (frame.Disposal is FrameDisposalMethod.RestoreBackground)
frame.Source.Clear(frame.Area, (uint)anicnt.BackgroundColor);

if (++currentFrame != lastFrame)
{
moveToFrame(currentFrame + 1);
loadFrame(Next!);
moveToFrame(currentFrame + 1, out var anifrm);
loadFrame(Next!, anifrm);
}

return true;
}

private void moveToFrame(int index)
private void moveToFrame(int index, out AnimationFrame anifrm)
{
context.ImageFrame.Dispose();
context.ImageFrame = context.ImageContainer.GetFrame(index);
Expand All @@ -143,7 +151,10 @@ private void moveToFrame(int index)
else
context.Source = context.ImageFrame.PixelSource.AsPixelSource();

MagicTransforms.AddAnimationFrameBuffer(context, false);
if (context.ImageFrame is not IMetadataSource fmsrc || !fmsrc.TryGetMetadata<AnimationFrame>(out anifrm))
anifrm = AnimationFrame.Default;

MagicTransforms.AddAnimationTransforms(context, anicnt, anifrm);

if ((context.Source is PlanarPixelSource plan ? plan.SourceY : context.Source) is IProfileSource prof)
context.AddProfiler(prof);
Expand All @@ -155,13 +166,10 @@ private void moveToFrame(int index)
}
}

private void loadFrame(AnimationBufferFrame frame)
private void loadFrame(AnimationBufferFrame frame, in AnimationFrame anifrm)
{
if (context.ImageFrame is not IMetadataSource fmsrc || !fmsrc.TryGetMetadata<AnimationFrame>(out var anifrm))
anifrm = AnimationFrame.Default;

frame.Delay = anifrm.Duration;
frame.Disposal = anifrm.Disposal == FrameDisposalMethod.RestoreBackground ? FrameDisposalMethod.RestoreBackground : FrameDisposalMethod.Preserve;
frame.Disposal = anifrm.Disposal is FrameDisposalMethod.RestoreBackground || (anifrm.Disposal is FrameDisposalMethod.RestorePrevious && anicnt.BackgroundColor is 0) ? FrameDisposalMethod.RestoreBackground : FrameDisposalMethod.Preserve;
frame.Blend = anifrm.Blend;
frame.HasTransparency = anifrm.HasAlpha;
frame.Area = context.Source.Area;
Expand All @@ -177,7 +185,7 @@ private void writeFrame(AnimationBufferFrame src, in GifEncoderOptions gifopt, I
{
if (gifopt.PredefinedPalette is not null)
{
indexedSource.SetPalette(MemoryMarshal.Cast<int, uint>(gifopt.PredefinedPalette.AsSpan()), gifopt.Dither == DitherMode.None);
indexedSource.SetPalette(MemoryMarshal.Cast<int, uint>(gifopt.PredefinedPalette.AsSpan()), gifopt.Dither is DitherMode.None);
}
else
{
Expand All @@ -186,7 +194,7 @@ private void writeFrame(AnimationBufferFrame src, in GifEncoderOptions gifopt, I
var buffCSpan = buffC.Span.Slice(src.Area.Y * buffC.Stride + src.Area.X * buffC.Format.BytesPerPixel);

bool isExact = quant.CreatePalette(gifopt.MaxPaletteSize, buffC.Format.AlphaRepresentation != PixelAlphaRepresentation.None, buffCSpan, src.Area.Width, src.Area.Height, buffC.Stride);
indexedSource.SetPalette(quant.Palette, isExact || gifopt.Dither == DitherMode.None);
indexedSource.SetPalette(quant.Palette, isExact || gifopt.Dither is DitherMode.None);
}
}

Expand Down
Loading

0 comments on commit 11e96cc

Please sign in to comment.