CodeLogic.Imaging 5.0.116-preview

This is a prerelease version of CodeLogic.Imaging.
This package has a SemVer 2.0.0 package version: 5.0.116-preview+258072a.
dotnet add package CodeLogic.Imaging --version 5.0.116-preview
                    
NuGet\Install-Package CodeLogic.Imaging -Version 5.0.116-preview
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodeLogic.Imaging" Version="5.0.116-preview" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodeLogic.Imaging" Version="5.0.116-preview" />
                    
Directory.Packages.props
<PackageReference Include="CodeLogic.Imaging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodeLogic.Imaging --version 5.0.116-preview
                    
#r "nuget: CodeLogic.Imaging, 5.0.116-preview"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodeLogic.Imaging@5.0.116-preview
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodeLogic.Imaging&version=5.0.116-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.Imaging&version=5.0.116-preview&prerelease
                    
Install as a Cake Tool

CodeLogic.Imaging

NuGet License: MIT

Image decoding, a lazy editing pipeline, EXIF metadata, colour analysis and web helpers for CodeLogic 5, built on SkiaSharp.

Every fallible call returns a CodeLogic Result / Result<T> whose Error carries a stable imaging.* code (ImagingErrors); bad input never throws. Uploads are checked against a pixel limit read from the header before any pixel memory is allocated, and against a byte limit while reading. Linux servers are covered by SkiaSharp.NativeAssets.Linux.NoDependencies.

Imaging used to live in CodeLogic.Common (CL.Common.Imaging.CLU_Imaging); see MIGRATION.md.

Install

dotnet add package CodeLogic.Imaging

No library has to be loaded: the static and fluent APIs work on their own. Loading ImagingLibrary only applies the imaging config section to the process-wide defaults.

Quick start

using CL.Imaging;

Result<ImageDocument> loaded = await ImageDocument.LoadAsync(stream, new ImageLoadOptions { MaxPixels = 40_000_000 }, ct);
if (loaded.IsFailure) return loaded.Error;           // imaging.decode_failed, imaging.too_large, ...
using var doc = loaded.Value!;

using var logo = (await ImageDocument.LoadFileAsync("logo.png", ct: ct)).Value!;

Result<EncodedImage> output = doc.Edit()
    .AutoOrient()
    .Resize(1200, 800, ResizeMode.Fit, ResampleQuality.High)
    .Brightness(0.05).Contrast(0.1).Saturation(-0.2)
    .Sharpen(0.4)
    .RoundCorners(24)
    .Watermark(new WatermarkImage(logo, RelativeWidth: 0.2), Anchor.BottomRight, opacity: 0.6, margin: 16)
    .StripMetadata()
    .Encode(ImageFormat.Webp, quality: 82, ct);

using var encoded = output.Value!;                   // pooled buffer: dispose when done
await encoded.SaveAsync("out.webp", ct);

One-call helpers:

// Safe upload handling: limits, upright, fit into a box, re-encode (drops EXIF and GPS).
using var avatar = ImageTools.ProcessUpload(upload, new UploadOptions { MaxWidth = 512, MaxHeight = 512 }).Value!;

// Responsive srcset variants from one decode.
using var variants = ImageTools.GenerateVariants(doc, [320, 640, 1280], ImageFormat.Webp).Value!;
string srcset = variants.ToSrcSet(v => $"/img/photo-{v.Width}.webp");

using var thumb = ImageTools.Thumbnail(doc, 200, 200, smartCrop: true).Value!;
string dataUri = ImageTools.ToDataUri(doc, ImageFormat.Png).Value!;

Features

  • Formats. Decodes JPEG, PNG, WebP, GIF (first frame), BMP and ICO; HEIF/HEIC and AVIF where the platform's Skia build has those codecs (otherwise imaging.unsupported_format; the signature is still recognised). Encodes JPEG, PNG and WebP (lossy, or lossless with ImageEncodeOptions.Lossless). JPEG output flattens transparency onto ImageEncodeOptions.Background (white).
  • Probe without decoding. ImageInfo.Probe(stream) reads format, size, orientation, alpha and frame count from the header; ImageFormatExtensions.Detect sniffs the signature.
  • Editing pipeline. doc.Edit() records steps; Apply() returns a new ImageDocument, Encode(...) returns an EncodedImage. Steps:
    • Geometry: AutoOrient, Resize (Fit, Fill (cover, with anchor), Stretch, Pad, Max), Crop, SmartCrop (edge/saturation energy), Rotate (exact for right angles, any angle with background), FlipHorizontal, FlipVertical.
    • Colour: Brightness, Contrast, Saturation, Gamma, Hue, Exposure, Grayscale, Sepia, Invert, Tint — consecutive colour steps are fused into one pass.
    • Filters: GaussianBlur, Sharpen (unsharp mask), Pixelate.
    • Frames and drawing: RoundCorners, Border, Pad, Watermark (image or text), DrawText, DrawRectangle, DrawEllipse, DrawLine, Composite (17 blend modes, opacity).
    • StripMetadata. Each step validates its arguments when recorded; the first invalid one is returned by Apply/Encode as imaging.invalid_argument. The source document is never modified, and a pipeline can be run again.
  • Metadata. doc.Metadata / ImageMetadataReader.Read(...) give an ImageMetadata record: camera make and model, lens, software, artist, copyright, date taken (+ UTC offset), GPS (latitude, longitude, altitude), orientation, ISO, exposure time, f-number, focal length (+ 35 mm), flash, EXIF pixel size. It comes from a small bounds-checked managed TIFF/EXIF reader for JPEG APP1, PNG eXIf and WebP EXIF chunks; malformed EXIF yields fewer fields, never a failure.
  • Metadata on encode. By default EXIF is dropped (ImagingSettings.StripMetadata). KeepMetadata = true writes the original EXIF back (orientation reset to 1 after AutoOrient; the source thumbnail and MakerNote removed), GPS included unless RemoveGps = true; KeepIccProfile = true writes the original ICC profile back byte for byte and keeps the pixels in that colour space. Without it, pixels are converted to sRGB.
  • Analysis (CL.Imaging.Analysis): ImageAnalysis.AverageColor, DominantColor, Palette(n) (median cut + k-means, deterministic), HasAlpha, IsMostlyTransparent; BlurHash.Encode/Decode; PerceptualHash.DHash/PHash with HammingDistance, Similarity, IsNearDuplicate; SmartCrop.FindRegion; ImageCompare.Difference (similarity, differing pixels, optional diff image).
  • Safety. Pixel limit from the header (default 40 MP), byte limit (default 100 MB), hard output cap of 16384² pixels, CancellationToken on every async method and between pipeline steps, all Skia objects disposed, encoder output in ArrayPool buffers.

Configuration (optional)

await Libraries.LoadAsync<CL.Imaging.ImagingLibrary>();

config.imaging.json:

Setting Default Meaning
DefaultMaxPixels 40000000 Largest image decoded (0 = no limit)
DefaultMaxBytes 104857600 Largest encoded input read (0 = no limit)
DefaultQuality 85 Lossy quality when a call doesn't give one
DefaultFormat Webp Output format when a call doesn't give one
StripMetadataByDefault true Drop EXIF unless a call keeps it

Without the library, set ImagingDefaults.Current = ImagingSettings.BuiltIn with { ... } yourself.

Errors

Code Kind When
imaging.too_large Validation Over the pixel or byte limit, or beyond the output cap
imaging.unsupported_format Validation HEIF/AVIF without a platform codec; encoding to a format other than JPEG/PNG/WebP
imaging.decode_failed Validation Not an image, corrupt or truncated
imaging.invalid_argument Validation A step's argument out of range, crop outside the image, bad BlurHash
imaging.encode_failed Internal The encoder produced nothing
imaging.cancelled Unavailable The CancellationToken was cancelled
imaging.file_not_found NotFound LoadFile/ProbeFile on a missing path
imaging.io_failed Internal Stream or file I/O failed
imaging.processing_failed Internal Unexpected engine failure

Limitations

  • No animated output. Animated GIF/WebP input decodes its first frame; animated encoding is out of scope.
  • HEIF/AVIF decode only where the platform's Skia build includes those codecs; CL.Imaging does not bundle decoders. Metadata for HEIF/AVIF comes from Skia (orientation only); the EXIF reader covers JPEG, PNG and WebP.
  • Text uses the platform's fonts. Minimal Linux containers (NoDependencies assets) may have none, so a family name can draw nothing; ship a font and use ImageFont.FromFile / FromBytes.
  • EXIF on re-encode is written back as it was (only the orientation tag is updated, and the IFD1 thumbnail and MakerNote are removed); tags such as the EXIF pixel size are not rewritten after resizing. XMP and IPTC are not carried over.
  • An ImageDocument is not meant for concurrent mutation; reading the same document from several pipelines at once is fine because pipelines never write to their source.
Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.116-preview 0 11/1/2026
5.0.115-preview 81 11/1/2026
5.0.113-preview 34 10/31/2026

# Changelog

## 5.0 (unreleased)

### Breaking

- **New package, moved from CodeLogic.Common.** Imaging lived in CL.Common as `CL.Common.Imaging.CLU_Imaging`
 (with `ImageProcessOptions`, `ProcessedImage` and `ImageFormat`). It is now CodeLogic.Imaging, namespace
 `CL.Imaging`, with a new API; CL.Common no longer references SkiaSharp. `MIGRATION.md` maps every old method.
 `ImageFormat` gained members, so its numeric values changed; `imaging.invalid_options` is now
 `imaging.invalid_argument`.
- Targets **.NET 11** and requires **CodeLogic 5.0** (`Result<T>` / `Error` unions).

### Added

- `ImageDocument`: load from streams, bytes and files (sync and async) with a header pixel limit and a byte limit;
 `Create`, `FromSkiaBitmap`, `GetPixel`, `Clone`. Decodes JPEG, PNG, WebP, GIF (first frame), BMP, ICO, and HEIF/AVIF
 where the platform's Skia has the codec.
- `ImagePipeline` (`doc.Edit()`): a lazy fluent pipeline with `AutoOrient`, `Resize` (Fit, Fill, Stretch, Pad, Max;
 anchor; resample quality), `Crop`, `SmartCrop`, `Rotate`, `FlipHorizontal`/`FlipVertical`, `Brightness`,
 `Contrast`, `Saturation`, `Gamma`, `Hue`, `Exposure`, `Grayscale`, `Sepia`, `Invert`, `Tint` (fused into one pass),
 `GaussianBlur`, `Sharpen`, `Pixelate`, `RoundCorners`, `Border`, `Pad`, `Watermark` (image or text), `DrawText`,
 `DrawRectangle`, `DrawEllipse`, `DrawLine`, `Composite` (blend modes, opacity) and `StripMetadata`. Argument errors
 surface from `Apply()` / `Encode()` as `imaging.invalid_argument`.
- Encoding to JPEG, PNG and WebP (lossy and lossless) into pooled buffers (`EncodedImage`, with `ToArray`,
 `AsStream`, `WriteToAsync`, `SaveAsync`, `ToBase64`, `ToDataUri`); `ImageEncodeOptions` with `KeepIccProfile`
 (original profile written back byte for byte), `KeepMetadata` (original EXIF written back, orientation updated), `RemoveGps` (drop only the GPS position from
 kept EXIF) and
 `Background` for JPEG.
- `ImageInfo.Probe` (format, size, orientation, alpha, frames from the header only) and
 `ImageFormatExtensions` (`Detect`, `MimeType`, `FileExtension`, `CanEncode`).
- `ImageMetadata` / `ImageMetadataReader`: camera, lens, software, artist, copyright, date taken and offset, GPS,
 orientation, ISO, exposure, f-number, focal length, flash, read by a managed EXIF/TIFF reader from JPEG, PNG and WebP.
- Analysis: `ImageAnalysis.AverageColor`, `DominantColor`, `Palette`, `HasAlpha`, `IsMostlyTransparent`;
 `BlurHash.Encode`/`Decode`; `PerceptualHash.DHash`/`PHash` with `HammingDistance`, `Similarity`,
 `IsNearDuplicate`; `SmartCrop.FindRegion`; `ImageCompare.Difference` with an optional diff image.
- `ImageTools`: `ProcessUpload(Async)` (the former `ProcessImage`), `GenerateVariants(Async)` with `ToSrcSet`,
 `Thumbnail(Async)`, `ToDataUri`, `FitWithin`.
- `ImagingErrors` with `imaging.*` codes, `ImagingDefaults` / `ImagingSettings`, and an optional `ImagingLibrary`
 whose `imaging` config section (`DefaultMaxPixels`, `DefaultMaxBytes`, `DefaultQuality`, `DefaultFormat`,
 `StripMetadataByDefault`) sets the defaults, live on config changes.

### Changed

- Package versions are pinned (SkiaSharp and SkiaSharp.NativeAssets.Linux.NoDependencies 4.154.0) instead of floating (`4.*`).
- **Kept EXIF no longer carries the source thumbnail or MakerNote.** `KeepMetadata = true` zeroes the IFD1 thumbnail
 (and unlinks IFD1) and the MakerNote before writing the EXIF back. Both describe the source pixels (MakerNotes can
 embed previews and a vendor GPS copy), so a blurred face stayed visible in the thumbnail and coordinates survived
 `RemoveGps`. Other tags are still written back as they were.
- `BlurHash.IsValid`/`Decode` reject size flags above 80 (ten component rows; the spec allows 1-9 per axis), and
 `Decode` caps the output at `BlurHash.MaxDecodePixels` (1 048 576 pixels, new constant) and evaluates the
 separable cosine basis from tables, so a hash from a request cannot buy seconds of CPU per call.
- Reading a stream with no byte limit (`MaxBytes = 0`) rents at most 16 MiB up front instead of whatever the stream
 reports as its length; larger images grow the buffer as they are read.