CodeLogic.Imaging
5.0.116-preview
dotnet add package CodeLogic.Imaging --version 5.0.116-preview
NuGet\Install-Package CodeLogic.Imaging -Version 5.0.116-preview
<PackageReference Include="CodeLogic.Imaging" Version="5.0.116-preview" />
<PackageVersion Include="CodeLogic.Imaging" Version="5.0.116-preview" />
<PackageReference Include="CodeLogic.Imaging" />
paket add CodeLogic.Imaging --version 5.0.116-preview
#r "nuget: CodeLogic.Imaging, 5.0.116-preview"
#:package CodeLogic.Imaging@5.0.116-preview
#addin nuget:?package=CodeLogic.Imaging&version=5.0.116-preview&prerelease
#tool nuget:?package=CodeLogic.Imaging&version=5.0.116-preview&prerelease
CodeLogic.Imaging
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 withImageEncodeOptions.Lossless). JPEG output flattens transparency ontoImageEncodeOptions.Background(white). - Probe without decoding.
ImageInfo.Probe(stream)reads format, size, orientation, alpha and frame count from the header;ImageFormatExtensions.Detectsniffs the signature. - Editing pipeline.
doc.Edit()records steps;Apply()returns a newImageDocument,Encode(...)returns anEncodedImage. 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 byApply/Encodeasimaging.invalid_argument. The source document is never modified, and a pipeline can be run again.
- Geometry:
- Metadata.
doc.Metadata/ImageMetadataReader.Read(...)give anImageMetadatarecord: 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, PNGeXIfand WebPEXIFchunks; malformed EXIF yields fewer fields, never a failure. - Metadata on encode. By default EXIF is dropped (
ImagingSettings.StripMetadata).KeepMetadata = truewrites the original EXIF back (orientation reset to 1 afterAutoOrient; the source thumbnail and MakerNote removed), GPS included unlessRemoveGps = true;KeepIccProfile = truewrites 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/PHashwithHammingDistance,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,
CancellationTokenon every async method and between pipeline steps, all Skia objects disposed, encoder output inArrayPoolbuffers.
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
ImageDocumentis not meant for concurrent mutation; reading the same document from several pipelines at once is fine because pipelines never write to their source.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
net11.0
- CodeLogic (>= 5.0.0-preview && < 6.0.0)
- SkiaSharp (>= 4.154.0)
- SkiaSharp.NativeAssets.Linux.NoDependencies (>= 4.154.0)
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.