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).
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:
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"); }
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:
wiki=https://commons.wikimedia.org/wiki/Special:FilePath curl -L -o dice.png $wiki/PNG_transparency_demonstration_1.png
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"); }
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:
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");
photo-200kb.jpg: 197 KBIf 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
Flattenfirst, 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.