Skip to content

Commit 779b676

Browse files
committed
add random-access GIF decoding support
1 parent 6e13bba commit 779b676

16 files changed

+584
-101
lines changed

modules/WicInterop

src/MagicScaler/Core/ColorMatrix.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Numerics;
2+
23
using PhotoSauce.MagicScaler.Transforms;
34

45
namespace PhotoSauce.MagicScaler

src/MagicScaler/Core/Enums.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public enum Orientation
115115
Rotate270 = 8
116116
}
117117

118-
/// <summary>Defines the modes that control <a href="https://magnushoff.com/jpeg-orientation.html">Exif Orientation</a> correction.</summary>
118+
/// <summary>Defines the modes that control <a href="https://magnushoff.com/articles/jpeg-orientation/">Exif Orientation</a> correction.</summary>
119119
public enum OrientationMode
120120
{
121121
/// <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>
@@ -159,4 +159,12 @@ public enum ChromaSubsampleMode
159159
[EditorBrowsable(EditorBrowsableState.Never)]
160160
Subsample440 = 4
161161
}
162+
163+
internal enum GifDisposalMethod
164+
{
165+
Undefined = 0,
166+
Preserve = 1,
167+
RestoreBackground = 2,
168+
RestorePrevious = 3
169+
}
162170
}

src/MagicScaler/Core/ImageFileInfo.cs

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.IO;
33
using System.Collections.Generic;
44

5+
using PhotoSauce.Interop.Wic;
6+
57
namespace PhotoSauce.MagicScaler
68
{
79
/// <summary>Represents basic information about an image container.</summary>
@@ -17,7 +19,7 @@ public readonly struct FrameInfo
1719
/// <summary>True if the image frame contains transparency data, otherwise false.</summary>
1820
public bool HasAlpha { get; }
1921
/// <summary>
20-
/// The stored <a href="https://magnushoff.com/jpeg-orientation.html">Exif orientation</a> for the image frame.
22+
/// The stored <a href="https://magnushoff.com/articles/jpeg-orientation/">Exif orientation</a> for the image frame.
2123
/// The <see cref="Width" /> and <see cref="Height" /> values reflect the corrected orientation, not the stored orientation.
2224
/// </summary>
2325
public Orientation ExifOrientation { get; }
@@ -71,7 +73,7 @@ public static ImageFileInfo Load(string imgPath)
7173
throw new FileNotFoundException("File not found", imgPath);
7274

7375
using var ctx = new PipelineContext(new ProcessImageSettings());
74-
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx.WicContext);
76+
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx);
7577

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

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

110112
using var ctx = new PipelineContext(new ProcessImageSettings());
111-
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx.WicContext);
113+
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx);
112114

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

116118
private static ImageFileInfo fromWicImage(PipelineContext ctx, long fileSize, DateTime fileDate)
117119
{
118-
var frames = new FrameInfo[ctx.ImageContainer.FrameCount];
120+
var cont = (WicImageContainer)ctx.ImageContainer;
121+
var cfmt = cont.ContainerFormat;
122+
var frames = cfmt == FileFormat.Gif ? getGifFrameInfo(cont) : getFrameInfo(cont);
123+
124+
return new ImageFileInfo(cfmt, frames, fileSize, fileDate);
125+
}
126+
127+
private static FrameInfo[] getFrameInfo(WicImageContainer cont)
128+
{
129+
var frames = new FrameInfo[cont.FrameCount];
119130
for (int i = 0; i < frames.Length; i++)
120131
{
121-
using var frame = (WicImageFrame)ctx.ImageContainer.GetFrame(i);
122-
123-
ctx.ImageFrame = frame;
124-
ctx.Source = frame.Source;
132+
using var frame = (WicImageFrame)cont.GetFrame(i);
133+
frame.WicSource.GetSize(out uint width, out uint height);
125134

126-
int width = ctx.Source.Width;
127-
int height = ctx.Source.Height;
128-
var orient = ctx.ImageFrame.ExifOrientation;
135+
var pixfmt = PixelFormat.FromGuid(frame.WicSource.GetPixelFormat());
136+
var orient = frame.ExifOrientation;
129137
if (orient.SwapsDimensions())
130138
(width, height) = (height, width);
131139

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

135-
return new ImageFileInfo(ctx.ImageContainer.ContainerFormat, frames, fileSize, fileDate);
143+
return frames;
144+
}
145+
146+
private static FrameInfo[] getGifFrameInfo(WicImageContainer cont)
147+
{
148+
using var cmeta = ComHandle.Wrap(cont.WicDecoder.GetMetadataQueryReader());
149+
int cwidth = cmeta.ComObject.GetValueOrDefault<ushort>(Wic.Metadata.Gif.LogicalScreenWidth);
150+
int cheight = cmeta.ComObject.GetValueOrDefault<ushort>(Wic.Metadata.Gif.LogicalScreenHeight);
151+
152+
bool alpha = cont.FrameCount > 1;
153+
if (!alpha)
154+
{
155+
using var frame = ComHandle.Wrap(cont.WicDecoder.GetFrame(0));
156+
using var fmeta = ComHandle.Wrap(frame.ComObject.GetMetadataQueryReader());
157+
alpha = fmeta.ComObject.GetValueOrDefault<bool>(Wic.Metadata.Gif.TransparencyFlag);
158+
}
159+
160+
var frames = new FrameInfo[cont.FrameCount];
161+
for (int i = 0; i < frames.Length; i++)
162+
frames[i] = new FrameInfo(cwidth, cheight, alpha, Orientation.Normal);
163+
164+
return frames;
136165
}
137166
}
138167
}

src/MagicScaler/Core/PipelineContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public PipelineContext(ProcessImageSettings settings)
7575
{
7676
Settings = settings.Clone();
7777

78-
// HACK this quiets the nullable warnings for now but needs refactoring
78+
// https://github.com/dotnet/runtime/issues/31877
7979
UsedSettings = null!;
8080
ImageContainer = null!;
8181
ImageFrame = null!;

src/MagicScaler/Core/PixelFormats.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,14 +295,14 @@ private static ReadOnlyDictionary<Guid, PixelFormat> getFormatCache()
295295

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

300300
do
301301
{
302302
count = cenum.ComObject.Next(count, formats);
303303
for (int i = 0; i < count; i++)
304304
{
305-
using var pixh = new ComHandle<IWICPixelFormatInfo2>(formats[i]);
305+
using var pixh = ComHandle.QueryInterface<IWICPixelFormatInfo2>(formats[i]);
306306
var pix = pixh.ComObject;
307307

308308
uint cch = pix.GetFriendlyName(0, null);

src/MagicScaler/Magic/MagicImageProcessor.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static ProcessImageResult ProcessImage(string imgPath, Stream outStream,
5555
checkOutStream(outStream);
5656

5757
using var ctx = new PipelineContext(settings);
58-
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx.WicContext);
58+
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx);
5959

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

7979
buildPipeline(ctx);
8080
return WriteOutput(ctx, outStream);
@@ -90,7 +90,7 @@ public static ProcessImageResult ProcessImage(Stream imgStream, Stream outStream
9090
checkOutStream(outStream);
9191

9292
using var ctx = new PipelineContext(settings);
93-
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx.WicContext);
93+
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx);
9494

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

141141
var ctx = new PipelineContext(settings);
142-
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx.WicContext);
142+
ctx.ImageContainer = WicImageDecoder.Load(imgPath, ctx);
143143

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

160160
buildPipeline(ctx, false);
161161
return new ProcessingPipeline(ctx);
@@ -170,7 +170,7 @@ public static ProcessingPipeline BuildPipeline(Stream imgStream, ProcessImageSet
170170
checkInStream(imgStream);
171171

172172
var ctx = new PipelineContext(settings);
173-
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx.WicContext);
173+
ctx.ImageContainer = WicImageDecoder.Load(imgStream, ctx);
174174

175175
buildPipeline(ctx, false);
176176
return new ProcessingPipeline(ctx);
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
3+
#if HWINTRINSICS
4+
using System.Runtime.Intrinsics;
5+
using System.Runtime.Intrinsics.X86;
6+
using System.Runtime.InteropServices;
7+
using System.Runtime.CompilerServices;
8+
#endif
9+
10+
namespace PhotoSauce.MagicScaler.Transforms
11+
{
12+
internal class OverlayTransform : PixelSource, IDisposable
13+
{
14+
const int bytesPerPixel = 4;
15+
16+
private readonly PixelSource overSource;
17+
private readonly int offsX, offsY;
18+
private readonly bool passthrough;
19+
20+
private ArraySegment<byte> lineBuff;
21+
22+
public OverlayTransform(PixelSource source, PixelSource over, int left, int top, bool alpha, bool replay = false) : base(source)
23+
{
24+
if (Format.NumericRepresentation != PixelNumericRepresentation.UnsignedInteger || Format.ChannelCount != bytesPerPixel || Format.BitsPerPixel != bytesPerPixel * 8)
25+
throw new NotSupportedException("Pixel format not supported.");
26+
27+
if (over.Format != Format)
28+
throw new NotSupportedException("Sources must be same pixel format.");
29+
30+
overSource = over;
31+
offsX = left;
32+
offsY = top;
33+
passthrough = replay;
34+
35+
if (alpha)
36+
lineBuff = BufferPool.Rent(over.Width * bytesPerPixel, true);
37+
}
38+
39+
unsafe protected override void CopyPixelsInternal(in PixelArea prc, int cbStride, int cbBufferSize, IntPtr pbBuffer)
40+
{
41+
var inner = new PixelArea(offsX, offsY, overSource.Width, overSource.Height);
42+
43+
int tx = Math.Max(prc.X - inner.X, 0);
44+
int tw = Math.Min(prc.Width, Math.Min(Math.Max(prc.X + prc.Width - inner.X, 0), inner.Width - tx));
45+
int cx = Math.Max(inner.X - prc.X, 0);
46+
byte* pb = (byte*)pbBuffer;
47+
48+
for (int y = 0; y < prc.Height; y++)
49+
{
50+
int cy = prc.Y + y;
51+
52+
if (!passthrough || tw < prc.Width || cy < inner.Y || cy >= inner.Y + inner.Height)
53+
{
54+
Profiler.PauseTiming();
55+
Source.CopyPixels(new PixelArea(prc.X, cy, prc.Width, 1), cbStride, cbBufferSize, (IntPtr)pb);
56+
Profiler.ResumeTiming();
57+
}
58+
59+
if (tw > 0 && cy >= inner.Y && cy < inner.Y + inner.Height)
60+
{
61+
var area = new PixelArea(tx, cy - inner.Y, tw, 1);
62+
var ptr = (IntPtr)(pb + cx * bytesPerPixel);
63+
64+
if (lineBuff.Array is null)
65+
copyPixelsDirect(area, cbStride, cbBufferSize, ptr);
66+
else
67+
copyPixelsBuffered(area, ptr);
68+
}
69+
70+
pb += cbStride;
71+
}
72+
}
73+
74+
private void copyPixelsDirect(in PixelArea prc, int cbStride, int cbBufferSize, IntPtr pbBuffer)
75+
{
76+
Profiler.PauseTiming();
77+
overSource.CopyPixels(prc, cbStride, cbBufferSize, pbBuffer);
78+
Profiler.ResumeTiming();
79+
}
80+
81+
unsafe private void copyPixelsBuffered(in PixelArea prc, IntPtr pbBuffer)
82+
{
83+
fixed (byte* buff = &lineBuff.Array![lineBuff.Offset])
84+
{
85+
Profiler.PauseTiming();
86+
overSource.CopyPixels(prc, lineBuff.Count, lineBuff.Count, (IntPtr)buff);
87+
Profiler.ResumeTiming();
88+
89+
uint* ip = (uint*)buff, ipe = ip + prc.Width;
90+
uint* op = (uint*)pbBuffer;
91+
92+
#if HWINTRINSICS
93+
var shuffleMaskAlpha = (ReadOnlySpan<byte>)(new byte[] { 3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15 });
94+
95+
if (Avx2.IsSupported && prc.Width >= Vector256<uint>.Count)
96+
{
97+
var vshufa = Avx2.BroadcastVector128ToVector256((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(shuffleMaskAlpha)));
98+
99+
ipe -= Vector256<uint>.Count;
100+
do
101+
{
102+
var vi = Avx.LoadVector256(ip);
103+
ip += Vector256<uint>.Count;
104+
105+
var va = Avx2.Shuffle(vi.AsByte(), vshufa).AsUInt32();
106+
var vo = Avx2.Or(Avx2.And(va, vi), Avx2.AndNot(va, Avx.LoadVector256(op)));
107+
108+
Avx.Store(op, vo);
109+
op += Vector256<uint>.Count;
110+
111+
} while (ip <= ipe);
112+
ipe += Vector256<uint>.Count;
113+
}
114+
else if (Ssse3.IsSupported && prc.Width >= Vector128<uint>.Count)
115+
{
116+
var vshufa = Sse2.LoadVector128((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(shuffleMaskAlpha)));
117+
118+
ipe -= Vector128<uint>.Count;
119+
do
120+
{
121+
var vi = Sse2.LoadVector128(ip);
122+
ip += Vector128<uint>.Count;
123+
124+
var va = Ssse3.Shuffle(vi.AsByte(), vshufa).AsUInt32();
125+
var vo = Sse2.Or(Sse2.And(va, vi), Sse2.AndNot(va, Sse2.LoadVector128(op)));
126+
127+
Sse2.Store(op, vo);
128+
op += Vector128<uint>.Count;
129+
130+
} while (ip <= ipe);
131+
ipe += Vector128<uint>.Count;
132+
}
133+
#endif
134+
135+
while (ip < ipe)
136+
{
137+
uint i = *ip++;
138+
if (i >> 24 != 0)
139+
*op = i;
140+
141+
op++;
142+
}
143+
}
144+
}
145+
146+
public void Dispose()
147+
{
148+
BufferPool.Return(lineBuff);
149+
lineBuff = default;
150+
}
151+
}
152+
}

src/MagicScaler/Magic/PixelSource.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,14 @@ public void Dispose() { }
9191

9292
internal class PixelSourceContainer : IImageContainer
9393
{
94-
private readonly PixelSourceFrame frame;
94+
private readonly IPixelSource pixelSource;
9595

9696
public FileFormat ContainerFormat => FileFormat.Unknown;
97-
9897
public int FrameCount => 1;
9998

100-
public PixelSourceContainer(IPixelSource source) => frame = new PixelSourceFrame(source);
99+
public PixelSourceContainer(IPixelSource source) => pixelSource = source;
101100

102-
public IImageFrame GetFrame(int index) => index == 0 ? frame : throw new IndexOutOfRangeException();
101+
public IImageFrame GetFrame(int index) => index == 0 ? new PixelSourceFrame(pixelSource) : throw new IndexOutOfRangeException();
103102
}
104103

105104
internal class NoopPixelSource : PixelSource

0 commit comments

Comments
 (0)