Skip to content

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 05:31
· 81 commits to main since this release

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.