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

SystemDrawingCalls.cs
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.

Output System.Drawing.Common 8.0.31 and 9.0.20
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')
Output System.Drawing.Common 10.0.12
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.FromFile throws OutOfMemoryException on System.Drawing.Common 8 and 9, and ExternalException with a longer message on 10. Memory is not the problem in either case. The file is in a format the call cannot read.
  • Image.FromStream and new Bitmap(path) throw ArgumentException, “Parameter is not valid.” The parameter is the image data. A FileStream and a MemoryStream holding the same bytes both threw it.
  • ImageFormat.Webp is 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, and GetImageDecoders() listed those plus EMF, WMF and ICO. Nothing in either list carries the WebP format ID.
  • Saving to a path with ImageFormat.Webp writes 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 .webp extension gets a PNG.
  • Saving to a stream with ImageFormat.Webp throws ArgumentNullException naming a parameter called encoder: 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.

WpfCalls.cs
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.

Output Without the WebP Image Extension
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.
Output With the WebP Image Extension 1.2.31.0
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 an Image control and captured with RenderTargetBitmap, every transparent pixel came out opaque. The colours were correct. WebP files written by Google's cwebp 1.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.

Install
dotnet add package Darnelix.Webp
WebpBitmap.cs
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 Format32bppArgb bitmap is stored as blue, green, red, alpha, with alpha not premultiplied. That is exactly the decoder's Bgra32 layout, 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.Stride bytes 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:

BitmapWebp.cs
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

WebpWpf.cs
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.

WebP for .NET

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.