Repository navigation
Releases: recursive-ai-oss/oneimage-csharp
Releases · recursive-ai-oss/oneimage-csharp
Release list
v1.10.2
Fixed
GaussianBlur,BoxBlurandSharpencould recurse without end and end the process with a stack overflow when the image was small compared with the blur: a Gaussian sigma above about 16 times an image side, or a box radius above about 48 times the longer side (for example a 1-pixel-wide image with sigma 20). The wide-blur approximation could not make such an axis any smaller, so it asked for the same blur again. Those axes are now blurred directly with the documented kernel and edge mode (exact up to 127 pixels, within 0.1 level at 8 bits beyond), and any finite sigma is accepted. Results for every blur that finished before are byte for byte the same. Layer mask feathering, which uses the Gaussian blur, is fixed by the same change.
v1.10.1
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
ArgumentOutOfRangeExceptionfrom the data reader. The same decoder reads TIFF Deflate strips and tiles, PSD ZIP channels, SVGZ, WOFF tables and PNGiCCP/zTXttext, 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.
iCCPprofiles 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)orLoadAsync(Stream)could read overwritten bytes if theImagebecame unreachable while the decode was still running (for exampleImage.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.
v1.10.0
Changed
RecursiveAI.OneImage.Textis built onRecursiveAI.OneFont1.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 ofOneImage.Textis 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.OneFontis the package's one new dependency;RecursiveAI.OneImageitself gains none.
Fixed
TextShaping.ShapeRunreturns 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.
v1.9.0
Added
ImageFrame.ReleaseDecodedPixels()is public, so one frame of a multi-frame image can give its decoded pixels back.CopyPixelsTo(destination, rect, useRegionCache: false)onImageandImageFrame: one region or band read that builds no region plane and counts toward none, whateverImageEngine.RegionCacheBytessays. 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 throughframe.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.
v1.8.1
Changed
- Reading an 8-bit RGB or RGBA image as
Gray8,Gray16or 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 asGray8now costs about the same as theRgb24read.
v1.8.0
Added
ImageEngine.RegionCacheBytes(default 64 MiB per frame, 0 = off) andImage.ReleaseDecodedPixels(): repeatedCopyPixelsToregion 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
RegionCacheBytesper frame, released byReleaseDecodedPixels()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. SetRegionCacheBytes = 0for the previous behaviour. Pixel values are unchanged. - The first
Image.Loadof 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.
v1.7.0
Added
PixelResizer: an allocation-free resize from a span to a span (Resize<TPixel>for the pixel structs,ResizeRawfor anyPixelFormatwith up to four channels, andPixelResizer.GetSizefor the sizeImage.Resizewould scale to). Output is byte for byte whatImage.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,maxScratchBytesandTrim()bound and release them. Calls with enough pixels split their output rows over up tomaxDegreethreads 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.0Image.Resize, and allocates nothing per call against 6 KB (same size) or 140 KB (a new width each call). See the guide.
Changed
Image.Resizeallocates 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.
v1.6.0
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 defaultImageConfiguration. 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).PdfRenderLimitsbound files, images, nesting, clips and pattern tiles, with a hostile-file test each, andfuzz/run.sh pdffuzzes 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/TRis 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 topdfimages, 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.GradientOverlayEffectandEffectGradient/EffectGradientFill(linear, radial, angle, reflected, diamond; midpoints, opacity stops, smoothness, reverse, scale, offset, align),PatternOverlayEffectwithEffectPattern/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 forStrokeEffect(and a shape burst gradient) and a gradient for the outer and inner glow.LayerStyle.AlignBoxsets the rectangle that aligned gradients and patterns are laid out on. The new effects have language-neutralDescribe()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 fromlfx2/lmfxand 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.Savewrites 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 asGdFl/PtFlsettings (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 makingSavethrow. Disabled effects are now read as disabledLayerEffects 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-peaktakes an optionalGATE_WARMUPS=Nto 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 ofMIAP/MIRP/MDRPand the other absolute values no longer overflow on the most negative integer, andISECTdivides in 128 bits. The twilight zone is now kept as the control value program leaves it (each glyph starts from a copy), so fonts whoseprepplaces 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 inEffectsNotDrawninstead of throwing an argument exception (found by fuzzing).
v1.5.0
Added
RecursiveAI.OneImage.Psd: gradient and pattern fill layers (and shape layers filled with them) are drawn from their settings asFillLayers 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), andgradient-fill.psdgoes 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) andImage.BrightnessContrast(brightness, contrast, linear, BrightnessContrastMode)withPhotoshopLegacy(exact) andPhotoshopSmooth(an approximation of the current mode).Adjustment.Posterize,CurvesandBrightnessContrasthave the same overloads. They fuse with other tone steps like the rest. OneImage.Layers: layer styles.Layer.Styletakes aLayerStyleof 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 optionalEffectContour. 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 intoLayer.Styleand drawn;PsdLimits.MaxEffectsPerLayer(32) bounds them, andPsdLayerInfo.EffectsNotDrawn/EffectsApproximatedcount 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 throwsImageOperationException. 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/JpegHashwrites 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 itsCgEddescriptor (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-peakno 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.
v1.4.0
Added
OneImage.Layers: a layered document that renders on demand.Documentholds pixel layers (aCanvasat 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, anyBlendMode, a raster mask (density, feather, outside value), clipping to the layer below and lock flags.Document.Render()is a lazyImage; a stack of layers renders to exactly the bytes of the equivalent chain ofCompositecalls.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, namespaceOneImage.Psd): Photoshop PSD and PSB.PsdFormatreads the merged image (gray, RGB, indexed, bitmap, CMYK, Lab; 8, 16 and 32 bits; raw, RLE and ZIP channels) as an ordinaryImageand writes flat documents;PsdFileloads the layer tree into anOneImage.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, apsdfuzz 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.Compositeof 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,Quantizewith a dither), the luma step ofHistogramEqualization, the 8-bit color-matrix kernel (ColorMatrix,Sepia, ...) and the float-path RGBACompositekernel 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.