Skip to content

Commit

Permalink
add random-access GIF decoding support
Browse files Browse the repository at this point in the history
  • Loading branch information
saucecontrol committed Mar 11, 2020
1 parent 6e13bba commit 779b676
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 101 deletions.
2 changes: 1 addition & 1 deletion modules/WicInterop
Submodule WicInterop updated 2 files
+13 −0 src/PropVariant.cs
+119 −107 src/WinCodec.cs
1 change: 1 addition & 0 deletions src/MagicScaler/Core/ColorMatrix.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Numerics;

using PhotoSauce.MagicScaler.Transforms;

namespace PhotoSauce.MagicScaler
Expand Down
10 changes: 9 additions & 1 deletion src/MagicScaler/Core/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public enum Orientation
Rotate270 = 8
}

/// <summary>Defines the modes that control <a href="https://magnushoff.com/jpeg-orientation.html">Exif Orientation</a> correction.</summary>
/// <summary>Defines the modes that control <a href="https://magnushoff.com/articles/jpeg-orientation/">Exif Orientation</a> correction.</summary>
public enum OrientationMode
{
/// <summary>Correct the image orientation according to the Exif tag on load. Save the output in normal orientation. This option ensures maximum compatibility with viewer software.</summary>
Expand Down Expand Up @@ -159,4 +159,12 @@ public enum ChromaSubsampleMode
[EditorBrowsable(EditorBrowsableState.Never)]
Subsample440 = 4
}

internal enum GifDisposalMethod
{
Undefined = 0,
Preserve = 1,
RestoreBackground = 2,
RestorePrevious = 3
}
}
57 changes: 43 additions & 14 deletions src/MagicScaler/Core/ImageFileInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.IO;
using System.Collections.Generic;

using PhotoSauce.Interop.Wic;

namespace PhotoSauce.MagicScaler
{
/// <summary>Represents basic information about an image container.</summary>
Expand All @@ -17,7 +19,7 @@ public readonly struct FrameInfo
/// <summary>True if the image frame contains transparency data, otherwise false.</summary>
public bool HasAlpha { get; }
/// <summary>
/// The stored <a href="https://magnushoff.com/jpeg-orientation.html">Exif orientation</a> for the image frame.
/// The stored <a href="https://magnushoff.com/articles/jpeg-orientation/">Exif orientation</a> for the image frame.
/// The <see cref="Width" /> and <see cref="Height" /> values reflect the corrected orientation, not the stored orientation.
/// </summary>
public Orientation ExifOrientation { get; }
Expand Down Expand Up @@ -71,7 +73,7 @@ public static ImageFileInfo Load(string imgPath)
throw new FileNotFoundException("File not found", imgPath);

using var ctx = new PipelineContext(new ProcessImageSettings());
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx);

return fromWicImage(ctx, fi.Length, fi.LastWriteTimeUtc);
}
Expand All @@ -89,7 +91,7 @@ unsafe public static ImageFileInfo Load(ReadOnlySpan<byte> imgBuffer, DateTime l
fixed (byte* pbBuffer = imgBuffer)
{
using var ctx = new PipelineContext(new ProcessImageSettings());
ctx.ImageContainer = WicImageDecoder.Load(pbBuffer, imgBuffer.Length, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(pbBuffer, imgBuffer.Length, ctx);

return fromWicImage(ctx, imgBuffer.Length, lastModified);
}
Expand All @@ -108,31 +110,58 @@ public static ImageFileInfo Load(Stream imgStream, DateTime lastModified)
if (imgStream.Length <= 0 || imgStream.Position >= imgStream.Length) throw new ArgumentException("Input Stream is empty or positioned at its end", nameof(imgStream));

using var ctx = new PipelineContext(new ProcessImageSettings());
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx);

return fromWicImage(ctx, imgStream.Length, lastModified);
}

private static ImageFileInfo fromWicImage(PipelineContext ctx, long fileSize, DateTime fileDate)
{
var frames = new FrameInfo[ctx.ImageContainer.FrameCount];
var cont = (WicImageContainer)ctx.ImageContainer;
var cfmt = cont.ContainerFormat;
var frames = cfmt == FileFormat.Gif ? getGifFrameInfo(cont) : getFrameInfo(cont);

return new ImageFileInfo(cfmt, frames, fileSize, fileDate);
}

private static FrameInfo[] getFrameInfo(WicImageContainer cont)
{
var frames = new FrameInfo[cont.FrameCount];
for (int i = 0; i < frames.Length; i++)
{
using var frame = (WicImageFrame)ctx.ImageContainer.GetFrame(i);

ctx.ImageFrame = frame;
ctx.Source = frame.Source;
using var frame = (WicImageFrame)cont.GetFrame(i);
frame.WicSource.GetSize(out uint width, out uint height);

int width = ctx.Source.Width;
int height = ctx.Source.Height;
var orient = ctx.ImageFrame.ExifOrientation;
var pixfmt = PixelFormat.FromGuid(frame.WicSource.GetPixelFormat());
var orient = frame.ExifOrientation;
if (orient.SwapsDimensions())
(width, height) = (height, width);

frames[i] = new FrameInfo(width, height, ctx.Source.Format.AlphaRepresentation != PixelAlphaRepresentation.None, orient);
frames[i] = new FrameInfo((int)width, (int)height, pixfmt.AlphaRepresentation != PixelAlphaRepresentation.None, orient);
}

return new ImageFileInfo(ctx.ImageContainer.ContainerFormat, frames, fileSize, fileDate);
return frames;
}

private static FrameInfo[] getGifFrameInfo(WicImageContainer cont)
{
using var cmeta = ComHandle.Wrap(cont.WicDecoder.GetMetadataQueryReader());
int cwidth = cmeta.ComObject.GetValueOrDefault<ushort>(Wic.Metadata.Gif.LogicalScreenWidth);
int cheight = cmeta.ComObject.GetValueOrDefault<ushort>(Wic.Metadata.Gif.LogicalScreenHeight);

bool alpha = cont.FrameCount > 1;
if (!alpha)
{
using var frame = ComHandle.Wrap(cont.WicDecoder.GetFrame(0));
using var fmeta = ComHandle.Wrap(frame.ComObject.GetMetadataQueryReader());
alpha = fmeta.ComObject.GetValueOrDefault<bool>(Wic.Metadata.Gif.TransparencyFlag);
}

var frames = new FrameInfo[cont.FrameCount];
for (int i = 0; i < frames.Length; i++)
frames[i] = new FrameInfo(cwidth, cheight, alpha, Orientation.Normal);

return frames;
}
}
}
2 changes: 1 addition & 1 deletion src/MagicScaler/Core/PipelineContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public PipelineContext(ProcessImageSettings settings)
{
Settings = settings.Clone();

// HACK this quiets the nullable warnings for now but needs refactoring
// https://github.com/dotnet/runtime/issues/31877
UsedSettings = null!;
ImageContainer = null!;
ImageFrame = null!;
Expand Down
4 changes: 2 additions & 2 deletions src/MagicScaler/Core/PixelFormats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,14 @@ private static ReadOnlyDictionary<Guid, PixelFormat> getFormatCache()

uint count = 10u;
var formats = new object[count];
using var cenum = new ComHandle<IEnumUnknown>(Wic.Factory.CreateComponentEnumerator(WICComponentType.WICPixelFormat, WICComponentEnumerateOptions.WICComponentEnumerateDefault));
using var cenum = ComHandle.Wrap(Wic.Factory.CreateComponentEnumerator(WICComponentType.WICPixelFormat, WICComponentEnumerateOptions.WICComponentEnumerateDefault));

do
{
count = cenum.ComObject.Next(count, formats);
for (int i = 0; i < count; i++)
{
using var pixh = new ComHandle<IWICPixelFormatInfo2>(formats[i]);
using var pixh = ComHandle.QueryInterface<IWICPixelFormatInfo2>(formats[i]);
var pix = pixh.ComObject;

uint cch = pix.GetFriendlyName(0, null);
Expand Down
12 changes: 6 additions & 6 deletions src/MagicScaler/Magic/MagicImageProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static ProcessImageResult ProcessImage(string imgPath, Stream outStream,
checkOutStream(outStream);

using var ctx = new PipelineContext(settings);
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx);

buildPipeline(ctx);
return WriteOutput(ctx, outStream);
Expand All @@ -74,7 +74,7 @@ unsafe public static ProcessImageResult ProcessImage(ReadOnlySpan<byte> imgBuffe
fixed (byte* pbBuffer = imgBuffer)
{
using var ctx = new PipelineContext(settings);
ctx.ImageContainer = WicImageDecoder.Load(pbBuffer, imgBuffer.Length, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(pbBuffer, imgBuffer.Length, ctx);

buildPipeline(ctx);
return WriteOutput(ctx, outStream);
Expand All @@ -90,7 +90,7 @@ public static ProcessImageResult ProcessImage(Stream imgStream, Stream outStream
checkOutStream(outStream);

using var ctx = new PipelineContext(settings);
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx);

buildPipeline(ctx);
return WriteOutput(ctx, outStream);
Expand Down Expand Up @@ -139,7 +139,7 @@ public static ProcessingPipeline BuildPipeline(string imgPath, ProcessImageSetti
if (settings is null) throw new ArgumentNullException(nameof(settings));

var ctx = new PipelineContext(settings);
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx);

buildPipeline(ctx, false);
return new ProcessingPipeline(ctx);
Expand All @@ -155,7 +155,7 @@ unsafe public static ProcessingPipeline BuildPipeline(ReadOnlySpan<byte> imgBuff
fixed (byte* pbBuffer = imgBuffer)
{
var ctx = new PipelineContext(settings);
ctx.ImageContainer = WicImageDecoder.Load(pbBuffer, imgBuffer.Length, ctx.WicContext, true);
ctx.ImageContainer = WicImageDecoder.Load(pbBuffer, imgBuffer.Length, ctx, true);

buildPipeline(ctx, false);
return new ProcessingPipeline(ctx);
Expand All @@ -170,7 +170,7 @@ public static ProcessingPipeline BuildPipeline(Stream imgStream, ProcessImageSet
checkInStream(imgStream);

var ctx = new PipelineContext(settings);
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx.WicContext);
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx);

buildPipeline(ctx, false);
return new ProcessingPipeline(ctx);
Expand Down
152 changes: 152 additions & 0 deletions src/MagicScaler/Magic/OverlayTransform.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System;

#if HWINTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
#endif

namespace PhotoSauce.MagicScaler.Transforms
{
internal class OverlayTransform : PixelSource, IDisposable
{
const int bytesPerPixel = 4;

private readonly PixelSource overSource;
private readonly int offsX, offsY;
private readonly bool passthrough;

private ArraySegment<byte> lineBuff;

public OverlayTransform(PixelSource source, PixelSource over, int left, int top, bool alpha, bool replay = false) : base(source)
{
if (Format.NumericRepresentation != PixelNumericRepresentation.UnsignedInteger || Format.ChannelCount != bytesPerPixel || Format.BitsPerPixel != bytesPerPixel * 8)
throw new NotSupportedException("Pixel format not supported.");

if (over.Format != Format)
throw new NotSupportedException("Sources must be same pixel format.");

overSource = over;
offsX = left;
offsY = top;
passthrough = replay;

if (alpha)
lineBuff = BufferPool.Rent(over.Width * bytesPerPixel, true);
}

unsafe protected override void CopyPixelsInternal(in PixelArea prc, int cbStride, int cbBufferSize, IntPtr pbBuffer)
{
var inner = new PixelArea(offsX, offsY, overSource.Width, overSource.Height);

int tx = Math.Max(prc.X - inner.X, 0);
int tw = Math.Min(prc.Width, Math.Min(Math.Max(prc.X + prc.Width - inner.X, 0), inner.Width - tx));
int cx = Math.Max(inner.X - prc.X, 0);
byte* pb = (byte*)pbBuffer;

for (int y = 0; y < prc.Height; y++)
{
int cy = prc.Y + y;

if (!passthrough || tw < prc.Width || cy < inner.Y || cy >= inner.Y + inner.Height)
{
Profiler.PauseTiming();
Source.CopyPixels(new PixelArea(prc.X, cy, prc.Width, 1), cbStride, cbBufferSize, (IntPtr)pb);
Profiler.ResumeTiming();
}

if (tw > 0 && cy >= inner.Y && cy < inner.Y + inner.Height)
{
var area = new PixelArea(tx, cy - inner.Y, tw, 1);
var ptr = (IntPtr)(pb + cx * bytesPerPixel);

if (lineBuff.Array is null)
copyPixelsDirect(area, cbStride, cbBufferSize, ptr);
else
copyPixelsBuffered(area, ptr);
}

pb += cbStride;
}
}

private void copyPixelsDirect(in PixelArea prc, int cbStride, int cbBufferSize, IntPtr pbBuffer)
{
Profiler.PauseTiming();
overSource.CopyPixels(prc, cbStride, cbBufferSize, pbBuffer);
Profiler.ResumeTiming();
}

unsafe private void copyPixelsBuffered(in PixelArea prc, IntPtr pbBuffer)
{
fixed (byte* buff = &lineBuff.Array![lineBuff.Offset])
{
Profiler.PauseTiming();
overSource.CopyPixels(prc, lineBuff.Count, lineBuff.Count, (IntPtr)buff);
Profiler.ResumeTiming();

uint* ip = (uint*)buff, ipe = ip + prc.Width;
uint* op = (uint*)pbBuffer;

#if HWINTRINSICS
var shuffleMaskAlpha = (ReadOnlySpan<byte>)(new byte[] { 3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15 });

if (Avx2.IsSupported && prc.Width >= Vector256<uint>.Count)
{
var vshufa = Avx2.BroadcastVector128ToVector256((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(shuffleMaskAlpha)));

ipe -= Vector256<uint>.Count;
do
{
var vi = Avx.LoadVector256(ip);
ip += Vector256<uint>.Count;

var va = Avx2.Shuffle(vi.AsByte(), vshufa).AsUInt32();
var vo = Avx2.Or(Avx2.And(va, vi), Avx2.AndNot(va, Avx.LoadVector256(op)));

Avx.Store(op, vo);
op += Vector256<uint>.Count;

} while (ip <= ipe);
ipe += Vector256<uint>.Count;
}
else if (Ssse3.IsSupported && prc.Width >= Vector128<uint>.Count)
{
var vshufa = Sse2.LoadVector128((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(shuffleMaskAlpha)));

ipe -= Vector128<uint>.Count;
do
{
var vi = Sse2.LoadVector128(ip);
ip += Vector128<uint>.Count;

var va = Ssse3.Shuffle(vi.AsByte(), vshufa).AsUInt32();
var vo = Sse2.Or(Sse2.And(va, vi), Sse2.AndNot(va, Sse2.LoadVector128(op)));

Sse2.Store(op, vo);
op += Vector128<uint>.Count;

} while (ip <= ipe);
ipe += Vector128<uint>.Count;
}
#endif

while (ip < ipe)
{
uint i = *ip++;
if (i >> 24 != 0)
*op = i;

op++;
}
}
}

public void Dispose()
{
BufferPool.Return(lineBuff);
lineBuff = default;
}
}
}
7 changes: 3 additions & 4 deletions src/MagicScaler/Magic/PixelSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,14 @@ public void Dispose() { }

internal class PixelSourceContainer : IImageContainer
{
private readonly PixelSourceFrame frame;
private readonly IPixelSource pixelSource;

public FileFormat ContainerFormat => FileFormat.Unknown;

public int FrameCount => 1;

public PixelSourceContainer(IPixelSource source) => frame = new PixelSourceFrame(source);
public PixelSourceContainer(IPixelSource source) => pixelSource = source;

public IImageFrame GetFrame(int index) => index == 0 ? frame : throw new IndexOutOfRangeException();
public IImageFrame GetFrame(int index) => index == 0 ? new PixelSourceFrame(pixelSource) : throw new IndexOutOfRangeException();
}

internal class NoopPixelSource : PixelSource
Expand Down
Loading

0 comments on commit 779b676

Please sign in to comment.