Load a WebP into a System.Drawing Bitmap or a WPF image in C#
System.Drawing cannot read or write WebP, and WPF reads it only when a Microsoft Store extension is installed, so decode the file to BGRA pixels yourself.
Applies to Darnelix.Webp 1.1.0 on .NET 8, 9 and 10 · Observed on Windows 11 25H2 (build 26200.9457) with System.Drawing.Common 8.0.31, 9.0.20 and 10.0.12
None of the errors says “WebP”, which is why this takes so long to diagnose. One
call reports that it is out of memory, however small the file. Another says a
parameter is not valid and does not say which. A save with
ImageFormat.Webp raises nothing at all and writes a file that is not a
WebP. What follows is each call, run, with what it actually did.
What System.Drawing does with a WebP
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
// Every way System.Drawing has of opening or saving an image, pointed at a WebP.
public static class SystemDrawingCalls
{
public static void Report(string webpPath, string scratchPath, TextWriter output)
{
Attempt(output, "Image.FromFile(path)", () =>
{
using Image image = Image.FromFile(webpPath);
return $"loaded {image.Width}x{image.Height}";
});
Attempt(output, "Image.FromStream(stream)", () =>
{
using FileStream stream = File.OpenRead(webpPath);
using Image image = Image.FromStream(stream);
return $"loaded {image.Width}x{image.Height}";
});
Attempt(output, "new Bitmap(path)", () =>
{
using var bitmap = new Bitmap(webpPath);
return $"loaded {bitmap.Width}x{bitmap.Height}";
});
using var canvas = new Bitmap(64, 48, PixelFormat.Format32bppArgb);
Attempt(output, "Save(path, ImageFormat.Webp)", () =>
{
canvas.Save(scratchPath, ImageFormat.Webp);
return "no exception; the file is " + Identify(File.ReadAllBytes(scratchPath));
});
Attempt(output, "Save(stream, ImageFormat.Webp)", () =>
{
using var stream = new MemoryStream();
canvas.Save(stream, ImageFormat.Webp);
return "no exception; the stream holds " + Identify(stream.ToArray());
});
}
static void Attempt(TextWriter output, string call, Func<string> run)
{
string result;
try
{
result = run();
}
catch (Exception e)
{
result = $"{e.GetType().FullName}: {e.Message}";
}
output.WriteLine($"{call,-31} {result}");
}
// The container is named by its first bytes, whatever the extension says.
static string Identify(byte[] file)
{
bool png = file.Length >= 8 && file[0] == 0x89 && file[1] == 'P' && file[2] == 'N' && file[3] == 'G';
bool webp = file.Length >= 12
&& file[0] == 'R' && file[1] == 'I' && file[2] == 'F' && file[3] == 'F'
&& file[8] == 'W' && file[9] == 'E' && file[10] == 'B' && file[11] == 'P';
return png ? "a PNG" : webp ? "a WebP" : "neither a PNG nor a WebP";
}
} Run against a 64 × 48 lossless WebP with transparency. Lossy, opaque and animated files gave the same three exceptions.
Image.FromFile(path) System.OutOfMemoryException: Out of memory.
Image.FromStream(stream) System.ArgumentException: Parameter is not valid.
new Bitmap(path) System.ArgumentException: Parameter is not valid.
Save(path, ImageFormat.Webp) no exception; the file is a PNG
Save(stream, ImageFormat.Webp) System.ArgumentNullException: Value cannot be null. (Parameter 'encoder') Image.FromFile(path) System.Runtime.InteropServices.ExternalException: An object could not be created, possibly due to a lack of memory, but most likely due to invalid input.
Image.FromStream(stream) System.ArgumentException: Parameter is not valid.
new Bitmap(path) System.ArgumentException: Parameter is not valid.
Save(path, ImageFormat.Webp) no exception; the file is a PNG
Save(stream, ImageFormat.Webp) System.ArgumentNullException: Value cannot be null. (Parameter 'encoder') Read against the questions people ask about it:
-
Image.FromFilethrowsOutOfMemoryExceptionon System.Drawing.Common 8 and 9, andExternalExceptionwith a longer message on 10. Memory is not the problem in either case. The file is in a format the call cannot read. -
Image.FromStreamandnew Bitmap(path)throwArgumentException, “Parameter is not valid.” The parameter is the image data. AFileStreamand aMemoryStreamholding the same bytes both threw it. -
ImageFormat.Webpis a name, not a codec. The property exists, and its documentation says the format is supported since Windows 10 1809. On the Windows 11 build named at the top of this page,ImageCodecInfo.GetImageEncoders()listed BMP, JPEG, GIF, TIFF and PNG, andGetImageDecoders()listed those plus EMF, WMF and ICO. Nothing in either list carries the WebP format ID. - Saving to a path with
ImageFormat.Webpwrites a PNG. No exception is raised, and the file has the name you gave it, so it looks like it worked. Its first bytes are the PNG signature. Anything downstream that trusts the.webpextension gets a PNG. - Saving to a stream with
ImageFormat.WebpthrowsArgumentNullExceptionnaming a parameter calledencoder: there is no encoder to hand the image to.
Microsoft's WebP Image Extension, from the Microsoft Store, changed none of this. With it installed and with it removed, all five lines were identical.
What WPF does with a WebP
WPF decodes through the Windows Imaging Component, which does accept codecs from outside, so here the extension decides the outcome.
using System;
using System.IO;
using System.Windows.Media.Imaging;
// WPF's two ways of opening an image file, pointed at a WebP that has transparent pixels.
public static class WpfCalls
{
public static void Report(string webpPath, TextWriter output)
{
Attempt(output, "BitmapImage", () =>
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = new Uri(Path.GetFullPath(webpPath));
image.EndInit();
return $"loaded {image.PixelWidth}x{image.PixelHeight}, Format {image.Format}";
});
Attempt(output, "BitmapDecoder.Create", () =>
{
using FileStream stream = File.OpenRead(webpPath);
BitmapDecoder decoder = BitmapDecoder.Create(
stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
BitmapFrame frame = decoder.Frames[0];
return $"loaded {frame.PixelWidth}x{frame.PixelHeight}, Format {frame.Format}, " +
$"codec \"{decoder.CodecInfo.FriendlyName}\"";
});
}
static void Attempt(TextWriter output, string call, Func<string> run)
{
string result;
try
{
result = run();
}
catch (Exception e)
{
result = $"{e.GetType().FullName}: {e.Message}";
}
output.WriteLine($"{call,-21} {result}");
}
} The same 64 × 48 WebP with transparency. The output was the same on .NET 8, 9 and 10.
BitmapImage System.NotSupportedException: No imaging component suitable to complete this operation was found.
BitmapDecoder.Create System.NotSupportedException: No imaging component suitable to complete this operation was found. BitmapImage loaded 64x48, Format Bgr32
BitmapDecoder.Create loaded 64x48, Format Bgr32, codec "Microsoft Webp Decoder" - Without the extension, both calls throw
NotSupportedException: “No imaging component suitable to complete this operation was found.” - With it, the image loads and its transparency is gone. The
decoder that answered was “Microsoft Webp Decoder”, loaded from the extension's
own folder, and it reported the pixel format as
Bgr32, which has no alpha channel. Drawn by anImagecontrol and captured withRenderTargetBitmap, every transparent pixel came out opaque. The colours were correct. WebP files written by Google'scwebp1.6.0 behaved the same way, lossy and lossless. - So the result depends on the machine. The same build of your application shows the image for one user and throws for another, according to whether a Store package is installed for that user.
Load a WebP into a System.Drawing Bitmap
Decode the file yourself and fill the bitmap with the pixels. The decoder used here is Darnelix.Webp, a managed library, so what it does is the same on every machine.
dotnet add package Darnelix.Webp using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using Darnelix.Webp;
using Darnelix.Webp.Decoding;
// Both libraries have a type called PixelFormat. Name the two apart once, here.
using GdiPixelFormat = System.Drawing.Imaging.PixelFormat;
using WebpPixelFormat = Darnelix.Webp.PixelFormat;
public static class WebpBitmap
{
public static Bitmap Load(string path) => FromBytes(File.ReadAllBytes(path));
// The caller owns the Bitmap and disposes it, as with any other.
public static Bitmap FromBytes(byte[] file)
{
// BGRA with straight alpha is the memory layout of Format32bppArgb, so no channel is swapped.
var options = new WebpDecodeOptions { PixelFormat = WebpPixelFormat.Bgra32 };
using WebpImage image = WebpDecoder.Decode(file, options);
byte[] pixels = image.Pixels.ToArray();
int rowBytes = image.Width * 4;
var bitmap = new Bitmap(image.Width, image.Height, GdiPixelFormat.Format32bppArgb);
BitmapData data = bitmap.LockBits(
new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.WriteOnly, GdiPixelFormat.Format32bppArgb);
try
{
// Row by row: the decoder's rows are tightly packed, the bitmap's are data.Stride apart.
for (int y = 0; y < image.Height; y++)
{
Marshal.Copy(pixels, y * rowBytes, data.Scan0 + y * data.Stride, rowBytes);
}
}
finally
{
bitmap.UnlockBits(data);
}
return bitmap;
}
} Usage: pictureBox.Image = WebpBitmap.Load("photo.webp"); The result is an ordinary Bitmap, transparency included.
Two details carry the correctness:
- Ask for BGRA. A
Format32bppArgbbitmap is stored as blue, green, red, alpha, with alpha not premultiplied. That is exactly the decoder'sBgra32layout, so the pixels are copied and never converted. Decode to the default RGBA instead and red and blue swap. - Copy row by row. The decoder's rows are packed end to end. The
bitmap's rows are
data.Stridebytes apart, which is not promised to equal the width times four.
Save a Bitmap as WebP
The reverse, for the save that ImageFormat.Webp does not perform:
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using Darnelix.Webp;
using Darnelix.Webp.Encoding;
using GdiPixelFormat = System.Drawing.Imaging.PixelFormat;
using WebpPixelFormat = Darnelix.Webp.PixelFormat;
public static class BitmapWebp
{
public static void Save(Bitmap bitmap, string webpPath)
{
int width = bitmap.Width;
int height = bitmap.Height;
int rowBytes = width * 4;
byte[] bgra = new byte[rowBytes * height];
// Asking LockBits for Format32bppArgb converts whatever the bitmap held.
BitmapData data = bitmap.LockBits(
new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, GdiPixelFormat.Format32bppArgb);
try
{
// The encoder wants tightly packed rows, so copy each row without the bitmap's padding.
for (int y = 0; y < height; y++)
{
Marshal.Copy(data.Scan0 + y * data.Stride, bgra, y * rowBytes, rowBytes);
}
}
finally
{
bitmap.UnlockBits(data);
}
// A new WebpEncodeOptions is zero throughout. Set every field you rely on.
var options = new WebpEncodeOptions { Lossless = true, Method = 4, AlphaQuality = 100 };
File.WriteAllBytes(webpPath, WebpEncoder.Encode(bgra, width, height, WebpPixelFormat.Bgra32, options));
}
} Lossless. For a photograph, set Quality, Method and FilterStrength instead of Lossless: a new WebpEncodeOptions is zero in every field.
Load a WebP into a WPF image
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Darnelix.Webp;
using Darnelix.Webp.Decoding;
using WebpPixelFormat = Darnelix.Webp.PixelFormat;
public static class WebpWpf
{
public static BitmapSource Load(string path) => FromBytes(File.ReadAllBytes(path));
public static BitmapSource FromBytes(byte[] file)
{
// PixelFormats.Bgra32 is BGRA with straight alpha, which is what the decoder hands back.
var options = new WebpDecodeOptions { PixelFormat = WebpPixelFormat.Bgra32 };
using WebpImage image = WebpDecoder.Decode(file, options);
BitmapSource source = BitmapSource.Create(
image.Width, image.Height, 96, 96, PixelFormats.Bgra32, null,
image.Pixels.ToArray(), image.Width * 4);
// Frozen, it can be decoded on a worker thread and handed to the UI thread.
source.Freeze();
return source;
}
} Usage: image.Source = WebpWpf.Load("photo.webp"); The project needs <UseWPF>true</UseWPF>.
PixelFormats.Bgra32 is the format with a real alpha channel, so the
transparency that the extension's decoder drops is kept: drawn by an
Image control and captured the same way as above, every pixel matched
the source. Because the BitmapSource is frozen before it is returned,
the decode can run on a worker thread and the result can be assigned on the UI
thread.
Animated WebP
A Bitmap and a BitmapSource hold one image each. Handed an
animated file, WebpDecoder.Decode throws a
WebpDecodeException rather than returning the first frame unasked. To
show the first frame, set AnimatedInputHandling.DecodeFirstFrame on
the decode options. To play the animation, read it with
WebpAnimDecoder, which hands back every frame as a full image with its
duration, and swap the control's image on a timer.
Other ways to get the pixels
- Another decoder. ImageSharp, SkiaSharp and Magick.NET all read WebP. Their licences and native dependencies are set out in converting WebP to PNG or JPEG, and back.
- Relying on the extension, in WPF only. It is a reasonable
choice for a tool that runs on machines you manage and shows images with no
transparency. It does nothing for
System.Drawing. - Converting the files beforehand. If the images are yours and fixed at build time, ship PNG or JPEG and both platform APIs read them as they always have.
Also called
System.Drawing WebP support, Image.FromFile WebP out of memory, Image.FromStream WebP parameter is not valid, convert WebP to Bitmap, load a WebP image in C#, WinForms PictureBox WebP, WPF BitmapImage WebP, ImageFormat.Webp encoder — the same two facts each time: GDI+ has no WebP codec, and WPF has one only when a Store extension supplies it.
A WebP decoder that does not depend on what Windows has installed.
Darnelix.Webp is a WebP codec written entirely in C#. It decodes and encodes
lossy, lossless and animated WebP, with alpha, as one managed assembly with no
native code and no dependencies, on .NET 8, 9 and 10. It decodes straight to
the BGRA layout that System.Drawing and WPF use, and it behaves the
same whether or not the machine has a WebP codec of its own.
The package is free of charge under a proprietary licence. It is not open
source; the terms are in the LICENSE file inside the package.
Install it with dotnet add package Darnelix.Webp.