You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.