Skip to content

Common Mistakes

Patterns that look reasonable but fight the module’s design.


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.


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 task
await 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 distortion
pipe.Resize(400, 400);
// ✅ 1000×500 fits into 400×400 as 400×200
pipe.Thumbnail(400, 400);

Only use Resize(w, h) when you deliberately want to force dimensions.


The native layer only supports lossless 90° rotations (0, 90, 180, 270).

// ❌
pipe.Rotate(45); // throws ImagingException
// ✅ round to the nearest quarter turn
pipe.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.


PNG is lossless — quality is ignored. If you pass quality: 50 and expect a small file, you’ll get a normal PNG.

// quality ignored
pipe.EncodeTo(out, ImageFormat.Png, quality: 50);
// For size-constrained output, use WebP or JPEG
pipe.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 = 0

If 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×200
pipe.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.


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.


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.