---
title: "Changelog"
url: https://recursiveai.org/oneimage/docs/changelog/
description: "Every release of the OneImage packages: what was added, changed and fixed."
version: "1.10.1"
source: https://github.com/recursive-ai-oss/oneimage-csharp/blob/1cac5abc37fd3268eb6bab6aa657ca381a6b11a1/CHANGELOG.md
---

# Changelog

All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [1.10.1] - 2026-10-25

### Fixed

- Valid PNG files could fail to decode, in every release from 1.0.0 to 1.10.0. The deflate decoder's fast loop could read a length/distance pair from bits it had not yet loaded when the pair followed a literal with a long code, which happens in files whose compressed data uses long codes for rare literals and for far, long matches (some optimizing encoders produce this; zlib and this library's own encoder did not in any test file). The stream then went out of step, and the read failed with "Invalid DEFLATE block type", "Stored block length check failed", "Invalid literal/length code", "Invalid scanline filter type", an Adler-32 mismatch, or an `ArgumentOutOfRangeException` from the data reader. The same decoder reads TIFF Deflate strips and tiles, PSD ZIP channels, SVGZ, WOFF tables and PNG `iCCP`/`zTXt` text, so those formats could fail the same way. The decoder now reloads its bits before such a pair.
- Wrong pixels without an error were possible for unusual compressed data, though rare in practice. The damaged bytes were caught only by the Adler-32 checksum or by the PNG scanline-header check, never by the decoder itself. In tests on the old decoder, using data that did trigger the bug, a whole-image read of a PNG, TIFF or PSD never returned wrong pixels: all of them failed (the six reported PNGs, 72 generated PNGs with the pattern at three densities, 40 generated streams each as TIFF and as PSD). Reading only part of a PNG could: a crop, region or band that ends inside the damaged data and before the next scanline header could return wrong pixels in its last rows with no error (1 of 956 partial reads of the reported files, in 1 of the 6 files; 43 of 11,448 partial reads of the generated files, in 29 of 72). Ordinary compressors (zlib at every level and strategy, and this library's own encoder) did not produce the pattern in any of about 200 test files; the six reported files came from an encoder that emits long codes for rare literals and for far, long matches.
- Complete reads of deflate data now check the checksum at the end of the stream, which an earlier release did only when the four trailer bytes happened to arrive with the last rows (about one PNG in ten thousand missed it). What is checked: a PNG read of every row, a TIFF Deflate strip or tile, a PSD ZIP channel, a WOFF table and a gzip-compressed SVG each verify the zlib Adler-32 (gzip: CRC-32 and size) and fail with a format error when a trailer that is present in full does not match. What is tolerated, as in earlier releases: a file that ends right after the last row or byte, or after 1 to 3 trailer bytes (gzip: fewer than the 8), is accepted and its pixels are returned unchanged; deflate data that ends before everything was produced is still an error. `iCCP` profiles and compressed XMP with a bad or missing trailer are dropped without failing the image. What is not checked: a read of only part of a PNG (a crop, a region, a streaming window) holds only part of the stream, so it cannot verify the checksum; only the chunk CRCs of the chunks it read cover it.
- A decode started from an image loaded with `Image.Load(Stream)` or `LoadAsync(Stream)` could read overwritten bytes if the `Image` became unreachable while the decode was still running (for example `Image.Load(stream).Save(...)` or `.ToArray<T>()` on a temporary). The compressed bytes of such an image live in a pooled buffer that is handed back to the pool when the image is collected, and the stream the decoder reads from did not keep that buffer alive; another thread could rent and overwrite it mid-read. Formats with checksums or strict structure (PNG, GIF, WebP, TIFF compression) then failed with a format error such as "Invalid DEFLATE block type", "Too many length or distance codes" or an Adler-32 mismatch; for data without redundancy (raw or lightly compressed pixels, JPEG coefficients) wrong pixels without an error were possible in principle. Present since 1.0.0. The stream now holds the buffer's owner. Loading from a file path or from memory you pass in (`ReadOnlyMemory<byte>`) is not affected by this.

## [1.10.0] - 2026-10-24

### Changed

- `RecursiveAI.OneImage.Text` is built on [`RecursiveAI.OneFont`](https://github.com/recursive-ai-oss/onefont-csharp) 1.2 or later for font parsing and shaping, instead of carrying its own copy: the sfnt, CFF and TrueType outline readers, the GSUB/GPOS engine and the script shapers, the bidirectional algorithm, line breaking and grapheme segmentation, and the Unicode tables. WOFF, WOFF2, hinting, color glyphs, vertical metrics, word-breaking modes and the layout stay here. The public API of `OneImage.Text` is unchanged and drawing and measuring are byte-identical to 1.9.0 over the test fonts, the complex-script case files and the SVG text tests. `OneFont` is the package's one new dependency; `RecursiveAI.OneImage` itself gains none.

### Fixed

- `TextShaping.ShapeRun` returns clusters that start at offset 0 and meet without gaps even when the shaper drops characters: a leading zero width non-joiner before a Kannada letter now belongs to the first cluster, and text of only joiners gives one empty cluster instead of none.
- A glyph id that the font does not have (a character map or shaping result pointing past the glyph count) is drawn and measured as the missing glyph instead of taking the last glyph's advance.

## [1.9.0] - 2026-10-24

### Added

- `ImageFrame.ReleaseDecodedPixels()` is public, so one frame of a multi-frame image can give its decoded pixels back.
- `CopyPixelsTo(destination, rect, useRegionCache: false)` on `Image` and `ImageFrame`: one region or band read that builds no region plane and counts toward none, whatever `ImageEngine.RegionCacheBytes` says. Bands read from the top down decode the image once and hold only the decoder's few tile rows; a read that has to go back above them decodes again for that call and keeps nothing afterwards. Pixels are identical.

### Fixed

- `ReleaseDecodedPixels()` now gives back everything decode-derived: it also clears the cached tiles of every operation above the decode (a gray conversion, a resize) and of a random-access decode (TIFF), where before only the region plane and sequential-decode rows went. Releasing through `frame.ToImage()` (or the image the frame came from) now frees the frame's region plane; planes used to belong to the frame object, so a single-frame image made from it had its own empty set.

## [1.8.1] - 2026-10-24

### Changed

- Reading an 8-bit RGB or RGBA image as `Gray8`, `Gray16` or gray+alpha (`CopyPixelsTo`, `ToArray`, direct decodes, region reads) is several times faster: the RGB-to-gray conversion now runs in integer SIMD (AVX2, 128-bit, scalar) instead of through float32 rows. A pixel whose exact value lies close to a rounding boundary is recomputed with the float expression, so every output byte is identical to before (checked for all 2^24 RGB triples and every alpha). A 4 MP-class page read as `Gray8` now costs about the same as the `Rgb24` read.

## [1.8.0] - 2026-10-24

### Added

- `ImageEngine.RegionCacheBytes` (default 64 MiB per frame, 0 = off) and `Image.ReleaseDecodedPixels()`: repeated `CopyPixelsTo` region reads (bands, random rectangles) of a large JPEG or PNG no longer decode the image again. See the guide's pixel-access section.

### Changed

- Region reads of large JPEG/PNG frames: from the second region read on, the rows read are kept in a plane in the requested pixel format (at most `RegionCacheBytes` per frame, released by `ReleaseDecodedPixels()` or when the image is collected). Reading 44 Gray8 bands or 20 random rectangles of an A4 page now costs about one decode instead of up to three, and a rotated (EXIF) image no longer restarts decoding for each output row. The price is memory: a Gray8 plane of an A4 page is about 8.7 MB and an Rgb24 plane about 26 MB, kept while the image is alive. Set `RegionCacheBytes = 0` for the previous behaviour. Pixel values are unchanged.
- The first `Image.Load` of a PNG or JPEG costs less resident memory (about +2.5 MB instead of +6 to +11 MB): the header parsers are split so the JIT compiles small methods instead of one large one.

## [1.7.0] - 2026-10-23

### Added

- `PixelResizer`: an allocation-free resize from a span to a span (`Resize<TPixel>` for the pixel structs, `ResizeRaw` for any `PixelFormat` with up to four channels, and `PixelResizer.GetSize` for the size `Image.Resize` would scale to). Output is byte for byte what `Image.Resize(w, h, Fit.Stretch, ...)` gives for every filter and mode (checked against 2,500 hashes captured from 1.6.0, on the scalar, AVX2 and AVX-512 paths). The resizer keeps its buffers, so after a warm-up it allocates nothing per call even when every call has a new size; `ScratchBytes`, `maxScratchBytes` and `Trim()` bound and release them. Calls with enough pixels split their output rows over up to `maxDegree` threads without allocating. On 2300x60 gray lines to height 32 (Lanczos3, linear light) it takes 70 us at the default degree (255 us on the calling thread alone) against 255 us for 1.6.0 `Image.Resize`, and allocates nothing per call against 6 KB (same size) or 140 KB (a new width each call). See the guide.

### Changed

- `Image.Resize` allocates less when it meets a size it has not seen: the tap tables are built in per-thread scratch and only their exact size is allocated (140 KB to 77 KB per call for 2300x60 to height 32 with a new width each time). The 8-bit gray conversions are faster (255 to 205 us for the same-size case). Output is unchanged.

## [1.6.0] - 2026-10-23

### Added

- JPEG 2000 decoding (internal, decode-only), for the PDF bridge: JP2 files and raw codestreams, all progression orders, tiles, precincts, layers, every code-block style, the 5-3 and 9-7 wavelets, palettes, channel definitions, sYCC, reduction levels and regions; lossless streams are bit-exact and lossy ones agree with OpenJPEG within one level at 8 bits. Passes ITU-T T.803 Class 1 (52 of 52 components), refuses hostile headers before allocating (limits in `docs/jpx.md`), and was fuzzed for 10 minutes without findings. Tiles decode in parallel with deterministic output; a 12-bit 2048x2048x3 stream with 256 tiles takes 319 ms instead of 539. It is not in the default `ImageConfiguration`.
- `RecursiveAI.OneImage.Pdf` (new package): PDF pages as an image format on OnePDF 1.2 display lists. Each page is a frame, drawn in 128 px cells on demand (a region is byte for byte the same as that part of a whole render, at any degree), at the requested size when resized. Paths, strokes, clips, text, images (JPEG, JPEG 2000 and JBIG2 decoded by OneImage; damaged JPEG 2000 and JBIG2 images are left out), shadings, tiling patterns, transparency groups (isolated, non-isolated and knockout), text and fill-and-stroke knockout, soft masks, blend modes, and form and markup annotations through OnePDF's generated appearances (`PdfRenderOptions.GenerateAppearances`). `PdfRenderLimits` bound files, images, nesting, clips and pattern tiles, with a hostile-file test each, and `fuzz/run.sh pdf` fuzzes the whole path. Rendering is checked against PDFium, MuPDF and pdf.js as a signal: 26 of 307 pages of OnePDF's test set are flagged for review against ISO 32000-2 (24 ambiguous, 2 where the renderer follows the specification and the references do not, none wrong), with a verdict and section for each. Known gap: soft-mask `/TR` is treated as identity. See [docs/pdf.md](https://recursiveai.org/oneimage/docs/pdf/).
- JBIG2 decoding (internal, decode-only), for the PDF bridge: generic, refinement, symbol dictionary, text, pattern dictionary and halftone regions, MMR, custom and standard Huffman tables, striped and unknown-height pages, standalone files and PDF-embedded streams with globals. Bounded by `Jbig2Limits` (one crafted-stream test per limit) and fuzzed (`fuzz/run.sh jbig2`). Checked against T.88 Annex H and 2327 streams from public PDFs (1925 identical to `pdfimages`, 402 the exact inverse because of `/Decode [1 0]` or `/ImageMask`, none different). See [docs/jbig2.md](https://recursiveai.org/oneimage/docs/jbig2/).
- `OneImage.Layers`: layer styles draw gradient overlays, pattern overlays and bevel and emboss. `GradientOverlayEffect` and `EffectGradient`/`EffectGradientFill` (linear, radial, angle, reflected, diamond; midpoints, opacity stops, smoothness, reverse, scale, offset, align), `PatternOverlayEffect` with `EffectPattern`/`EffectPatternFill` (scale, link with layer, phase), `BevelEmbossEffect` (inner and outer bevel, emboss, pillow emboss and stroke emboss; smooth, chisel hard and soft; depth, size, soften, up or down, angle and altitude, highlight and shadow mode, color and opacity, gloss contour) and gradient or pattern paint for `StrokeEffect` (and a shape burst gradient) and a gradient for the outer and inner glow. `LayerStyle.AlignBox` sets the rectangle that aligned gradients and patterns are laid out on. The new effects have language-neutral `Describe()` names and bounded parameters, and are drawn per tile with the same dirty-rectangle reach as the other effects.
- `RecursiveAI.OneImage.Psd`: gradient and pattern overlays, bevel and emboss, gradient and pattern strokes and gradient glows are read from `lfx2`/`lmfx` and drawn, and are written when a style is created or edited (a pattern that the document does not hold is added to its pattern resources). 8 more sample files are fully modelled; the files that use these effects are 1.5 to 20 levels closer to their stored composite, and the files that were modelled are unchanged. Bevels, pattern phase on text and linear dodge/burn are approximations (see the PSD page).
- `RecursiveAI.OneImage.Psd`: `PsdFile.Save` writes layer styles and gradient and pattern fills that are created or edited in the model. A style that differs from the one read is written as a new effects block (`lfx2`) from the model, keeping the items of the original block that the model has no field for (gradient and pattern overlays, bevel and emboss, gradient strokes and glows, grain, and so on) and writing unchanged effects back as they were; an untouched style is still written back byte for byte. A new or replaced linear or radial gradient, bitmap pattern or hatch brush on a fill layer is written as `GdFl`/`PtFl` settings (patterns go to the document's pattern resources) beside its raster, and a brush with no such equivalent is written as a pixel layer instead of making `Save` throw. Disabled effects are now read as disabled `LayerEffect`s instead of being left out of the style. See [docs/psd.md](https://recursiveai.org/oneimage/docs/psd/#writing-fills).

### Changed

- WebP lossless encoding of small images uses less memory, with byte-identical output: the match-search hash table of an image of up to 16384 pixels (the picture and the predictor, color-transform and palette maps of a file) is sized to the image instead of a 1 MiB array per entropy-coded image. A 400x300 lossless encode allocates 4.9 MB instead of 12 MB and a 2000x1500 one 44 MB instead of 54 MB. The lossy encoder is unchanged: its 2000x1500 source planes are needed in full, and the thumb-webp gate row's excess over native encoders is one-time JIT cost (see [docs/performance.md](https://recursiveai.org/oneimage/docs/performance/#small-and-medium-encodes)). `gate-peak` takes an optional `GATE_WARMUPS=N` to show that cost; the default stays one warm-up.
- Progressive JPEG decoding is much faster, with identical output. Refinement scans read their correction bits through a mask of the nonzero coefficient positions instead of one coefficient at a time, and the scans of a progressive Huffman file of 1 MP or more decode side by side, each waiting only for the rows of earlier scans it depends on; rows reach the transform while the last scan is still running. On a 24 MP file: 389 to 68.5 ms (noisy) and 199 to 62.3 ms (smooth) at full size, 38.5 to 12.8 ms at 1/8 scale (ImageSharp 3.1.12: 432 and 210 ms; NetVips: 404 and 257 ms). A progressive file that fails in a late scan may now deliver some rows to the destination before the exception, as the streaming decoders already do; the exception is the same as before.

### Fixed

- `RecursiveAI.OneImage.Text`: hinting no longer lets a hostile font throw out of the interpreter: `ABS`, the cut-in compares of `MIAP`/`MIRP`/`MDRP` and the other absolute values no longer overflow on the most negative integer, and `ISECT` divides in 128 bits. The twilight zone is now kept as the control value program leaves it (each glyph starts from a copy), so fonts whose `prep` places twilight points hint as intended. All programs of one top-level glyph share a budget of 2^20 instructions; a composite that exceeds it is drawn unhinted instead of costing up to 2^18 per component.
- `RecursiveAI.OneImage.Psd`: a pattern channel whose rectangle claims more data than the file holds is refused before its buffer is allocated, and the linear-light interpolation of a gradient glow is written and read back like that of other gradients.
- `OneImage.Layers`: an edit of a layer's pixels now also recomputes the tiles that an inner effect (inner shadow, inner glow, inside stroke) draws from the edited shape, not only the ones outer effects reach. The pixels were correct after a fresh render; an incremental render could leave a stale tile next to the edit.
- `RecursiveAI.OneImage.Psd`: an effect contour with a point that is not a number, or two points a hair apart that become one float, is skipped and counted in `EffectsNotDrawn` instead of throwing an argument exception (found by fuzzing).

## [1.5.0] - 2026-10-23

### Added

- `RecursiveAI.OneImage.Psd`: gradient and pattern fill layers (and shape layers filled with them) are drawn from their settings as `FillLayer`s instead of loading as stored pixels. Gradients cover linear, radial, angle, reflected and diamond styles with midpoints, opacity stops, smoothness, reverse, scale, offset and linear-light interpolation; patterns come from the document resources. Where Photoshop stored a raster the colors are taken from it (alpha stays the drawn one); noise or perceptual gradients, scaled or rotated patterns, boxes under 16 px and malformed descriptors fall back to the stored pixels. Unchanged fills are written back from their original block. `PsdReadOptions.DrawFills` (default true) restores the old behavior. The median error over the 152 previously modelled sample files stays 0.134 levels; 160 files are now fully modelled (median 0.108), and `gradient-fill.psd` goes from 8.3 to 0.0 levels. See [docs/psd.md](https://recursiveai.org/oneimage/docs/psd/#gradient-and-pattern-fills).
- Photoshop-style options on three tone operations, with the existing overloads unchanged: `Image.Posterize(levels, linear, PosterizeMode.Buckets)` (equal input bins rounded down), `Image.Curves(points, channel, linear, CurveInterpolation.Natural)` (a natural cubic spline) and `Image.BrightnessContrast(brightness, contrast, linear, BrightnessContrastMode)` with `PhotoshopLegacy` (exact) and `PhotoshopSmooth` (an approximation of the current mode). `Adjustment.Posterize`, `Curves` and `BrightnessContrast` have the same overloads. They fuse with other tone steps like the rest.
- `OneImage.Layers`: layer styles. `Layer.Style` takes a `LayerStyle` of drop shadows, inner shadows, outer and inner glows, strokes (outside, inside, center), color overlays and satin, each with blend mode, opacity, color, size, distance, spread or choke and an optional `EffectContour`. Effects are drawn from the layer's shape when the document is composited, with exact signed-distance strokes and Gaussian blurs, and an edit recomputes only the tiles the effects reach. See [docs/layers.md](https://recursiveai.org/oneimage/docs/layers/#layer-styles).
- `RecursiveAI.OneImage.Psd`: layer effects (`lmfx`/`lfx2`) are read into `Layer.Style` and drawn; `PsdLimits.MaxEffectsPerLayer` (32) bounds them, and `PsdLayerInfo.EffectsNotDrawn` / `EffectsApproximated` count what is not reproduced (gradient and pattern overlays and strokes, gradient glows, bevel and emboss, random grain). The effects block is written back byte for byte; saving a layer with a changed style throws `ImageOperationException`. Against the stored composites of the sample files, 10 more files are fully modelled (170, median 0.111 levels); the stroke files went from 15-40 levels to 0.00-0.16, and layer_effects from 108 to 4.6. See [docs/psd.md](https://recursiveai.org/oneimage/docs/psd/#layer-effects).

### Changed

- JPEG decoding is faster, with identical output. Full size: the per-row transform is split in two (inverse DCT; then upsampling, color conversion and delivery) and, with three or more cores, each runs on its own thread beside the entropy decoder; with two cores the entropy thread does the inverse DCT itself, and progressive or multi-scan frames overlap the delivery with the transform. A 24 MP 4:2:0 photograph decodes into a reused buffer in 32 ms (was 44-55 on the same loaded worker; ImageSharp 82-97); noisy 8.9 MB files stay entropy-bound at 43 ms, and a smooth 2 MB one goes from 34 to 20 ms on a desktop host. The entropy decoder may now fail on data past the last row a request needs without failing the request, as the single-threaded path never read it. Restart markers: the restart intervals of baseline Huffman scans (1 MP or more) are entropy-decoded on worker threads at the per-call degree, then delivered in order; damaged files give the same rows and the same exception as before. A 24 MP 4:2:0 file with one interval per MCU row decodes in 25.5 ms instead of 32.3 ms at full size and in 9.0 ms instead of 27.0 ms at 1/8 (thumbnail 400 px: 9.0 ms instead of 27.1 ms); files without restart markers are unchanged. Reduced sizes: the 4x4 and 2x2 inverse DCTs of 1/2 and 1/4 scale decoding run on 128-bit vectors (SSE/AVX2 hosts and ARM64 AdvSimd) with scalar references, for 8-bit and 12-bit samples; a 4x4 block takes 26 ns instead of 72 and a 2x2 block 20 ns instead of 32, and a 24 MP decode at 1/2 goes from 38.9 to 31.9 ms (the entropy floor of that file, so 1/4 and 1/8 do not change). 128-bit transforms: the full-size inverse DCT and the forward DCT with quantization have an SSE2/SSE4.1 and AdvSimd path between AVX2 and scalar, bit-identical to both. With AVX2 turned off, a 24 MP decode takes 47 ms (was 79) and a q90 encode 179 ms (was 225); AVX2 times are unchanged. Decode hashes of the sample corpus and of 24 MP files at every scale and hardware setting are the same as before. `tools/JpegHash` writes the test files and prints decode hashes for comparing two builds.
- JPEG encoding is faster, with byte-identical output. Color conversion and chroma averaging use vector kernels, quantized blocks are reordered by placing only their non-zero values, and without restart markers each worker takes the next MCU row and does all of its work (so there is no barrier between stages, and symbol counts are gathered in the same pass as the transform). Finished rows are appended to the output by whichever worker is free. A 24 MP q90 4:2:0 encode with optimized tables takes 51.7 ms at the default degree (was 82.8; ImageSharp 101.7, NetVips 53.3), the gate peak memory went from 4.9 to 3.1 MiB, and a 3 MP encode from 7.3 to 5.2 ms.
- TIFF reading is faster, with identical pixels at every degree and hardware path. Whole-frame reads of a frame of 1 Mpixel or more (`CopyPixelsTo`, `ToArray`) decode strips and tiles in parallel straight into the caller's buffer instead of through 128×128 tiles; blocks are handed out one at a time and returned to the pool as soon as they are converted, so memory stays at a few blocks per thread. MSB-first LZW strips (the usual TIFF kind) go through a whole-block decoder that holds short strings inline, and the 8-bit horizontal predictor is undone with a vector prefix sum; on a corrupt block whose output is already full, a trailing bad code is now ignored, as libtiff does. The 10-page 2000×1500 LZW read takes 46.9 ms at the default degree (was about 60 before the LZW work and 49.9 after it) and allocates 92 KB (was 965); at 8 threads 32.0 ms and on all cores 27.1 ms (NetVips 26), measured on the benchmark worker under load from other jobs. Smaller frames, region reads and frames with a strip over 16 MiB of raw samples keep the tile path. The tile-to-buffer copy of a computed image was measured at 1.9 times a contiguous copy of the same bytes (6.1 ms for 24 MP Rgba32 at the default degree) and left as it is; see [docs/performance.md](https://recursiveai.org/oneimage/docs/performance/#whole-frame-copy-out).
- WebP encoding needs much less working memory, with byte-identical output. A lossy still image is converted to the encoder's YUV planes strip by strip instead of through a whole-frame RGBA copy (alpha is gathered, one byte per pixel, only if some pixel is translucent); the lossless encoder packs its 32-bit pixels band by band the same way. The lossy encoder no longer keeps whole reconstructed planes (only the last line of each macroblock row and the row being coded), stores the quantized levels compactly (about 1/4 of their former size on a noisy photograph), writes its partitions in 64 KiB chunks instead of doubling arrays, and the file is written to the destination chunk by chunk rather than assembled first. Peak working memory of a 24 MP lossy q75 encode went from 304 to about 101 MB (SkiaSharp 87, Magick.NET 173, NetVips 237, ImageSharp 1450); a 3 MP lossy encode from 22 to 5 MB; a 24 MP lossless encode from 1055 to 851 MB (and to 377 MB by the lossless-state work below). See [docs/performance.md](https://recursiveai.org/oneimage/docs/performance/#webp-encode-memory).
- The lossless WebP encoder needs less than half the working memory, with byte-identical output. Its transforms run in place on the packed pixels, matches are stored instead of one token per pixel, hash chains cover only the last 2^20 pixels, and the output is written into one array of its exact size. Peak working memory of a 24 MP lossless encode went from 852 to 377 MB (SkiaSharp 663, Magick.NET 737) and a 3 MP one from 107 to 46 MB; time did not change beyond noise (24 MP: 1.07 s to about 0.96 s on the benchmark worker). See [docs/performance.md](https://recursiveai.org/oneimage/docs/performance/#lossless-encoder-state).
- `RecursiveAI.OneImage.Psd`: posterize, curves and brightness/contrast layers read from PSD files use Photoshop's formulas, and brightness/contrast in Photoshop's current mode is read from its `CgEd` descriptor (it was read as no adjustment). Against the stored composites of 19 sample files the median mean error went from 10.5 to 0.005 levels, and the median over the 152 fully modelled files from 0.275 to 0.134. A model-built posterize, curves or brightness/contrast adjustment saved to PSD reads back with those formulas. See [docs/psd.md](https://recursiveai.org/oneimage/docs/psd/#adjustment-formulas).

### Fixed

- `RecursiveAI.OneImage.Psd`: a hue/saturation adjustment with saturation or lightness outside -100..100 (found by fuzzing) is kept and not reproduced instead of throwing an argument exception when the file is loaded.
- `bench/gate.sh --skip-peak` no longer crashes while writing its report.

### Known limitations

- Known limitations of the PSD package: saving a PSD after a layer style was edited or added throws `ImageOperationException`, and so does saving after a gradient or pattern fill's brush was replaced; writing layer styles from the model is not built yet. A style or fill left unchanged is written back byte for byte. See [docs/psd.md](https://recursiveai.org/oneimage/docs/psd/#known-limitations).

## [1.4.0] - 2026-10-23

### Added

- `OneImage.Layers`: a layered document that renders on demand. `Document` holds pixel layers (a `Canvas` at an offset), pass-through and isolated groups, fill layers (solid or gradient brush) and adjustment layers (`Adjustment.Levels`, `Curves`, `HueSaturation`, `ColorMatrix`, ...), each with visibility, opacity, fill opacity, any `BlendMode`, a raster mask (density, feather, outside value), clipping to the layer below and lock flags. `Document.Render()` is a lazy `Image`; a stack of layers renders to exactly the bytes of the equivalent chain of `Composite` calls. `Snapshot()` takes O(layers) and finds the tiles that changed by comparing canvas tile tables, so a re-render after an edit recomputes only the dirty tiles. Layers and documents have serializable descriptions with language-neutral names. See [docs/layers.md](https://recursiveai.org/oneimage/docs/layers/).
- `DocumentOptions.Spill`: an opt-in disk-spill tile store for a document. Cold tiles move to a temporary file (unlinked on Unix, delete-on-close on Windows) under a byte budget and come back on demand.
- `RecursiveAI.OneImage.Psd` (new package, namespace `OneImage.Psd`): Photoshop PSD and PSB. `PsdFormat` reads the merged image (gray, RGB, indexed, bitmap, CMYK, Lab; 8, 16 and 32 bits; raw, RLE and ZIP channels) as an ordinary `Image` and writes flat documents; `PsdFile` loads the layer tree into an `OneImage.Layers.Document` (pixel layers with offsets, groups, blend modes, opacity and fill opacity, raster and vector masks, clipping, solid fills, levels/curves/hue-saturation/brightness-contrast/exposure/invert/threshold/posterize adjustments) and writes it back, keeping what the model cannot express as blocks (text, smart objects, layer effects, gradient and pattern fills and vector strokes render from their stored pixels and are written back untouched; multichannel documents are refused). Limits for every size read from the file, a `psd` fuzz target and a crafted file per limit. See [docs/psd.md](https://recursiveai.org/oneimage/docs/psd/).

### Changed

- Layers: rendering is fast and bounded in memory. A layer is composited onto a tile by reading its tiles in place and writing into a tile the render owns, with no per-tile nodes or descriptors, and the disk-spill store reuses an evicted tile's memory as soon as no reader can still hold a span of it, without forcing garbage collections. A cold flatten of 10 full-size 24 MP layers takes 77 ms at the default per-call degree of 4 threads and 34 ms at degree 32, and 50 layers take 361 and 129 ms, allocating 0.25 MB; ImageSharp 3.1.12 takes 72 and 297 ms using every core, so at the default degree this is slower than ImageSharp on those rows (published as an exception in [docs/performance.md](https://recursiveai.org/oneimage/docs/performance/#layer-flatten-exception)). With AVX2 only, 115 and 544 ms. A 100-layer 24 MP document renders in 1.4 s at about 540 MB peak resident under a 256 MiB spill budget. A pass-through group clipped to a layer below draws its layers onto the clip surface one after another, as if each were clipped to the base. See [docs/layers.md](https://recursiveai.org/oneimage/docs/layers/).
- `Image.Composite` of 8-bit RGBA onto 8-bit RGBA with the Normal blend mode and source-over runs a vector kernel with bit-identical output to the float path, at about a fifth of the CPU time. The kernel converts through signed integers (AVX2 has no unsigned conversions, so the old code emulated them) and skips the alpha work over an opaque backdrop, which makes it 6.8 times faster on CPUs with AVX2 but without AVX-512 (12.2 to 1.8 ns per pixel).
- Vector kernels no longer lean on conversions that CPUs without AVX-512 have to emulate. Error-diffusion dithering (`Dither`, `Quantize` with a dither), the luma step of `HistogramEqualization`, the 8-bit color-matrix kernel (`ColorMatrix`, `Sepia`, ...) and the float-path RGBA `Composite` kernel converted unsigned 32-bit lanes to float and back, which AVX2 and SSE do not have; they now go through signed lanes and the non-saturating conversion where the range is known. Output is byte-identical on every hardware path (checked by hash under default, AVX-512 off, AVX2 off and intrinsics off). Per pixel on one thread with AVX-512 off: error-diffusion Floyd-Steinberg 60.8 to 18.5 ns (3.3 times), the equalization luma step 0.35 to 0.18 ns, the color-matrix kernel 2.6 to 1.5 ns, the float RGBA composite kernel 2.15 to 1.12 ns. The color matrix also has a four-pixel path for CPUs with 128-bit vectors only (3.7 to 3.1 ns RGBA, 3.8 to 2.5 ns RGB).
- JPEG entropy decoding is faster, with identical output. AC symbols of images of 256 K pixels or more resolve their Huffman code and magnitude bits with one lookup in a 12-bit table; the bit buffer is topped up once per symbol without a branch on its fill level; and progressive AC scans keep the buffer in locals. At a reduced scale where a component contributes only its DC coefficient (the luma of a 4:2:0 image at 1/8), that component's AC data is read past without being stored, and progressive AC scans of such a component are skipped (before, only images where every component was DC-only did this). A 24 MP 4:2:0 photograph decodes at 1/8 scale in 28 ms (was 38), and a 24 MP CMYK file in 90 ms (was 111) on one host; a 24 MP thumbnail (`thumb-jpeg`) takes 27-28 ms (was 38-40) on the benchmark machine. Full-size 4:2:0 decodes are bound by the transform thread, not by entropy decoding, and are unchanged. Decoded pixels are identical to before at every scale and mode (checked by hash over the test corpus with and without hardware intrinsics, and by a test that compares the lookahead path with the plain tables on good and damaged files).
- Resize costs less CPU, with identical output: a box pre-reduce of 8-bit RGB/RGBA now converts and averages in one pass instead of writing a float row first, the horizontal pass computes four output pixels at a time and the vertical pass four vectors at a time (both hide add latency; every sum keeps its tap order). On a decoded 24 MP image at the default degree, 24 MP to 400 px takes 8.8 ms (was 11.5) and a 3x reduction 25.3 ms (was 30.0).
- `RecursiveAI.OneImage.Svg`: filter speed. Gaussian blur and the 8-bit/float/linear-light conversions of filters run on AVX kernels with scalar references and byte-identical output (a 1024 px blur of 400 shapes 10.7 ms against 33.5, a 4096 px blur 156 ms against 491, a drop shadow 17.8 ms against 44.1); the blur works in place without a second buffer, skips empty lines and runs in parallel bands.

## [1.3.0] - 2026-10-22

### Added

- `AlphaComposition.Plus` (descriptor name `plus`): the Porter-Duff plus operator, CSS `plus-lighter`. Premultiplied color and alpha of overlay and backdrop add and clamp at 1; an empty overlay leaves the backdrop, and an opaque backdrop stays opaque.
- `OneImage.Text`: right-to-left text. The Unicode Bidirectional Algorithm (UAX #9, Unicode 17) runs per paragraph, lines are reordered after wrapping, and `TextOptions.TextDirection` (`Auto`, `LeftToRight`, `RightToLeft`) sets the base direction. `TextAlignment.Start` and `End` follow the paragraph direction. Left-to-right text without right-to-left characters skips the algorithm.
- `OneImage.Text`: an Arabic joining shaper (also used for Syriac and N'Ko) with per-glyph form features, and Hebrew with points and cantillation marks.
- `OneImage.Text`: complex-script shaping. An Indic shaper (Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada, Malayalam: syllables, reph, half forms, pre-base matras, split vowels, dotted circle for broken clusters), a Universal Shaping Engine shaper (Khmer, Myanmar, Sinhala, Tibetan, Balinese, Javanese and the other USE scripts) and Thai/Lao shaping (SARA AM). Unicode property tables for them are generated by `tools/gen_unicode_tables.py`. Syllable length, reordering and run size are bounded. Line breaking for Thai, Lao, Khmer and Myanmar stays UAX #14 without dictionaries. Against SixLabors.Fonts 2.1.3 on 1,042 cases in 17 Noto subsets, 897 match exactly, 119 differ deliberately (listed with reasons in `docs/text.md`) and the reference throws on 26. A Devanagari paragraph measures in 1.4 ms against 4.0 ms and draws in 2.5 ms against 10.2 ms; a Khmer paragraph 0.53 against 2.8 ms and 1.4 against 7.0 ms (local smoke, loaded host). A `complex` fuzz target covers the shapers.
- `OneImage.Text`: vertical and bottom-to-top layout (`TextOptions.LayoutMode`, with the members of SixLabors.Fonts' enum), from `vhea`, `vmtx`, `VORG`, the `vert`/`vrt2` features and the UAX #50 orientation property; TrueType hinting (`TextOptions.HintingMode.Standard` runs the font's instructions at the drawing size, y positions only, under hard limits; measuring is unchanged); and COLR version 1 paint graphs (gradients, transforms, clips, composite modes) painted by `DrawText`. Compared with SixLabors.Fonts 2.1.3 (vertical: 296 of 296 measurement cases; hinting: mean 0.45 against 1.47 levels unhinted at 9-16 px) and SkiaSharp (COLR v1: mean 0.36 levels). `fuzz/run.sh font` covers hinting and paint graphs. See `docs/text.md`.
- Tests run the official `BidiTest.txt` and `BidiCharacterTest.txt` in full and compare glyphs and layout with SixLabors.Fonts 2.1.3; a `bidi` fuzz target covers mixed-direction text.
- `RecursiveAI.OneImage.Svg` (new package): SVG and gzip `.svgz` as an image format, registered with `new ImageConfiguration().Add(SvgFormat.Instance)` (not part of `ImageConfiguration.Default`, so it trims away when unused). It is drawn directly at the size you ask for, one region at a time (see `DecoderCapabilities.RenderAtSize` below), as 8-bit sRGB RGBA. It draws shapes and paths with fills and strokes (caps, joins, dashes, `paint-order`), linear and radial gradients (focal point, `spreadMethod`, `href` inheritance) and patterns, markers, opacity, `mix-blend-mode` with every CSS blend mode and `isolation`, transforms and `transform-origin`, nested `svg`, `use` and `symbol`, `clipPath` and `mask`, `<style>` sheets (type, class, id, attribute, combinator and structural selectors, specificity, `!important`), `switch` with `systemLanguage`, `data:` images (any raster format the library reads, and nested SVG), all filter primitives and the CSS filter functions, and text (`tspan`, `textPath`, per-character positioning, bidirectional and vertical text, decorations, fallback, paint servers on text, text as a clip) with fonts from `SvgFontOptions` (files, bytes, streams, generic families, installed fonts; `DefaultFontSize` is 16, the CSS `medium` size). It never does I/O: external references, `@font-face`, scripts, SMIL animation and `foreignObject` are not loaded or run, and the XML parser (our own, iterative) refuses DTDs and entity declarations. Documents are untrusted: `SvgLimits` bounds document size, elements, nesting, attributes, style sheets and matching work, path segments, `use` fan-out and reference depth, layers, pattern tiles, embedded images, filter size and text, a document that exceeds one fails with `ImageLimitException` before the memory is allocated, and every limit has a hostile-document test. The XML, path-data and render fuzz targets ran clean. Against the public test collection of the resvg project (its files and reference PNGs only; a test passes at no more than 0.25% of pixels differing by more than 24 of 255 over white), 1,513 of 1,706 files pass (88.7%): shapes 128 of 133, painting 277 of 307, structure 197 of 230, paint-servers 146 of 154, masking 91 of 93, filters 395 of 406 (97.3%), text 279 of 383 (72.8%; variable and color fonts are not supported). Edge-only failures come from exact-area anti-aliasing against the reference's supersampling. An icon at 256 px renders in 141 µs including the load and a 3,000-shape illustration at 1024 px in 25 ms, against 363 µs and 79 ms for Svg.Skia 5.2.3. See `docs/svg.md`.
- `OneImage.Text`: `TextShaping` (`GetBidiLevels`, `GetVisualOrder`, `ShapeRun`, `IsUprightInVerticalText`), `ShapedCluster` (advance, outline, `VerticalAdvance`, `GetUprightOutline`), `ShapingOptions` (including `Vertical`) and `FontFaceInfo`, for callers that lay text out themselves: shaped clusters with their advances and outlines, one bidirectional paragraph at a time, upright or rotated in a vertical column.
- `OneImage.Text`: subtables the shaping engine cannot use now count against a run's work budget, so a font with lookups of tens of thousands of them no longer costs seconds per run (a fuzz-found font that took 3.5 s per 1,200 glyphs now takes milliseconds; shaping results are unchanged).
- `Brush.LinearGradient(..., Matrix3x2 transform, ...)` places a gradient through an affine transform; `Brush.RadialGradient(center, radius, focalCenter, focalRadius, ...)` is a two-circle (focal) gradient with an optional transform; `Brush.FromImage(source, ImageSampling.Bilinear, ...)` reads an image brush with bilinear sampling on premultiplied color (new `ImageSampling` enum). The existing brushes are unchanged.
- `DecoderCapabilities.RenderAtSize` and `DecodeRequest.TargetWidth`/`TargetHeight`: a vector decoder is asked for the frame at its final size when a resize sits directly on the load, so nothing is scaled as a raster.
- `Image.ApplyMask(mask, channel, x, y, linear)` and `MaskChannel`: multiplies an image's coverage by a per-pixel mask taken from another image (its alpha, or its luminance times alpha), lazily and tile by tile; everything outside the mask becomes transparent. This is the layer-mask operation the SVG renderer composites masked groups with.

### Changed

- Tiles replaced by a write after `Canvas.Snapshot()` are reference counted. When a snapshot's tile table has outlived a collection, its tiles go back to the tile pool once the last snapshot reading them is collected, instead of being left to the garbage collector; `Canvas.Dispose` and `Image.FromPixels` do the same at once. Ten whole-frame draws on a 24 MP canvas, each after a dropped snapshot, allocate about 400 MB instead of about 940 MB, and in-place draws after a snapshot on a 2000 x 2000 canvas allocate a third less. Snapshots that are dropped within one collection are not tracked, so small edits cost what they did.
- `Resize` with `Fit.Cover` pushes its crop down to a decoder that can produce a region.
- Drawing and SVG rendering cost less per shape: a `Canvas` fill reuses its flattened path, edge table and buckets from one fill to the next on a thread, the tile rasterizer sums and clears only the accumulator cells an edge touched, tile workers no longer share one scratch bag, and 8-bit/float RGBA row conversion skips its temporary float rows. Output is unchanged. In `RecursiveAI.OneImage.Svg`, opacity and blend layers are read in place and give their tiles back to the pool, which cuts the allocation of a blend-heavy drawing from 65 MB to 7 MB per render; an icon at 256 px renders in 118 µs (298 µs before) and the 3,000-shape illustration in 11.5 ms (25.2 ms).
- Packaging: `RecursiveAI.OneImage.Text` and `RecursiveAI.OneImage.Svg` use internals of the packages they build on, so they now depend on the exact version they were built with (`[x.y.z]` of `RecursiveAI.OneImage`, and of `RecursiveAI.OneImage.Text` for Svg) instead of "x.y.z or later"; upgrading the core package alone can no longer end in a `MissingMethodException`. `RecursiveAI.OneImage.Web` keeps its range.

## [1.2.0] - 2026-10-22

### Added

- `RecursiveAI.OneImage.Text`, a separate package for fonts and text: `FontCollection` loads TrueType, OpenType/CFF, TrueType Collections and WOFF 1.0 (own inflate); `TextOptions` (origin, wrapping, line spacing, tab width, dpi, horizontal, vertical and text alignment, kerning), `TextMeasurer` (advance, ink bounds, lines, character boxes), `TextBuilder.GeneratePath` (text as a `Path`) and `DrawText` on `Image` and `Canvas` with a brush and/or pen. Outlines, advances, kerning, line counts and bounds match SixLabors.Fonts 2.1.3; rendering agrees with ImageSharp.Drawing 2.1.7 DrawText to coverage differences. See `docs/text.md`. A fuzz target `font` covers the parsers.
- OpenType shaping in `RecursiveAI.OneImage.Text`: GDEF glyph classes and mark filtering sets, GSUB lookup types 1-8 and GPOS types 1-9 with all lookup flags, so ligatures (`fi`, `ffl`), `ccmp` compositions, language forms (`locl`), contextual alternates, pair kerning and mark positioning on bases, ligatures and other marks work for Latin, Greek and Cyrillic. `TextOptions.Features` (`FontFeature.On("smcp")`, `Off("liga")`, alternates by value) and `TextOptions.Language` choose features and language forms; `KerningMode.None` is the `kern` feature off. Lookups are bounded in depth, work and growth, and damaged layout tables are ignored. Glyph ids and origins match SixLabors.Fonts 2.1.3 on Noto Sans; measuring and drawing a ligature-heavy paragraph is 8x and 19x faster. See `docs/text.md`.
- Unicode text segmentation in `RecursiveAI.OneImage.Text`: wrapping now ends lines at the break opportunities of the Unicode line breaking algorithm (UAX #14, Unicode 17.0: spaces, hyphens, soft hyphen, zero width space, no-break space and word joiner, quotation marks and brackets, numbers, East Asian text, Korean syllables, emoji sequences, flags), and never inside an extended grapheme cluster (UAX #29): `GetCharacterBounds` returns one box per cluster. New `TextOptions.WordBreaking` (`Standard`, `BreakAll`, `KeepAll`, `BreakWord`) and `TextOptions.TextJustification` (`None`, `InterWord`, `InterCharacter`), with ImageSharp's names and behavior. The property tables (72 KB) are generated from the Unicode Character Database by `tools/gen_unicode_tables.py`; `LineBreakTest.txt` (19,338 cases) and `GraphemeBreakTest.txt` (766 cases) pass in full. Ordinary text wraps at the same places as SixLabors.Fonts 2.1.3 (1,590 cases, none different), and justification matches it to 0.0001 px; text with break opportunities inside words differs, and `docs/text.md` lists why.
- Rich text, font fallback, text on a path and installed fonts in `RecursiveAI.OneImage.Text`. `TextOptions.TextRuns` (`TextRun`: its own font and size, `TextAttributes` super- and subscript, `TextDecorations` underline, strikeout and overline at the font's positions and thicknesses, brush, pen and per-decoration pens) works through layout, measuring, `TextBuilder.GeneratePath` and `DrawText`; lines mix sizes and faces. `TextOptions.FallbackFontFamilies` shapes characters a font lacks in the first fallback family that has them. `TextOptions.Path` lays text along a `Path`, rotating each glyph to the tangent, with open paths continued straight and closed ones wrapped. `SystemFonts` / `SystemFontSet` find installed fonts by scanning the standard directories of Windows, macOS and Linux, reading only name tables until a family is requested. Glyph ink boxes, advances, line counts and decoration segments match SixLabors.Fonts 2.1.3 to 0.01 px or better and text on a path matches ImageSharp.Drawing 2.1.7 to 0.001 px on lines; the differences (wrapping with fallback, nested runs, one reference kerning quirk, closed-path edges) are listed in `docs/text.md`. Run counts (16,384), fallback families (32), scan depth and file counts are bounded. Color glyphs (COLR v0) work inside runs and in fallback faces: a run's brush fills the foreground layers, layers with their own color keep it, and the pens draw last. New fuzz targets `richtext` and `sysfont`.

### Changed

- Strokes whose outline lies on itself along an edge (a path that doubles back, a closed two-point figure, a cusp) are drawn with the coverage of the union, not the sum: their edge pixels were up to twice as dense. The outline is merged only when such a turn (sharper than about 172 degrees) occurs, so other strokes are unchanged in output and time (stroke benchmark rows within noise).
- `Canvas.Fill(..., linear: true)` (and `Draw` with `linear: true`) no longer reads the canvas through a public snapshot: without an outside snapshot it swaps in fresh pooled tiles and returns the replaced ones, and it gives back the tiles its one-shot graph cached, which used to fill the shared tile cache (up to its 256 MiB budget) until the canvas was collected. A linear-light fill of a circle on a 2000 x 2000 canvas allocates 65 MB instead of 82 MB, with identical output. The color for a linear-light solid fill is now computed without leaving a pooled tile behind.
- Linear-light fills and strokes (`linear: true`) on 8-bit gray and RGB images and canvases without an embedded profile now decode, blend and encode only the covered pixels instead of converting whole tiles to float. On a 2000 x 2000 Rgba32 image a solid circle fill took 74 ms and allocated 52 MB, and on a canvas 242 ms and 65 MB; now both take about 2 ms (encoded: 1.9 and 1.3 ms) and allocate 24 KB. Gradient, Multiply and stroke fills are within 1.5x of their encoded cost. Output agrees with the whole-tile conversion path to 1 level at 8 bits (untouched pixels are now never rewritten). Other formats and images with an embedded profile keep the whole-tile path.
- `Canvas.Clip` no longer copies tiles through a public snapshot: it swaps in fresh pooled tiles and returns the replaced and cached ones to the pool. A blur clipped to a circle on a 2000 x 2000 canvas allocates 88 KB instead of 9.6 MB, at the same speed and with identical output.

- WOFF 2.0 fonts in `RecursiveAI.OneImage.Text`: `FontCollection.Add` and `AddCollection` read single fonts and collections, with the transformed `glyf`/`loca` and `hmtx` tables, on the package's own Brotli decoder (RFC 7932, safe managed code; the BCL decoder is native and is not used). Declared sizes and expansion are bounded before anything is allocated. Faces, outlines, advances, kerning and shaping equal the source fonts'. A fuzz target `brotli` joins `font`. See `docs/text.md`.
- Color glyphs in `RecursiveAI.OneImage.Text`: `TextOptions.ColorFontSupport` (`MicrosoftColrFormat` by default, as in SixLabors.Fonts; `None` turns it off) and `ColorPaletteIndex` make `DrawText` on `Image` and `Canvas` paint COLR version 0 layers with CPAL palette colors or the brush (palette index 0xFFFF); `Font.ColorPaletteCount` and `Font.HasColorGlyph`. Matches ImageSharp.Drawing 2.1.7 to a mean of 0.07 levels on a layered test font. COLR version 1 glyphs draw as their plain outline. Text as a path stays monochrome.

## [1.1.0] - 2026-10-22

### Added

- Vector drawing foundation (`OneImage.Drawing`): `Path`, `PathBuilder` (lines, quadratic and cubic Béziers, SVG elliptical arcs), shape factories (rectangle, rounded rectangle, ellipse, circle, polygon, regular polygon, star, combine), transforms, reverse, length, bounds, flattening, and strict bounded SVG path-data parsing (`Path.TryParseSvg`). `Image.Fill`, `Image.Clear`, `Canvas.Fill` and `Canvas.Clear` fill paths with a solid `Brush`: exact-area anti-aliasing (or none), non-zero and even-odd rules, blend modes, Porter-Duff operators, opacity, a transform, and linear-light blending. `Image` fills are lazy and a run of fills computes in one pass; `Canvas` fills draw in place in parallel with identical bytes. See `docs/drawing.md`. A fuzz target `svg` covers the path parser.
- Strokes: `Image.Draw` / `Canvas.Draw` with a `Pen` (any brush, width, `StrokeJoin` Miter/Round/Bevel/Square with a miter limit, `StrokeCap` Butt/Round/Square, dash patterns with offset, presets Dash/Dot/DashDot/DashDotDot) or a color and width; helpers `DrawLine`, `DrawPolyline`, `DrawPolygon`, `DrawBeziers`, `DrawRectangle`, `DrawEllipse`; and `Path.GenerateOutline` returning the stroke as a `Path`. Overlapping parts of one stroke are painted once, so translucent pens are even. 6 to 7 times faster than ImageSharp.Drawing on strokes with a fifth to a quarter of the allocation; see `docs/drawing.md`.
- Brushes for fills (`OneImage.Drawing.Brush`): linear, radial and elliptic gradients with color stops and the repetition modes none, repeat, reflect and dont-fill; a path gradient; pattern brushes with the hatch presets (horizontal, vertical, forward and backward diagonal, 10%, 20%, min); an image brush with region, offset, transform and tiling; and a recolor brush. Gradients interpolate premultiplied colors in encoded values by default and blend in linear light with `linear: true`, which now works with every brush. `Brush` is public to derive from: implement `FillRow` (and `ReadsBackdrop` for brushes that read the existing pixels). Opaque brush colors over fully covered pixels are written directly, through a vector kernel for 8-bit RGBA. Agrees with ImageSharp.Drawing 2.1.7 to rounding on every brush and mode; see `docs/drawing.md` for the differences.
- `Image.Composite(..., blend:, alpha:)`: all 24 Photoshop/W3C blend modes (`BlendMode`: Multiply, Screen, Overlay, Darken, Lighten, Color Dodge/Burn, Hard/Soft/Vivid/Linear/Pin Light, Hard Mix, Difference, Exclusion, Add, Subtract, Linear Burn, Divide, Hue, Saturation, Color, Luminosity) and the twelve Porter-Duff operators (`AlphaComposition`). Normal source-over output is unchanged.
- `Image.Perspective`: a projective (perspective) transform from a `ProjectiveMatrix`, a `ProjectiveTransformBuilder` (taper, rotation, scale, skew, translation, matrices, with `Append` and `Prepend` forms) or four corner points. `ProjectiveMatrix` also builds quad-to-quad maps.
- `Fit.BoxPad` and `Fit.Min` (ImageSharp's `ResizeMode.BoxPad` and `Min`), the aliases `Fit.Max` and `Fit.Crop`, and `Image.ResizeInto` for resizing into a rectangle of a canvas (`ResizeMode.Manual`).
- Resampling filters `Hermite`, `Lanczos5`, `Lanczos8`, `Robidoux`, `RobidouxSharp`, `Spline` and `Welch`, with the aliases `Bicubic` and `Triangle`, in `Resize` and in the rotate, affine and perspective samplers.
- Neighbourhood effects, matching ImageSharp 3.1.12: `Pixelate`, `OilPaint`, `Vignette`, `Glow`, `BokehBlur`, `GaussianSharpen`, `DetectEdges` (`EdgeDetector`: Sobel, Prewitt, Scharr, RobertsCross, Kayyali, Laplacian3x3/5x5, LaplacianOfGaussian, Kirsch, Robinson) and `EntropyCrop`. `OilPaint` centers its window, where ImageSharp's is shifted; see the migration guide.
- Color matrix: `Image.ApplyColorMatrix(ColorMatrix)` with a public `ColorMatrix` (ImageSharp's 5x4 layout), and the looks built on it: `Sepia`, `BlackWhite`, `Kodachrome`, `Lomograph`, `Polaroid`, `ColorBlindness(ColorBlindnessMode)` and `Opacity`. `Lomograph` and `Polaroid` include ImageSharp's vignette and glow; `ColorMatrix.Lomograph` and `.Polaroid` are the plain matrices. They fuse with the other color ops into one pass per tile, and a color chain directly on a decode now runs on rows as they are decoded when the whole frame is read at once.
- `Image.HistogramEqualization`: global, adaptive tile interpolation and adaptive sliding window equalization with histogram clipping, luminance levels and tile count, matching ImageSharp 3.1.12 within 1 level at 8 bits (`HistogramEqualizationMethod`). The luminance is mapped and written to all color channels, as in ImageSharp.
- `RecursiveAI.OneImage.Web`, a separate package with ASP.NET Core middleware for on-the-fly image processing (`AddOneImage()` / `UseOneImage()`): URL commands for size, fit, anchor and focal point, filter, background, format, quality and orientation, with ImageSharp.Web query names accepted too; presets and a presets-only mode; web root, folder, `IFileProvider` and custom providers; a disk cache with configurable layout, ETag and Last-Modified validators with 304 answers, source-change invalidation and single processing of concurrent identical requests; HMAC-signed URLs with a signer and a Razor tag helper; request hooks; and size, source and concurrency limits. See `docs/web.md`.
- Histograms, thresholds and dithering, matching ImageSharp 3.1.12: `Image.AutoLevel`, `Image.AdaptiveThreshold`, and a public `Image.Dither(palette, dither, scale, fastMatch)` (`fastMatch` defaults to true: ImageSharp's order-dependent cell matching, bit-identical to its output; `false` matches each pixel exactly) and `BinaryDither`. The `Dither` enum gains Atkinson, Burkes, JarvisJudiceNinke, Sierra2, Sierra3, SierraLite, StevensonArce, Stucki, Bayer2x2, Ordered3x3, Bayer4x4, Bayer8x8 and Bayer16x16 (they also work in `Quantize` and the GIF encoder). `Quantizer` (Wu, Octree, WebSafe, Werner) selects the palette builder in `Quantize` and `GifEncoderOptions.Quantizer`, and `KnownPalettes.WebSafe` and `KnownPalettes.Werner` are public. Existing GIF and indexed-PNG output is unchanged.
- Boolean operations on paths: `PathBoolean` (`path.Boolean(BooleanOperation, other, ...)`, `Union`, `Intersect`, `Subtract`, `Xor`, `UnionAll`, `Resolve`, `Offset`) with exact integer geometry on a 1/256 px grid, per-operand non-zero or even-odd rules, and polygon results without overlaps. `Image.Clip(path, operations)` / `Canvas.Clip` run any operations only inside a path with anti-aliased edges, and `Fill` / `Draw` take `clip:` to limit a shape to a clip path. A 200-circle union takes 7 ms against ImageSharp.Drawing's 11 ms with one seventh of the allocation, and a Gaussian blur clipped to a circle on 2000 x 2000 takes 12-13 ms against 56 ms; see `docs/drawing.md`.

## [1.0.0] - 2026-10-22

First release, published on NuGet as `RecursiveAI.OneImage` (namespace `OneImage`).

### Added

- Lazy, immutable `Image` and mutable tiled `Canvas` on one engine, with O(1) snapshots.
- Codecs: PNG (including APNG and indexed output), JPEG (baseline, progressive, arithmetic, 12-bit, CMYK, shrink-on-load), WebP (lossy, lossless, animation), GIF (animation, quantizing, dithering), TIFF (including BigTIFF, CCITT and multi-page), BMP and ICO.
- ICC color management (v2 and v4 profiles, built-in profiles, a generic CMYK profile), resampling in linear light, geometry, tone and filter operations, analysis, and normal-mode compositing.
- `Image.Thumbnail`, which applies EXIF orientation, converts to sRGB and strips EXIF, XMP and IPTC by default.
- Size limits on by default, checked from headers before allocation; typed exceptions; a fuzz target for every decoder and parser.
- Depends only on the .NET base class library; NativeAOT- and trim-compatible.
- Documentation: a guide, a migration guide from ImageSharp and System.Drawing, a comparison with other libraries, and a [performance page](https://github.com/recursive-ai-oss/oneimage-csharp/blob/v1.0.0/docs/performance.md) with the benchmark method and every loss.

[Unreleased]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.10.1...HEAD
[1.10.1]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.10.0...v1.10.1
[1.10.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.8.1...v1.9.0
[1.8.1]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.8.0...v1.8.1
[1.8.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.7.0...v1.8.0
[1.7.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/recursive-ai-oss/oneimage-csharp/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/recursive-ai-oss/oneimage-csharp/releases/tag/v1.0.0
