Recursive AI

Changelog

All oneimage docs

All notable changes are listed here. The format follows Keep a Changelog and versions follow Semantic Versioning.

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 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.
  • 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.
  • 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 LayerEffects instead of being left out of the style. See docs/psd.md.

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). 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 FillLayers 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.
  • 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.
  • 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.

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.
  • 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.
  • 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.
  • 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.

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.

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.
  • 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.

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). 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.
  • 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 with the benchmark method and every loss.