Recursive AI

Compress an image in C#

All oneimage recipes

Resize the image to the size it is shown at, then save it as JPEG or WebP with a quality setting. Resizing saves the most; quality and format do the rest. The program below measures each step on NASA’s “Blue Marble” photograph (4579×4579, 1.9 MB, public domain).

Terminal
dotnet new console -o CompressImage && cd CompressImage
dotnet add package RecursiveAI.OneImage --version 1.10.4
curl -L -o photo.jpg \
  https://images-assets.nasa.gov/image/as17-148-22727/as17-148-22727~orig.jpg

OneImage needs the .NET 10 SDK. Put this in Program.cs and run dotnet run:

C#
using OneImage;
 
static SaveOptions Jpeg(int quality) =>
    new() { EncoderOptions = new JpegEncoderOptions { Quality = quality } };
 
Image photo = Image.Load("photo.jpg");
 
// 1. Lower the JPEG quality (1 to 100, default 75).
photo.Save("q80.jpg", Jpeg(80));
photo.Save("q60.jpg", Jpeg(60));
 
// 2. Resize to the displayed size first. Thumbnail also strips EXIF and GPS.
Image web = Image.Thumbnail("photo.jpg", 1600);
web.Save("1600-q80.jpg", Jpeg(80));
 
// 3. Write WebP instead of JPEG.
web.Save("1600-q80.webp", new SaveOptions
{
    EncoderOptions = new WebPEncoderOptions { Quality = 80 },
});
 
foreach (string file in new[] { "photo.jpg", "q80.jpg", "q60.jpg", "1600-q80.jpg",
                                "1600-q80.webp" })
{
    Console.WriteLine($"{file}: {new FileInfo(file).Length / 1024:N0} KB");
}
Text
photo.jpg: 1,870 KB
q80.jpg: 1,402 KB
q60.jpg: 1,111 KB
1600-q80.jpg: 278 KB
1600-q80.webp: 208 KB

Resizing to the 1600 px it is shown at saved more than any quality setting, and WebP took a quarter off that. Pick the quality by looking at the result at the size it is shown: below about 60, blocks and smeared color appear around edges. The encoder already uses optimized Huffman tables and 4:2:0 chroma subsampling; Progressive = true in JpegEncoderOptions usually saves a little more and lets browsers show a rough version early.

Compress a PNG without losing quality

PNG has no quality setting. A higher deflate level, lossless WebP or a 256-color palette are the choices, from no pixel change to fewer colors. This uses Wikimedia’s transparent dice image:

Terminal
wiki=https://commons.wikimedia.org/wiki/Special:FilePath
curl -L -o dice.png $wiki/PNG_transparency_demonstration_1.png
C#
using OneImage;
 
Image png = Image.Load("dice.png");
 
// Lossless: the same pixels, compressed harder.
png.Save("level9.png", new SaveOptions
{
    EncoderOptions = new PngEncoderOptions { CompressionLevel = 9 },
});
png.Save("lossless.webp", new SaveOptions
{
    EncoderOptions = new WebPEncoderOptions { Lossless = true },
});
 
// Lossy: a 256-color palette, as pngquant does.
png.Quantize(colors: 256, dither: Dither.FloydSteinberg).Save("256.png");
 
foreach (string file in new[] { "dice.png", "level9.png", "lossless.webp", "256.png" })
{
    Console.WriteLine($"{file}: {new FileInfo(file).Length / 1024:N0} KB");
}
Text
dice.png: 219 KB
level9.png: 233 KB
lossless.webp: 172 KB
256.png: 87 KB

This PNG was already squeezed by an optimizer, so level 9 came out larger than the original; on PNGs written by ordinary software it usually saves a few percent. Lossless WebP kept every pixel in a smaller file, and the 256-color palette, which changes pixels, cut the file to 40%. Re-saving a JPEG at any quality decodes and encodes it again, which always changes some pixels.

Compress to a target size in KB

Upload forms often cap files at 100 or 200 KB. File size doesn’t follow from quality by a formula, so search for the highest quality that fits; seven encodes cover 10 to 95:

C#
using OneImage;
 
static byte[] CompressToSize(Image image, long maxBytes)
{
    byte[]? best = null;
    int low = 10, high = 95;
    while (low <= high)
    {
        int quality = (low + high) / 2;
        using var buffer = new MemoryStream();
        image.Save(buffer, JpegFormat.Instance, new SaveOptions
        {
            EncoderOptions = new JpegEncoderOptions { Quality = quality },
        });
        if (buffer.Length <= maxBytes) { best = buffer.ToArray(); low = quality + 1; }
        else { high = quality - 1; }
    }
    return best ?? throw new InvalidOperationException("Too large at quality 10.");
}
 
byte[] jpeg = CompressToSize(Image.Thumbnail("photo.jpg", 2000), 200 * 1024);
File.WriteAllBytes("photo-200kb.jpg", jpeg);
Console.WriteLine($"photo-200kb.jpg: {jpeg.Length / 1024} KB");
Text
photo-200kb.jpg: 197 KB

If even quality 10 is too big, make the image smaller rather than going lower. The same function works for WebP with WebPFormat.Instance and WebPEncoderOptions.

Limits

  • Sizes depend on content. Your savings will differ; measure on your own images.
  • JPEG has no transparency. Put transparent images on a background with Flatten first, or use WebP or PNG.
  • No AVIF or JPEG XL output, which can be smaller than WebP.

For WebP options, see Convert an image to WebP; for grayscale, which also shrinks files, Convert to grayscale. Every encoder setting is in the guide.