How to Fix `ExternalException` When Saving a C# Bitmap
Fix C# Bitmap.Save ExternalException by checking paths, source-file overwrites, formats, streams, encoders, and System.Drawing platform support.
Direct answer: an ExternalException from Bitmap.Save is usually caused by one of these conditions: the destination directory is missing or not writable, the bitmap is being saved over the file it came from, the requested format and encoder are invalid or mismatched, the output stream is unsuitable, or System.Drawing.Common is running on an unsupported platform. Capture the complete exception and save inputs first, then check those causes in that order.
Microsoft documents two especially important restrictions: saving an image to the same file it was constructed from is not allowed, and an image must not be saved to the stream used to construct it. See the Image.Save documentation.
1. Capture the evidence before changing code
The message “A generic error occurred in GDI+” does not identify a single root cause. Log diagnostic metadata without logging image contents:
ex.ToString(), including the stack trace and inner exception.ex.HResult.- Target framework, .NET runtime version, operating system, and process architecture.
- The absolute output path, selected
ImageFormat, and whether the bitmap came from a file, stream, or memory. - Whether the destination is the same path as the source.
try
{
bitmap.Save(outputPath, ImageFormat.Png);
}
catch (ExternalException ex)
{
Console.Error.WriteLine(ex.ToString());
Console.Error.WriteLine($"HResult: 0x{ex.HResult:X8}");
Console.Error.WriteLine($"Output: {outputPath}");
Console.Error.WriteLine($"Runtime: {Environment.Version}");
Console.Error.WriteLine($"OS: {Environment.OSVersion}");
throw;
}
2. Save to a known-good, writable path
Use an absolute path whose parent directory exists and is writable by the actual process identity. Services, IIS workers, scheduled tasks, containers, and desktop applications can run under different identities.
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
string outputPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp",
"output.png");
string? directory = Path.GetDirectoryName(outputPath);
if (directory is null)
throw new InvalidOperationException("Output directory is unavailable.");
Directory.CreateDirectory(directory);
using var bitmap = new Bitmap(100, 100);
bitmap.Save(outputPath, ImageFormat.Png);
Directory.CreateDirectory handles an already-existing directory, but it does not grant permissions. If this still fails, verify the account running the process can create and write a file in that directory. A dotnet/runtime issue report records this exception when the destination folder was missing; that is evidence of one real cause, not a universal diagnosis.
3. Never overwrite the source image
If the bitmap was loaded from source.jpg, save it to another path such as converted.png. Microsoft explicitly states that saving to the same file from which the image was constructed throws an exception.
using var source = Image.FromFile("source.jpg");
string destination = Path.Combine("output", "converted.png");
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
source.Save(destination, ImageFormat.Png);
If replacement is required, write a separate temporary file, dispose objects that hold the source, and then replace or move the files with normal filesystem error handling. Merely changing the extension does not remove a source-file lock or same-file restriction.
4. Choose an explicit format and matching extension
Do not rely on Image.Save(path) to infer the desired encoding. Select the format explicitly and keep the filename extension consistent:
bitmap.Save("photo.png", ImageFormat.Png);
bitmap.Save("photo.jpg", ImageFormat.Jpeg);
bitmap.Save("photo.bmp", ImageFormat.Bmp);
bitmap.Save("photo.gif", ImageFormat.Gif);
bitmap.Save("photo.tif", ImageFormat.Tiff);
GDI+ has built-in encoders for BMP, GIF, JPEG, PNG, and TIFF. The Microsoft encoder guide explains the encoder model. For a format that needs a codec, locate an ImageCodecInfo and handle a missing result:
using System.Drawing.Imaging;
static ImageCodecInfo GetEncoder(ImageFormat format)
{
return ImageCodecInfo.GetImageEncoders()
.FirstOrDefault(codec => codec.FormatID == format.Guid)
?? throw new InvalidOperationException($"No encoder for {format}.");
}
Some formats are not encoded as you might expect. Microsoft notes that WMF and EMF saving uses PNG because the .NET Framework GDI+ component does not provide those encoders.
5. Use streams correctly
When calling Save(Stream, ImageFormat), use a writable output stream that is different from the stream used to construct the image. Set its position to zero before saving, and do not prepend unrelated bytes.
using var input = File.OpenRead("source.jpg");
using var image = Image.FromStream(input);
using var output = new MemoryStream();
image.Save(output, ImageFormat.Png);
output.Position = 0;
using var file = File.Create("output.png");
output.CopyTo(file);
For a reusable stream, call SetLength(0) and set Position = 0 before saving. Keep the source stream alive for as long as the image may depend on it. Microsoft warns that saving to the construction stream is invalid and that data written before image bytes can corrupt the output.
6. Check the runtime and operating system
On .NET 6 and later, System.Drawing.Common is supported only on Windows. On Linux or macOS, compile-time warnings and runtime exceptions are expected failure modes. Confirm the deployed OS, target framework, runtime identifier, and architecture. If the application must run cross-platform, choose an image library that supports that target instead of trying to solve a platform failure with path changes.
The Bitmap documentation describes this platform limitation and supported bitmap behavior.
7. Reduce the failure to a minimal reproduction
- Create a small bitmap entirely in memory.
- Save it as PNG to a new, absolute path under a deliberately created directory.
- If that works, restore the original input image.
- Then restore the original output format, stream, path, and deployment context one at a time.
using System.Drawing;
using System.Drawing.Imaging;
using var bitmap = new Bitmap(100, 100);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.White);
}
string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"BitmapSaveRepro",
"test.png");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
bitmap.Save(path, ImageFormat.Png);
Console.WriteLine(path);
8. Diagnose by symptom
| Observable condition | Likely check | Fix |
|---|---|---|
| Parent directory does not exist | Directory.Exists(Path.GetDirectoryName(path)) |
Create the intended directory and verify process permissions. |
| Source and destination paths are identical | Compare normalized absolute paths | Save to a different file, then replace after disposing the source. |
| Extension says PNG but code requests JPEG | Compare extension with ImageFormat |
Use a matching extension and explicit format. |
| Save uses the construction stream | Trace stream ownership | Use a fresh writable output stream at offset zero. |
| No suitable encoder | Inspect ImageCodecInfo.GetImageEncoders() |
Use a built-in encoder or a supported image library. |
| Runs on Linux or macOS with .NET 6+ | Check deployed OS and System.Drawing.Common |
Run on Windows or migrate to a cross-platform library. |
9. Common errors and fixes
“A generic error occurred in GDI+”
Inspect the path, permissions, source-file identity, stream, format, and platform. The text is deliberately generic; changing only directory permissions can leave a same-file or stream error untouched.
“Parameter is not valid”
Check that the image is valid, the stream is readable and positioned correctly, and the selected format is supported. Preserve the original exception because this message can occur before the save operation itself.
The file is created but cannot be opened
Make sure the output stream started at offset zero and contained no earlier bytes. Flush and dispose the output stream after saving, then reopen the resulting file.
It works locally but fails in production
Compare the production identity, current working directory, absolute path, OS, runtime version, and installed codecs. Relative paths often resolve differently in services and web applications.
It fails only when replacing an existing file
Use a temporary destination and close every object that references the source before moving or replacing it. Do not save directly back to the file used to construct the bitmap.
10. Reliability, performance, and cost considerations
- Reliability: use absolute paths, create directories deliberately, dispose images and streams deterministically, and log the complete exception with non-sensitive metadata.
- Performance: avoid loading and re-encoding an image repeatedly; keep the minimal reproduction separate from the production path; use streams when you need controlled storage, but still follow the source/output separation rule.
- Concurrency: give concurrent operations distinct temporary paths. Coordinate replacement when multiple processes can write the same destination.
- Cost: the .NET API itself has no per-save service charge. Operational cost comes from CPU, memory, disk, and any external storage or image service used around it.
Or skip the browser setup
If the bitmap is being created from a web page screenshot, ScreenshotNeo provides the capture step as one API request. It removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Each response identifies the page verdict and billing status in headers. Its MCP server lets Claude, Cursor, and other MCP clients call screenshot tools directly.
See the ScreenshotNeo API documentation for request options and formats.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);
Every plan includes the capture features. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Does changing JPG to PNG fix the exception?
Only when the original format or encoder was the problem. It does not fix a missing directory, permissions, same-file overwrite, invalid stream, or unsupported platform.
Can I save a bitmap over the file I loaded?
No. Save to a different path, dispose the source image, and replace the original through separate filesystem operations if necessary.
Is System.Drawing.Common safe for a Linux service?
It is Windows-only on .NET 6 and later. Use a supported cross-platform image library for Linux or macOS deployments.
Why does a stream need to start at position zero?
Image bytes must begin at the expected offset. Earlier data can make the resulting file invalid even when Save returns.
What should I include in a bug report?
Include the full exception, runtime and OS, source origin, output path shape, selected format, stream details, and whether source and destination are the same. Exclude image contents and secrets.


