.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. |
MIT |
Bindings to Google’s Skia, with native assets per platform. JPEG, PNG and WebP encode, plus drawing. Fast and widely used. |
|
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 });
}
}
Reference: ImageSharp documentation.
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. |
Encoder only. |
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 |
|---|---|---|
MIT |
.NET wrapper over the ffmpeg and ffprobe executables, with a fluent argument builder and helpers for snapshots. |
|
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.