Convert WebP to PNG or JPEG, and back, in C#
Decode the WebP to raw pixels with a WebP codec and write them with a PNG or JPEG encoder; to go the other way, read the image to pixels and encode those as WebP.
Applies to Darnelix.Webp 1.1.0 on .NET 8, 9 and 10, with System.Drawing or WPF on Windows
.NET has no built-in API that does both halves. System.Drawing and WPF
read and write PNG and JPEG and have no WebP codec of their own, so
image.Save("out.webp") and Image.FromFile("in.webp") are
not the answer. A conversion is two steps joined by a buffer of raw pixels, and the
decisions that matter are which library does each step and what happens to
transparency on the way.
Which library does which half
The samples on this page use Darnelix.Webp for the WebP half. It reads and writes WebP and nothing else: it does not open a PNG or a JPEG, and it does not save one. Those come from something you already have:
- On Windows,
System.Drawingor WPF'sBitmapEncoderandBitmapDecoderclasses, which sit on the Windows Imaging Component. Both are shown below. - On Linux and macOS, neither exists. Since .NET 6,
System.Drawing.CommonthrowsPlatformNotSupportedExceptionoff Windows, and from .NET 7 there is no switch to turn it back on. The PNG or JPEG half then has to come from another library, and the ones usually used for it also handle WebP themselves. They are compared further down.
dotnet add package Darnelix.Webp A Windows Forms project already has System.Drawing. A console or ASP.NET Core project adds it with: dotnet add package System.Drawing.Common.
WebP to PNG or JPEG
using System;
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;
// Windows only: System.Drawing writes the PNG and the JPEG, Darnelix.Webp reads the WebP.
public static class WebpToImage
{
public static void ToPng(string webpPath, string pngPath)
{
using Bitmap bitmap = Decode(webpPath);
bitmap.Save(pngPath, ImageFormat.Png);
}
// JPEG has no alpha channel, so the image is flattened onto a background colour first.
public static void ToJpeg(string webpPath, string jpegPath, long quality = 90, Color? background = null)
{
using Bitmap decoded = Decode(webpPath);
using var flat = new Bitmap(decoded.Width, decoded.Height, GdiPixelFormat.Format24bppRgb);
using (Graphics graphics = Graphics.FromImage(flat))
{
graphics.Clear(background ?? Color.White);
graphics.DrawImageUnscaled(decoded, 0, 0);
}
ImageCodecInfo jpeg = Array.Find(ImageCodecInfo.GetImageEncoders(), c => c.FormatID == ImageFormat.Jpeg.Guid)!;
using var parameters = new EncoderParameters(1);
parameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
flat.Save(jpegPath, jpeg, parameters);
}
static Bitmap Decode(string webpPath)
{
byte[] file = File.ReadAllBytes(webpPath);
// 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: WebpToImage.ToPng("photo.webp", "photo.png"); or WebpToImage.ToJpeg("photo.webp", "photo.jpg", 90);
Three things in that sample are there because leaving them out produces a wrong image:
- The pixel layout is chosen, not converted. The decoder is asked
for BGRA, which is how a
Format32bppArgbbitmap is laid out in memory. Decoding to the default RGBA and copying it in swaps red and blue. - The copy is row by row. The decoder's rows are packed end to
end; a bitmap's rows are
Stridebytes apart, and the stride is not promised to equal the width times four. One block copy of the whole buffer works until it meets a bitmap where it does not. - JPEG has no alpha channel.
ToJpegdraws the image onto a background colour before it encodes. Skip that and the transparent parts of a logo come out in whatever colour the file stored under them.
PNG or JPEG to WebP
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;
// Windows only: System.Drawing reads the PNG or the JPEG, Darnelix.Webp writes the WebP.
public static class ImageToWebp
{
// Every pixel survives exactly. The usual choice for a PNG: screenshots, UI, line art.
public static void Lossless(string imagePath, string webpPath)
{
byte[] bgra = ReadBgra(imagePath, out int width, out int height);
// 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));
}
// Smaller files, some loss. The usual choice for a photograph, which is what most JPEGs are.
public static void Lossy(string imagePath, string webpPath, float quality = 80f)
{
byte[] bgra = ReadBgra(imagePath, out int width, out int height);
var options = new WebpEncodeOptions { Quality = quality, Method = 4, FilterStrength = 60, AlphaQuality = 100 };
File.WriteAllBytes(webpPath, WebpEncoder.Encode(bgra, width, height, WebpPixelFormat.Bgra32, options));
}
static byte[] ReadBgra(string imagePath, out int width, out int height)
{
using var bitmap = new Bitmap(imagePath);
width = bitmap.Width;
height = bitmap.Height;
// Asking LockBits for Format32bppArgb converts whatever the file held: 24-bit, indexed, greyscale.
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.
int rowBytes = width * 4;
byte[] bgra = new byte[rowBytes * height];
for (int y = 0; y < height; y++)
{
Marshal.Copy(data.Scan0 + y * data.Stride, bgra, y * rowBytes, rowBytes);
}
return bgra;
}
finally
{
bitmap.UnlockBits(data);
}
}
} Usage: ImageToWebp.Lossless("logo.png", "logo.webp"); or ImageToWebp.Lossy("photo.jpg", "photo.webp", 80f);
WebP has two compression modes and the source format suggests which one you want. Lossless keeps every pixel exactly, which suits what PNG is used for: screenshots, interface art, anything with hard edges or transparency that has to stay clean. Lossy gives smaller files at the cost of some detail, which suits photographs. A JPEG has already lost detail once; encoding it losslessly spends its bytes preserving those artefacts exactly.
The options type deserves one warning. A new WebpEncodeOptions is zero
in every field, and zero means quality 0, effort 0 and the loop filter off. Set the
fields you rely on, as the sample does, or use the two one-line helpers
Webp.EncodeLossless and Webp.EncodeLossy, which take RGBA
pixels and fill the rest in.
The same conversion with WPF's encoders
In a WPF application the PNG and JPEG half is already there as
PngBitmapEncoder, JpegBitmapEncoder and
BitmapDecoder, with no reference to System.Drawing at all.
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Darnelix.Webp;
using Darnelix.Webp.Decoding;
using Darnelix.Webp.Encoding;
using WebpPixelFormat = Darnelix.Webp.PixelFormat;
// Windows only: WPF's encoders and decoders (the Windows Imaging Component) handle the PNG and
// the JPEG, Darnelix.Webp handles the WebP. Needs <UseWPF>true</UseWPF> in the project file.
public static class WebpWic
{
public static void ToPng(string webpPath, string pngPath)
{
Save(new PngBitmapEncoder(), Decode(webpPath, WebpPixelFormat.Bgra32, PixelFormats.Bgra32), pngPath);
}
// Decoding to BGR24 discards the alpha channel. It does not blend onto a background: a pixel
// that was transparent keeps whatever colour the file stored under it.
public static void ToJpeg(string webpPath, string jpegPath, int quality = 90)
{
var encoder = new JpegBitmapEncoder { QualityLevel = quality };
Save(encoder, Decode(webpPath, WebpPixelFormat.Bgr24, PixelFormats.Bgr24), jpegPath);
}
public static void FromImage(string imagePath, string webpPath)
{
BitmapSource source;
using (FileStream input = File.OpenRead(imagePath))
{
// OnLoad reads the whole file now, so the stream can close before the pixels are used.
BitmapDecoder decoder = BitmapDecoder.Create(
input, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
source = new FormatConvertedBitmap(decoder.Frames[0], PixelFormats.Bgra32, null, 0);
}
int stride = source.PixelWidth * 4;
byte[] bgra = new byte[stride * source.PixelHeight];
source.CopyPixels(bgra, stride, 0);
var options = new WebpEncodeOptions { Lossless = true, Method = 4, AlphaQuality = 100 };
File.WriteAllBytes(
webpPath,
WebpEncoder.Encode(bgra, source.PixelWidth, source.PixelHeight, WebpPixelFormat.Bgra32, options));
}
static BitmapSource Decode(string webpPath, WebpPixelFormat layout, System.Windows.Media.PixelFormat format)
{
byte[] file = File.ReadAllBytes(webpPath);
var options = new WebpDecodeOptions { PixelFormat = layout };
using WebpImage image = WebpDecoder.Decode(file, options);
int stride = image.Width * (format.BitsPerPixel / 8);
return BitmapSource.Create(
image.Width, image.Height, 96, 96, format, null, image.Pixels.ToArray(), stride);
}
static void Save(BitmapEncoder encoder, BitmapSource source, string path)
{
encoder.Frames.Add(BitmapFrame.Create(source));
using FileStream output = File.Create(path);
encoder.Save(output);
}
} The project file needs <UseWPF>true</UseWPF> and a target framework ending in -windows.
To put the decoded image on screen instead of into a file, see loading a WebP into a System.Drawing Bitmap or a WPF image.
Animated WebP
PNG and JPEG hold one image, so an animated WebP has no direct equivalent. The
samples above do not guess: handed an animated file,
WebpDecoder.Decode throws a WebpDecodeException rather
than quietly converting the first frame. To convert every frame, read them with
WebpAnimDecoder, which hands back each one as a full image with its
duration, and save each as its own file. To take just the first, set
AnimatedInputHandling.DecodeFirstFrame on the decode options.
Doing both halves in one library
Three libraries read and write WebP, PNG and JPEG on their own, and with any of them the conversion is a load and a save. That is a sound way to do it. What separates them, and separates them from the two-library route above, is the licence and what ships inside your application:
- SixLabors.ImageSharp 4.1.2. Managed code: the package holds one assembly for .NET 8 and no native binaries. It is under the Six Labors Split License 1.0, which grants Apache 2.0 terms to open-source and source-available software, to transitive dependencies, to for-profit users with under 1M USD in annual gross revenue, and to non-profits; anyone else needs the commercial licence. The package checks for a licence key when the project builds: with none present, a Release build of a test project stopped with “No Six Labors license found”, and a Debug build reported the same as a warning. Keys come from Six Labors.
- SkiaSharp 4.153.0. MIT licence. It is a managed wrapper around
Google's Skia, a native library: the package brings
libSkiaSharpfor Windows and macOS with it, and a Linux deployment addsSkiaSharp.NativeAssets.Linuxas a separate package. - Magick.NET 14.17.2. Apache 2.0 licence. It is a managed wrapper
around ImageMagick, a native library, which the
Magick.NET-Q8-AnyCPUpackage carries for Windows (x86, x64, Arm64), Linux (x64 and Arm64, and musl x64) and macOS (x64, Arm64). - Darnelix.Webp 1.1.0, for the same four facts: WebP only. Managed code, one assembly for each of .NET 8, 9 and 10, no native binaries and no dependencies. Free of charge under a proprietary licence; it is not open source.
The choice follows from what you need. If you need PNG and JPEG off Windows, or resizing, cropping and drawing as well as conversion, use one of the first three and pick on licence and deployment. If the application already reads and writes PNG and JPEG through Windows and the only thing missing is WebP, adding a WebP codec is the smaller change.
Converting a few files without writing code
For a one-off, Google's own command-line tools do it: cwebp reads PNG
and JPEG and writes WebP, and dwebp reads WebP and writes PNG. Both come
with the libwebp download. There is no dwebp switch for JPEG, so that
direction goes through PNG and a second tool.
Also called
convert webp to png in C#, convert webp to jpg, convert image to webp, png to webp, jpg to webp, save a bitmap as webp, webp converter for .NET — one problem in each direction: a WebP codec for one side, a PNG or JPEG codec for the other, and raw pixels in between.
The WebP half of the conversion, as one managed assembly.
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 hands you pixels in the
layout System.Drawing and WPF already use, so the conversion is a
copy rather than a per-pixel loop.
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.