Skip to content

Commit 69bf150

Browse files
authored
Merge pull request #1 from timvw74/DecodeAVIF
Add AVIF Decode ability
2 parents b7adaf3 + ab029f5 commit 69bf150

11 files changed

Lines changed: 387 additions & 6 deletions

File tree

src/AVIFConfigurationModule.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using SixLabors.ImageSharp;
2+
using SixLabors.ImageSharp.Formats;
3+
4+
5+
namespace NeoSolve.ImageSharp.AVIF;
6+
7+
public sealed class AVIFConfigurationModule : IImageFormatConfigurationModule
8+
{
9+
public void Configure(Configuration configuration)
10+
{
11+
configuration.ImageFormatsManager.SetEncoder(AVIFFormat.Instance, new AVIFEncoder());
12+
configuration.ImageFormatsManager.SetDecoder(AVIFFormat.Instance, AVIFDecoder.Instance);
13+
configuration.ImageFormatsManager.AddImageFormatDetector(new AVIFImageFormatDetector());
14+
}
15+
}

src/AVIFDecoder.cs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using SixLabors.ImageSharp;
2+
using SixLabors.ImageSharp.Formats;
3+
using SixLabors.ImageSharp.Metadata;
4+
using SixLabors.ImageSharp.PixelFormats;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
13+
namespace NeoSolve.ImageSharp.AVIF;
14+
15+
public class AVIFDecoder : IImageDecoder
16+
{
17+
public static IImageDecoder Instance = new AVIFDecoder();
18+
19+
public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream)
20+
where TPixel : unmanaged, IPixel<TPixel>
21+
{
22+
return DecodeAsync<TPixel>(options, stream, CancellationToken.None).Result;
23+
}
24+
25+
public Image Decode(DecoderOptions options, Stream stream)
26+
{
27+
return DecodeAsync(options, stream, CancellationToken.None).Result;
28+
}
29+
30+
/// <summary>
31+
/// Decodes an AVIF image from a stream into an ImageSharp image,
32+
/// piping the data through avifdec.
33+
/// </summary>
34+
/// <typeparam name="TPixel">The pixel format.</typeparam>
35+
/// <param name="options">The decoder options.</param>
36+
/// <param name="stream">The input stream containing the AVIF data.</param>
37+
/// <param name="cancellationToken">A cancellation token.</param>
38+
/// <returns>A decoded image.</returns>
39+
public async Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
40+
where TPixel : unmanaged, IPixel<TPixel>
41+
{
42+
string inputFilePath = Path.GetTempFileName();
43+
string outputFilePath = Path.GetTempFileName() + ".png";
44+
45+
try
46+
{
47+
using (var fileStream = new FileStream(inputFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true))
48+
{
49+
await stream.CopyToAsync(fileStream, cancellationToken);
50+
}
51+
52+
var arguments = new List<string> { inputFilePath, outputFilePath };
53+
54+
var psi = new ProcessStartInfo
55+
{
56+
FileName = Native.CAVIFDEC,
57+
Arguments = string.Join(' ', arguments),
58+
UseShellExecute = false,
59+
CreateNoWindow = true,
60+
RedirectStandardError = true
61+
};
62+
63+
var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start avifdec process.");
64+
65+
await process.WaitForExitAsync(cancellationToken);
66+
67+
if (process.ExitCode != 0)
68+
{
69+
string error = await process.StandardError.ReadToEndAsync();
70+
throw new InvalidOperationException($"AVIF decoding failed with exit code {process.ExitCode}. Error: {error}");
71+
}
72+
73+
using (var outputStream = new FileStream(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, true))
74+
{
75+
return await Image.LoadAsync<TPixel>(outputStream, cancellationToken);
76+
}
77+
}
78+
finally
79+
{
80+
File.Delete(inputFilePath);
81+
File.Delete(outputFilePath);
82+
}
83+
}
84+
85+
86+
public async Task<Image> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
87+
{
88+
return await DecodeAsync<Rgba32>(options, stream, cancellationToken);
89+
}
90+
91+
92+
public ImageInfo Identify(DecoderOptions options, Stream stream)
93+
{
94+
return IdentifyAsync(options, stream, CancellationToken.None).Result;
95+
}
96+
97+
public async Task<ImageInfo> IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
98+
{
99+
string tempFilePath = Path.GetTempFileName();
100+
101+
try
102+
{
103+
await using (var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true))
104+
{
105+
await stream.CopyToAsync(fileStream, cancellationToken);
106+
}
107+
108+
stream.Position = 0;
109+
110+
var process = new Process
111+
{
112+
StartInfo = new ProcessStartInfo
113+
{
114+
FileName = Native.CAVIFDEC,
115+
Arguments = $"--info \"{tempFilePath}\"",
116+
RedirectStandardOutput = true,
117+
RedirectStandardError = true,
118+
UseShellExecute = false,
119+
CreateNoWindow = true
120+
}
121+
};
122+
123+
process.Start();
124+
await process.WaitForExitAsync(cancellationToken);
125+
126+
if (process.ExitCode == 0)
127+
{
128+
string output = await process.StandardOutput.ReadToEndAsync();
129+
var avifInfo = ParseAVIFInfo(output);
130+
var imageMetadata = new ImageMetadata();
131+
132+
Size size = new(avifInfo.Width, avifInfo.Height);
133+
return new ImageInfo(new PixelTypeInfo(avifInfo.BitDepth), size, imageMetadata);
134+
}
135+
else
136+
{
137+
string error = await process.StandardError.ReadToEndAsync();
138+
return null;
139+
}
140+
}
141+
finally
142+
{
143+
if (File.Exists(tempFilePath))
144+
{
145+
File.Delete(tempFilePath);
146+
}
147+
}
148+
}
149+
150+
public static AVIFInfo ParseAVIFInfo(string text)
151+
{
152+
var avifInfo = new AVIFInfo();
153+
var reader = new StringReader(text);
154+
string line;
155+
156+
while ((line = reader.ReadLine()) != null)
157+
{
158+
if (string.IsNullOrWhiteSpace(line) || !line.Contains(":"))
159+
{
160+
continue;
161+
}
162+
163+
var parts = line.Split(':', 2, StringSplitOptions.TrimEntries);
164+
var key = parts[0].Trim().Replace("*", "").Trim();
165+
var value = parts[1].Trim();
166+
167+
switch (key)
168+
{
169+
case "Resolution":
170+
var resolutionParts = value.Split('x');
171+
if (resolutionParts.Length == 2)
172+
{
173+
avifInfo.Width = int.Parse(resolutionParts[0].Trim());
174+
avifInfo.Height = int.Parse(resolutionParts[1].Trim());
175+
}
176+
break;
177+
case "Bit Depth":
178+
avifInfo.BitDepth = int.Parse(value);
179+
break;
180+
case "Format":
181+
avifInfo.Format = value;
182+
break;
183+
case "Chroma Sam. Pos":
184+
avifInfo.ChromaSamPos = int.Parse(value);
185+
break;
186+
case "Alpha":
187+
avifInfo.Alpha = value;
188+
break;
189+
case "Range":
190+
avifInfo.Range = value;
191+
break;
192+
case "Color Primaries":
193+
avifInfo.ColorPrimaries = int.Parse(value);
194+
break;
195+
case "Transfer Char.":
196+
avifInfo.TransferChar = int.Parse(value);
197+
break;
198+
case "Matrix Coeffs.":
199+
avifInfo.MatrixCoeffs = int.Parse(value);
200+
break;
201+
case "ICC Profile":
202+
avifInfo.IccProfile = value;
203+
break;
204+
case "XMP Metadata":
205+
avifInfo.XmpMetadata = value;
206+
break;
207+
case "Exif Metadata":
208+
avifInfo.ExifMetadata = value;
209+
break;
210+
case "Transformations":
211+
avifInfo.Transformations = value;
212+
break;
213+
case "Progressive":
214+
avifInfo.Progressive = value;
215+
break;
216+
case "Gain map":
217+
avifInfo.GainMap = value;
218+
break;
219+
}
220+
}
221+
222+
return avifInfo;
223+
}
224+
}

src/AVIFEncoder.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
using System.Diagnostics;
44
using System.Globalization;
55
using System.IO;
6-
using System.Linq;
7-
using System.Reflection.Emit;
8-
using System.Text;
96
using System.Threading;
107
using System.Threading.Tasks;
118
using SixLabors.ImageSharp;
@@ -58,7 +55,7 @@ public async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, Cancel
5855

5956
var psi = new ProcessStartInfo
6057
{
61-
FileName = Native.CAVIF,
58+
FileName = Native.CAVIFENC,
6259
Arguments = string.Join(' ', arguments),
6360
RedirectStandardInput = true,
6461
RedirectStandardOutput = true

src/AVIFFormat.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
using System;
1+
using SixLabors.ImageSharp.Metadata;
22
using System.Collections.Generic;
33
using SixLabors.ImageSharp.Formats;
44

55
namespace NeoSolve.ImageSharp.AVIF;
66
public class AVIFFormat : IImageFormat {
77
public string Name => "AVIF";
8+
public static AVIFFormat Instance { get; } = new AVIFFormat();
89

910
public string DefaultMimeType => "image/avif";
1011

1112
public IEnumerable<string> MimeTypes => AVIFConstants.MimeTypes;
1213

1314
public IEnumerable<string> FileExtensions => AVIFConstants.FileExtensions;
15+
public IImageDecoder Decoder { get; } = new AVIFDecoder();
16+
public IImageEncoder Encoder { get; } = new AVIFEncoder();
17+
18+
public ImageMetadata CreateDefaultFormatMetadata() => new();
1419
}

src/AVIFImageFormatDetector.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using SixLabors.ImageSharp.Formats;
2+
using System;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace NeoSolve.ImageSharp.AVIF;
6+
7+
public class AVIFImageFormatDetector : IImageFormatDetector
8+
{
9+
public int HeaderSize => 12;
10+
11+
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat format)
12+
{
13+
bool isAVIF = header.Length <= HeaderSize && IsAvif(header);
14+
format = isAVIF ? AVIFFormat.Instance : null;
15+
return isAVIF;
16+
}
17+
18+
private static bool IsAvif(ReadOnlySpan<byte> header)
19+
{
20+
// Check for the 'ftyp' box type at byte offset 4.
21+
if (header[4] != 'f' || header[5] != 't' || header[6] != 'y' || header[7] != 'p')
22+
{
23+
return false;
24+
}
25+
26+
// Check for the 'avif' brand at byte offset 8.
27+
if (header[8] == 'a' && header[9] == 'v' && header[10] == 'i' && header[11] == 'f')
28+
{
29+
return true;
30+
}
31+
32+
return false;
33+
}
34+
}

src/AVIFInfo.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace NeoSolve.ImageSharp.AVIF;
2+
3+
public class AVIFInfo
4+
{
5+
public int Width { get; set; }
6+
public int Height { get; set; }
7+
public int BitDepth { get; set; }
8+
public string Format { get; set; }
9+
public int ChromaSamPos { get; set; }
10+
public string Alpha { get; set; }
11+
public string Range { get; set; }
12+
public int ColorPrimaries { get; set; }
13+
public int TransferChar { get; set; }
14+
public int MatrixCoeffs { get; set; }
15+
public string IccProfile { get; set; }
16+
public string XmpMetadata { get; set; }
17+
public string ExifMetadata { get; set; }
18+
public string Transformations { get; set; }
19+
public string Progressive { get; set; }
20+
public string GainMap { get; set; }
21+
}

src/Native.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
namespace NeoSolve.ImageSharp.AVIF;
66

77
public static class Native {
8-
public static string CAVIF => Path.Combine("native", OSFolder, "avifenc") + ExecutableExtension;
8+
public static string CAVIFENC => Path.Combine("native", OSFolder, "avifenc") + ExecutableExtension;
9+
public static string CAVIFDEC => Path.Combine("native", OSFolder, "avifdec") + ExecutableExtension;
910

1011
private static string OSFolder {
1112
get {

src/native/linux/avifdec

13.9 MB
Binary file not shown.

src/native/win-x64/avifdec.exe

11.6 MB
Binary file not shown.

0 commit comments

Comments
 (0)