Recursive AI

Compress an image in C#

Reduce image file size in C# with pure .NET code: JPEG quality, resizing first, WebP, lossless and palette PNG, and compressing to a target size in KB.

To compress an image in C#, save it again with a lower JPEG quality, resize it to the size it’s shown at, or switch to WebP. With OneImage each of those is a single call in pure .NET code, on Windows, Linux, macOS and in containers. This page measures what each step saves on a real photograph, shows lossless options for screenshots and documents, and ends with a function that compresses a JPEG to a target size such as 200 KB. Every sample was run against OneImage 1.10.2 on .NET 10.

TerminalNuGet
dotnet add package RecursiveAI.OneImage

The photo is NASA’s public-domain “Blue Marble” (photo.jpg, 3000×3002, 6,358 KB). The PNG example is a tax form page rendered at 150 dpi (page.png, 1275×1650, 654 KB). Your sizes will differ with the content.

Lower the JPEG quality

Program.cs.NET 10
using OneImage;
 
Image photo = Image.Load("photo.jpg");
 
foreach (int quality in new[] { 90, 80, 70, 60 })
{
    string file = $"photo-q{quality}.jpg";
    photo.Save(file, new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = quality } });
    Console.WriteLine($"{file}: {new FileInfo(file).Length / 1024:N0} KB");
}
Output1.10.2
photo-q90.jpg: 1,622 KB
photo-q80.jpg: 1,063 KB
photo-q70.jpg: 818 KB
photo-q60.jpg: 670 KB

Quality runs from 1 to 100 on the scale libjpeg uses, so a number means roughly the same thing as in other tools. The default is 75. Between 90 and 60 the file shrinks by more than half, and the drop from 90 to 80 alone saves a third. As quality drops, 8×8 blocks and color smearing appear around edges and in smooth areas, so pick the level by looking at the result at the size it’s shown.

The encoder already does the free things by default: optimized Huffman tables (5 to 10% smaller at the same quality) and 4:2:0 chroma subsampling, which stores color at half resolution. For print or graphics with sharp colored edges, Subsampling = JpegSubsampling.Yuv444 keeps full color resolution at the cost of a larger file.

Resize first: the biggest saving

A 3000-pixel photo shown in a 1600-pixel slot wastes most of its bytes. On our photo, resizing to the displayed size saved more than any quality setting did:

Program.cs.NET 10
using OneImage;
 
// 1600 px wide, upright, sRGB, EXIF and GPS removed, then JPEG at quality 80.
Image.Thumbnail("photo.jpg", 1600)
    .Save("photo-1600.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 80 } });
 
Console.WriteLine($"{new FileInfo("photo-1600.jpg").Length / 1024:N0} KB");

The result is 1600×1601 and 380 KB, against 1,063 KB for the full-size photo at the same quality 80. Image.Thumbnail also applies the EXIF orientation, converts to sRGB and removes EXIF, XMP and IPTC metadata, which can carry GPS positions and camera details. On a large JPEG it decodes at 1/2, 1/4 or 1/8 scale when that is enough, so the full-size photo is never held in memory. Our guide to resizing images in C# covers fit, crop and padding.

Switch to WebP

Program.cs.NET 10
using OneImage;
 
Image photo = Image.Thumbnail("photo.jpg", 1600);
 
photo.Save("photo-1600.webp", new SaveOptions { EncoderOptions = new WebPEncoderOptions { Quality = 80 } });
Console.WriteLine($"{new FileInfo("photo-1600.webp").Length / 1024:N0} KB");

The same 1600-pixel image is 245 KB as WebP at quality 80, against 380 KB as JPEG. The two quality scales aren’t the same scale, so compare the images by eye at the sizes you need, not by the number. Current versions of Chrome, Edge, Firefox and Safari display WebP (Can I use: WebP). See converting images to WebP in C# for effort settings, lossless WebP and animations.

Compress PNG files

PNG is lossless, so a lower “quality” setting doesn’t exist. You have three choices, from no change in pixels to fewer colors:

Program.cs.NET 10
using OneImage;
 
Image page = Image.Load("page.png");
 
// Lossless: the same pixels, compressed harder.
page.Save("page-level9.png", new SaveOptions { EncoderOptions = new PngEncoderOptions { CompressionLevel = 9 } });
page.Save("page-lossless.webp", new SaveOptions { EncoderOptions = new WebPEncoderOptions { Lossless = true } });
 
// Lossy: reduce to a 256-color palette first, then write an indexed PNG.
page.Quantize(colors: 256, dither: Dither.FloydSteinberg)
    .Save("page-256.png", new SaveOptions { EncoderOptions = new PngEncoderOptions { CompressionLevel = 9 } });
 
foreach (string file in new[] { "page.png", "page-level9.png", "page-lossless.webp", "page-256.png" })
{
    Console.WriteLine($"{file}: {new FileInfo(file).Length / 1024:N0} KB");
}
Output1.10.2
page.png: 654 KB
page-level9.png: 644 KB
page-lossless.webp: 314 KB
page-256.png: 247 KB
  • Deflate level 9 gives the same pixels in a slightly smaller file: 2% here. The default level is 6.
  • Lossless WebP kept every pixel, checked by comparing the decoded images, and halved the file.
  • A 256-color palette, the technique behind tools like pngquant, cut the file to 38% of the original. On this form the result can’t be told apart from the original at normal zoom, because the page has few colors to start with. On photos, gradients and soft shadows, check the result: Quantize takes fewer colors and a choice of dithering.

The PNG encoder also writes images with 256 or fewer distinct colors as exact palette images on its own, so icons and flat graphics often get small without any setting.

Compress an image without losing quality

If “without losing quality” means without changing a single pixel, only lossless formats qualify: PNG at a higher deflate level, lossless WebP, or TIFF with LZW or Deflate. Lossless WebP gave the biggest saving of the three on our form page. Re-saving a JPEG at any quality decodes and encodes it again, which always changes some pixels. For photos, “no visible loss” is the realistic goal: resize to the display size and choose a quality by comparing the result with the original.

Compress an image to a target size in KB

Upload forms and APIs often cap the file at 100 KB or 200 KB. File size doesn’t follow from quality in a formula, so search for the highest quality that fits. Seven encodes cover qualities 10 to 95:

Program.cs.NET 10
using OneImage;
 
// The highest JPEG quality whose file fits in maxBytes.
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();   // fits: try a higher quality
            low = quality + 1;
        }
        else
        {
            high = quality - 1;        // too big: try a lower quality
        }
    }
    return best ?? throw new InvalidOperationException("Too large even at quality 10. Resize it first.");
}
 
Image photo = Image.Thumbnail("photo.jpg", 2000);
byte[] jpeg = CompressToSize(photo, 200 * 1024);
File.WriteAllBytes("photo-200kb.jpg", jpeg);
Console.WriteLine($"{jpeg.Length / 1024:N0} KB");

This wrote a 2000×2001 JPEG of 196 KB, under the 200 KB budget. The encodes run in memory, and only the result is written. If even quality 10 doesn’t fit, the function throws. Resize further rather than going lower, because at that point a smaller image at a decent quality looks better than a large one full of blocks. The same function works for WebP with WebPFormat.Instance and WebPEncoderOptions.

Progressive JPEG

Program.cs.NET 10
using OneImage;
 
Image photo = Image.Thumbnail("photo.jpg", 1600);
 
photo.Save("baseline.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 80 } });
photo.Save("progressive.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 80, Progressive = true } });
 
foreach (string file in new[] { "baseline.jpg", "progressive.jpg" })
{
    Console.WriteLine($"{file}: {new FileInfo(file).Length / 1024:N0} KB");
}

The progressive file was 359 KB against 380 KB for the baseline one, at the same quality. Progressive JPEGs load in several passes of increasing detail, so a browser can show a rough version early. Set Progressive = true alongside the quality, as above.

Which setting to use

ImageUseMeasured here
Photo for the webResize to the display size, then WebP or JPEG at quality 75 to 856,358 KB to 245 KB (WebP) or 380 KB (JPEG)
Photo with a size limitResize, then search for the quality196 KB for a 200 KB cap
Screenshot, document, diagramLossless WebP, or a palette PNG when it must be PNG654 KB to 314 KB (lossless) or 247 KB (256 colors)
Logo or iconPNG: small palettes are automaticDepends on the colors

To compress images as users upload them, the resize guide has an ASP.NET Core endpoint that resizes and re-encodes an upload in one step. For black-and-white and grayscale output, which also shrinks files, see converting images to grayscale in C#. The OneImage overview lists every format and encoder option.