.NET Image and Video Libraries

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. Latest stable versions listed on NuGet on that date: SixLabors.ImageSharp 4.1.2, SkiaSharp 4.153.1, NetVips 3.2.0, BlurHashSharp.SkiaSharp 1.3.4, FFMpegCore 5.5.0 and Xabe.FFmpeg 6.1.1. The snippets below follow the documented APIs of each library. They were not compiled for this page, and ImageSharp and SkiaSharp have changed their APIs between major versions, so check the release notes of the version you install.

Image libraries

Library Licence Notes

ImageSharp (SixLabors.ImageSharp)

Six Labors Split License

Fully managed code, no native dependencies. JPEG, PNG, GIF, BMP, TIFF and WebP. No AVIF or HEIC. Under the Six Labors Split License a commercial licence is needed above US$1M revenue. Read the license text for the exact terms before you ship it in a product.

SkiaSharp

MIT

Bindings to Google’s Skia, with native assets per platform. JPEG, PNG and WebP encode, plus drawing. Fast and widely used.

NetVips

MIT

Bindings to libvips, with the native library in a companion package. The best throughput and the widest format support, including AVIF and HEIC when libvips is built with libheif.

If the product is closed source and earns more than the ImageSharp threshold, the practical choices are paying for the licence, or using SkiaSharp or NetVips, which are MIT.

ImageSharp

dotnet add package SixLabors.ImageSharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;

public static async Task MakeVariantsAsync(string input, string outDir)
{
    foreach (var width in new[] { 320, 640, 1280 })
    {
        using var image = await Image.LoadAsync(input);
        image.Mutate(x => x
            .AutoOrient()                                      // apply EXIF orientation
            .Resize(new ResizeOptions { Size = new Size(width, 0), Mode = ResizeMode.Max }));
        image.Metadata.ExifProfile = null;                     // drop EXIF, including GPS
        await image.SaveAsync($"{outDir}/w{width}.webp", new WebpEncoder { Quality = 78 });
        await image.SaveAsync($"{outDir}/w{width}.jpg", new JpegEncoder { Quality = 80 });
    }
}

SkiaSharp

using SkiaSharp;

public static void MakeWebp(string input, string output, int width)
{
    using var original = SKBitmap.Decode(input);
    var height = (int)Math.Round(original.Height * (double)width / original.Width);
    using var resized = original.Resize(new SKImageInfo(width, height), new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear));
    using var image = SKImage.FromBitmap(resized);
    using var data = image.Encode(SKEncodedImageFormat.Webp, 78);
    using var stream = File.OpenWrite(output);
    data.SaveTo(stream);
}

SkiaSharp does not apply EXIF orientation automatically for bitmaps. Use SKCodec.EncodedOrigin to find the rotation and draw the bitmap on a rotated canvas. Reference: SkiaSharp API documentation.

NetVips

dotnet add package NetVips
dotnet add package NetVips.Native
using NetVips;

// Thumbnail() shrinks on load, so it is much cheaper than load-then-resize.
using var image = Image.Thumbnail("original.jpg", 640, size: Enums.Size.Down);
// keep: ForeignKeep.None strips all metadata (libvips 8.15 replaced the old strip option with keep).
image.Webpsave("w640.webp", q: 78, keep: Enums.ForeignKeep.None);
image.Heifsave("w640.avif", q: 60, compression: Enums.ForeignHeifCompression.Av1, keep: Enums.ForeignKeep.None);

Reference: NetVips documentation and the libvips API.

BlurHash in .NET

Package Notes

Blurhash.ImageSharp, Blurhash.SkiaSharp and Blurhash.Core (blurhash.net)

Encoder and decoder with one package per imaging library. Pick the one that matches your image library.

BlurHashSharp.SkiaSharp and BlurHashSharp.ImageSharp

Encoder only. BlurHashEncoder.Encode(xComponent, yComponent, stream) reads an encoded image from a stream or a path, and there is an overload that resizes the image before encoding.

using BlurHashSharp.SkiaSharp;

await using var stream = File.OpenRead("original.jpg");
string hash = BlurHashEncoder.Encode(4, 3, stream);       // 4 x 3 components, 28 characters

Resize to about 64 pixels before encoding when your library lets you, see the BlurHash page.

Video libraries

Library Licence Notes

FFMpegCore

MIT

.NET wrapper over the ffmpeg and ffprobe executables, with a fluent argument builder and helpers for snapshots.

Xabe.FFmpeg

Non-commercial licence

Free for non-commercial use. Commercial use needs a paid licence, so check the terms before you use it in a product.

Both need FFmpeg and FFprobe installed (or downloaded at startup) and accessible to the process.

FFMpegCore: probe, transcode and snapshot

dotnet add package FFMpegCore
using FFMpegCore;
using FFMpegCore.Enums;
using System.Drawing;

GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "/usr/bin" });

var info = await FFProbe.AnalyseAsync("input.mov");            // reject unreasonable input first
var duration = info.Duration;

await FFMpegArguments
    .FromFileInput("input.mov")
    .OutputToFile("720p.mp4", overwrite: true, options => options
        .WithVideoCodec(VideoCodec.LibX264)
        .WithConstantRateFactor(22)
        .WithAudioCodec(AudioCodec.Aac)
        .WithVideoFilters(filter => filter.Scale(-2, 720))
        .WithFastStart())
    .ProcessAsynchronously();

FFMpeg.Snapshot("input.mov", "cover.jpg", new Size(720, -1), TimeSpan.FromSeconds(duration.TotalSeconds * 0.1));

This follows the examples in the FFMpegCore README. System.Drawing Size is used by the snapshot helper, and FFMpegCore.Extensions.SkiaSharp offers an in-memory variant.

Xabe.FFmpeg

using Xabe.FFmpeg;

FFmpeg.SetExecutablesPath("/usr/bin");
IMediaInfo info = await FFmpeg.GetMediaInfo("input.mov");
IVideoStream video = info.VideoStreams.First()!.SetCodec(VideoCodec.h264).SetSize(VideoSize.Hd720);
IConversion conversion = FFmpeg.Conversions.New().AddStream(video).AddStream(info.AudioStreams.First()!)
    .SetOutput("720p.mp4");
await conversion.Start();

Reference: Xabe.FFmpeg. For fine control of the encoder options, give FFmpeg the argument list directly (AddParameter).

ASP.NET Core upload endpoint with BackgroundService and Channel

For small workloads in one process, a bounded Channel<T> read by a BackgroundService is a simple in-memory queue. It is lost when the process restarts, so it is suited to images that can be regenerated, not to the only copy of a job.

using System.Threading.Channels;

public record ProcessMedia(Guid Id, string Path);

public sealed class MediaQueue
{
    private readonly Channel<ProcessMedia> _channel = Channel.CreateBounded<ProcessMedia>(100);
    public ValueTask EnqueueAsync(ProcessMedia job) => _channel.Writer.WriteAsync(job);
    public IAsyncEnumerable<ProcessMedia> ReadAllAsync(CancellationToken ct) => _channel.Reader.ReadAllAsync(ct);
}

public sealed class MediaWorker(MediaQueue queue, IServiceScopeFactory scopes, ILogger<MediaWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in queue.ReadAllAsync(stoppingToken))
        {
            try
            {
                using var scope = scopes.CreateScope();
                var processor = scope.ServiceProvider.GetRequiredService<IMediaProcessor>();
                await processor.ProcessAsync(job, stoppingToken);       // idempotent: skip if already READY
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                logger.LogError(ex, "Media job {Id} failed", job.Id);
            }
        }
    }
}

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MediaQueue>();
builder.Services.AddHostedService<MediaWorker>();
builder.Services.AddScoped<IMediaProcessor, MediaProcessor>();
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 10 * 1024 * 1024);

var app = builder.Build();

app.MapPost("/images", async (IFormFile file, MediaQueue queue) =>
{
    if (file.Length is 0 or > 10 * 1024 * 1024)
        return Results.BadRequest("Invalid size");
    var id = Guid.NewGuid();
    var path = Path.Combine(Path.GetTempPath(), id.ToString());
    await using (var stream = File.Create(path))
        await file.CopyToAsync(stream);
    await queue.EnqueueAsync(new ProcessMedia(id, path));
    return Results.Accepted($"/images/{id}", new { id, status = "queued" });
}).DisableAntiforgery();

app.Run();

Reference: Background tasks with hosted services in ASP.NET Core and System.Threading.Channels. IFormFile binding in minimal APIs requires DisableAntiforgery() for a public API in .NET 8 and later. Keep the CSRF decision explicit for your own authentication model.

A queue-backed alternative

When jobs must survive restarts, scale across instances or be retried, replace the channel with a broker and keep the same processor:

  • Azure Service Bus or Storage Queues with the Azure SDK, see the Azure page.

  • Amazon SQS with the AWS SDK for .NET, see the AWS pipeline.

  • RabbitMQ or Kafka with the official clients, see messaging.

  • A library such as Hangfire or Quartz.NET for persisted background jobs, see scheduling.

Publish a job from the endpoint, run a worker as a separate deployment, and give it the idempotency, retry and dead-letter settings described in upload pipelines.