Common Mistakes
Patterns that look reasonable but fight the module’s design.
1. Forgetting to dispose the pipeline
Section titled “1. Forgetting to dispose the pipeline”ImagePipeline holds a native handle. Not disposing leaks native memory until the finaliser runs.
// ❌var pipe = ImagePipeline.Load(bytes);pipe.Resize(400, 400).EncodeTo(out, ImageFormat.Webp);// pipe never disposed — leaks until GC
// ✅using var pipe = ImagePipeline.Load(bytes);pipe.Resize(400, 400).EncodeTo(out, ImageFormat.Webp);Prefer using/await using even for single-operation flows.
2. Sharing a pipeline across threads
Section titled “2. Sharing a pipeline across threads”ImagePipeline is not thread-safe. The native handle holds mutable state, and concurrent access from the managed side will corrupt it or panic the Rust code.
// ❌using var pipe = ImagePipeline.Load(bytes);await Task.WhenAll( Task.Run(() => pipe.Thumbnail(200, 200).EncodeTo(outA, ImageFormat.Png)), Task.Run(() => pipe.Thumbnail(400, 400).EncodeTo(outB, ImageFormat.Png)));
// ✅ one pipeline per taskawait Task.WhenAll( Task.Run(() => { using var p = ImagePipeline.Load(bytes); p.Thumbnail(200, 200).EncodeTo(outA, ImageFormat.Png); }), Task.Run(() => { using var p = ImagePipeline.Load(bytes); p.Thumbnail(400, 400).EncodeTo(outB, ImageFormat.Png); }));For bounded concurrency over many inputs, use ImageBatch.
3. Processing untrusted uploads without ImagingOptions
Section titled “3. Processing untrusted uploads without ImagingOptions”Decompression-bomb attacks (20KB file that decodes to 50,000×50,000) and memory-exhaustion attacks are common on public image endpoints. Defaults are conservative but not adversarial-hardened.
// ❌using var pipe = ImagePipeline.Load(userUpload);
// ✅using var pipe = ImagePipeline.Load(userUpload, new ImagingOptions{ MaxWidth = 4096, MaxHeight = 4096, MaxDecodedBytes = 100 * 1024 * 1024, AllowedFormats = ImageFormat.Jpeg | ImageFormat.Png | ImageFormat.Webp,});4. Using Resize when Thumbnail is what you want
Section titled “4. Using Resize when Thumbnail is what you want”Resize(width, height) forces exact dimensions and stretches if the aspect ratio changes. Thumbnail(maxW, maxH) fits into the bounding box and preserves aspect.
// ❌ 1000×500 stretched into 400×400 becomes a square distortionpipe.Resize(400, 400);
// ✅ 1000×500 fits into 400×400 as 400×200pipe.Thumbnail(400, 400);Only use Resize(w, h) when you deliberately want to force dimensions.
5. Trying to rotate by non-90° angles
Section titled “5. Trying to rotate by non-90° angles”The native layer only supports lossless 90° rotations (0, 90, 180, 270).
// ❌pipe.Rotate(45); // throws ImagingException
// ✅ round to the nearest quarter turnpipe.Rotate(90);Arbitrary-angle rotation would require resampling and antialiasing; if you need it, use a full imaging library.
6. Expecting EXIF metadata to survive re-encoding
Section titled “6. Expecting EXIF metadata to survive re-encoding”All transformations decode and re-encode the image. EXIF is dropped as a side effect.
- For thumbnails / resized versions this is usually what you want (metadata stripped for privacy)
- For original-quality re-saves you’d lose camera / GPS / orientation data
Pragmatic.Imaging does not preserve EXIF through re-encoding. If you need it, use a dedicated metadata-preservation tool.
7. Ignoring the quality argument for PNG
Section titled “7. Ignoring the quality argument for PNG”PNG is lossless — quality is ignored. If you pass quality: 50 and expect a small file, you’ll get a normal PNG.
// quality ignoredpipe.EncodeTo(out, ImageFormat.Png, quality: 50);
// For size-constrained output, use WebP or JPEGpipe.EncodeTo(out, ImageFormat.Webp, quality: 75);8. Loading a Stream then expecting to reuse it
Section titled “8. Loading a Stream then expecting to reuse it”ImagePipeline.Load(Stream) reads the entire stream during the call. After load, the stream position is at the end.
using var fs = File.OpenRead("photo.jpg");using var pipe = ImagePipeline.Load(fs);// fs is now at EOF — re-reading needs fs.Position = 0If you need to re-read, call ImageInfo.FromStream first (it leaves the stream positioned after the header; rewind before Load).
9. Forgetting that Thumbnail is a no-op when the source is smaller
Section titled “9. Forgetting that Thumbnail is a no-op when the source is smaller”// Source is 300×200pipe.Thumbnail(400, 400); // result is still 300×200 (source fits in bounds)If you need to enforce a minimum size, check ImageInfo first and reject or upscale explicitly via Resize.
10. Expecting QR codes to embed logos or custom colours
Section titled “10. Expecting QR codes to embed logos or custom colours”QrCode.GeneratePng produces a black/white QR with configurable module size and margin. No colour, no logo, no error-correction level tuning. For richer QR output, use a dedicated library.
11. Running on Alpine Linux
Section titled “11. Running on Alpine Linux”The native binary is built against glibc. Alpine uses musl. The app will fail at first image operation with DllNotFoundException.
Switch to a Debian/Ubuntu-based base image, or build the native library yourself targeting musl (non-trivial).
See native-deployment.md.
12. Cancelling long operations mid-call
Section titled “12. Cancelling long operations mid-call”The native functions run to completion once invoked — there’s no cancellation cooperation on the Rust side. Cancellation tokens are checked between operations, not during them.
For long batches, use ImageBatch with maxConcurrency so you can cap work-in-flight and interrupt between items.
Related
Section titled “Related”- troubleshooting.md — runtime errors
- concepts.md — why the API is shaped this way