Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/AVIFConfigurationModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;


namespace NeoSolve.ImageSharp.AVIF;

public sealed class AVIFConfigurationModule : IImageFormatConfigurationModule
{
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(AVIFFormat.Instance, new AVIFEncoder());
configuration.ImageFormatsManager.SetDecoder(AVIFFormat.Instance, AVIFDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new AVIFImageFormatDetector());
}
}
224 changes: 224 additions & 0 deletions src/AVIFDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;


namespace NeoSolve.ImageSharp.AVIF;

public class AVIFDecoder : IImageDecoder
{
public static IImageDecoder Instance = new AVIFDecoder();

public Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
return DecodeAsync<TPixel>(options, stream, CancellationToken.None).Result;
}

public Image Decode(DecoderOptions options, Stream stream)
{
return DecodeAsync(options, stream, CancellationToken.None).Result;
}

/// <summary>
/// Decodes an AVIF image from a stream into an ImageSharp image,
/// piping the data through avifdec.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="options">The decoder options.</param>
/// <param name="stream">The input stream containing the AVIF data.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A decoded image.</returns>
public async Task<Image<TPixel>> DecodeAsync<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
where TPixel : unmanaged, IPixel<TPixel>
{
string inputFilePath = Path.GetTempFileName();
string outputFilePath = Path.GetTempFileName() + ".png";

try
{
using (var fileStream = new FileStream(inputFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true))
{
await stream.CopyToAsync(fileStream, cancellationToken);
}

var arguments = new List<string> { inputFilePath, outputFilePath };

var psi = new ProcessStartInfo
{
FileName = Native.CAVIFDEC,
Arguments = string.Join(' ', arguments),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true
};

var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start avifdec process.");

await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode != 0)
{
string error = await process.StandardError.ReadToEndAsync();
throw new InvalidOperationException($"AVIF decoding failed with exit code {process.ExitCode}. Error: {error}");
}

using (var outputStream = new FileStream(outputFilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, true))
{
return await Image.LoadAsync<TPixel>(outputStream, cancellationToken);
}
}
finally
{
File.Delete(inputFilePath);
File.Delete(outputFilePath);
Comment on lines +80 to +81
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File deletion operations can throw exceptions if files are in use or access is denied. Wrap these calls in try-catch blocks to prevent cleanup failures from propagating.

Suggested change
File.Delete(inputFilePath);
File.Delete(outputFilePath);
try
{
File.Delete(inputFilePath);
}
catch
{
// Suppress any exceptions during cleanup
}
try
{
File.Delete(outputFilePath);
}
catch
{
// Suppress any exceptions during cleanup
}

Copilot uses AI. Check for mistakes.
}
}


public async Task<Image> DecodeAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
{
return await DecodeAsync<Rgba32>(options, stream, cancellationToken);
}


public ImageInfo Identify(DecoderOptions options, Stream stream)
{
return IdentifyAsync(options, stream, CancellationToken.None).Result;
}

public async Task<ImageInfo> IdentifyAsync(DecoderOptions options, Stream stream, CancellationToken cancellationToken = default)
{
string tempFilePath = Path.GetTempFileName();

try
{
await using (var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true))
{
await stream.CopyToAsync(fileStream, cancellationToken);
}

stream.Position = 0;

var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = Native.CAVIFDEC,
Arguments = $"--info \"{tempFilePath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};

process.Start();
await process.WaitForExitAsync(cancellationToken);

if (process.ExitCode == 0)
{
string output = await process.StandardOutput.ReadToEndAsync();
var avifInfo = ParseAVIFInfo(output);
var imageMetadata = new ImageMetadata();

Size size = new(avifInfo.Width, avifInfo.Height);
return new ImageInfo(new PixelTypeInfo(avifInfo.BitDepth), size, imageMetadata);
}
else
{
string error = await process.StandardError.ReadToEndAsync();
return null;
}
}
finally
{
if (File.Exists(tempFilePath))
{
File.Delete(tempFilePath);
}
}
}

public static AVIFInfo ParseAVIFInfo(string text)
{
var avifInfo = new AVIFInfo();
var reader = new StringReader(text);
string line;

while ((line = reader.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line) || !line.Contains(":"))
{
continue;
}

var parts = line.Split(':', 2, StringSplitOptions.TrimEntries);
var key = parts[0].Trim().Replace("*", "").Trim();
var value = parts[1].Trim();

switch (key)
{
case "Resolution":
var resolutionParts = value.Split('x');
if (resolutionParts.Length == 2)
{
avifInfo.Width = int.Parse(resolutionParts[0].Trim());
avifInfo.Height = int.Parse(resolutionParts[1].Trim());
Comment on lines +173 to +174
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using int.Parse without validation can throw exceptions on malformed input. Consider using int.TryParse and handle parsing failures gracefully.

Copilot uses AI. Check for mistakes.
}
break;
case "Bit Depth":
avifInfo.BitDepth = int.Parse(value);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple uses of int.Parse without validation can throw exceptions on malformed input. Consider using int.TryParse and handle parsing failures gracefully for all integer parsing operations.

Copilot uses AI. Check for mistakes.
break;
case "Format":
avifInfo.Format = value;
break;
case "Chroma Sam. Pos":
avifInfo.ChromaSamPos = int.Parse(value);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple uses of int.Parse without validation can throw exceptions on malformed input. Consider using int.TryParse and handle parsing failures gracefully for all integer parsing operations.

Copilot uses AI. Check for mistakes.
break;
case "Alpha":
avifInfo.Alpha = value;
break;
case "Range":
avifInfo.Range = value;
break;
case "Color Primaries":
avifInfo.ColorPrimaries = int.Parse(value);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple uses of int.Parse without validation can throw exceptions on malformed input. Consider using int.TryParse and handle parsing failures gracefully for all integer parsing operations.

Copilot uses AI. Check for mistakes.
break;
case "Transfer Char.":
avifInfo.TransferChar = int.Parse(value);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple uses of int.Parse without validation can throw exceptions on malformed input. Consider using int.TryParse and handle parsing failures gracefully for all integer parsing operations.

Copilot uses AI. Check for mistakes.
break;
case "Matrix Coeffs.":
avifInfo.MatrixCoeffs = int.Parse(value);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple uses of int.Parse without validation can throw exceptions on malformed input. Consider using int.TryParse and handle parsing failures gracefully for all integer parsing operations.

Copilot uses AI. Check for mistakes.
break;
case "ICC Profile":
avifInfo.IccProfile = value;
break;
case "XMP Metadata":
avifInfo.XmpMetadata = value;
break;
case "Exif Metadata":
avifInfo.ExifMetadata = value;
break;
case "Transformations":
avifInfo.Transformations = value;
break;
case "Progressive":
avifInfo.Progressive = value;
break;
case "Gain map":
avifInfo.GainMap = value;
break;
}
}

return avifInfo;
}
}
5 changes: 1 addition & 4 deletions src/AVIFEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
Expand Down Expand Up @@ -58,7 +55,7 @@ public async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, Cancel

var psi = new ProcessStartInfo
{
FileName = Native.CAVIF,
FileName = Native.CAVIFENC,
Arguments = string.Join(' ', arguments),
RedirectStandardInput = true,
RedirectStandardOutput = true
Expand Down
7 changes: 6 additions & 1 deletion src/AVIFFormat.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
using System;
using SixLabors.ImageSharp.Metadata;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;

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

public string DefaultMimeType => "image/avif";

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

public IEnumerable<string> FileExtensions => AVIFConstants.FileExtensions;
public IImageDecoder Decoder { get; } = new AVIFDecoder();
public IImageEncoder Encoder { get; } = new AVIFEncoder();

public ImageMetadata CreateDefaultFormatMetadata() => new();
}
34 changes: 34 additions & 0 deletions src/AVIFImageFormatDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using SixLabors.ImageSharp.Formats;
using System;
using System.Diagnostics.CodeAnalysis;

namespace NeoSolve.ImageSharp.AVIF;

public class AVIFImageFormatDetector : IImageFormatDetector
{
public int HeaderSize => 12;

public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat format)
{
bool isAVIF = header.Length <= HeaderSize && IsAvif(header);
Copy link

Copilot AI Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition should be >= not <=. A header with length less than HeaderSize (12) cannot contain a valid AVIF signature that requires checking bytes at positions 4-11.

Suggested change
bool isAVIF = header.Length <= HeaderSize && IsAvif(header);
bool isAVIF = header.Length >= HeaderSize && IsAvif(header);

Copilot uses AI. Check for mistakes.
format = isAVIF ? AVIFFormat.Instance : null;
return isAVIF;
}

private static bool IsAvif(ReadOnlySpan<byte> header)
{
// Check for the 'ftyp' box type at byte offset 4.
if (header[4] != 'f' || header[5] != 't' || header[6] != 'y' || header[7] != 'p')
{
return false;
}

// Check for the 'avif' brand at byte offset 8.
if (header[8] == 'a' && header[9] == 'v' && header[10] == 'i' && header[11] == 'f')
{
return true;
}

return false;
}
}
21 changes: 21 additions & 0 deletions src/AVIFInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace NeoSolve.ImageSharp.AVIF;

public class AVIFInfo
{
public int Width { get; set; }
public int Height { get; set; }
public int BitDepth { get; set; }
public string Format { get; set; }
public int ChromaSamPos { get; set; }
public string Alpha { get; set; }
public string Range { get; set; }
public int ColorPrimaries { get; set; }
public int TransferChar { get; set; }
public int MatrixCoeffs { get; set; }
public string IccProfile { get; set; }
public string XmpMetadata { get; set; }
public string ExifMetadata { get; set; }
public string Transformations { get; set; }
public string Progressive { get; set; }
public string GainMap { get; set; }
}
3 changes: 2 additions & 1 deletion src/Native.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
namespace NeoSolve.ImageSharp.AVIF;

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

private static string OSFolder {
get {
Expand Down
Binary file added src/native/linux/avifdec
Binary file not shown.
Binary file added src/native/win-x64/avifdec.exe
Binary file not shown.
Loading
Loading