Skip to content

Releases: recursive-ai-oss/oneimage-csharp

v1.10.2

Choose a tag to compare

@github-actions github-actions released this 27 Oct 04:10

Fixed

  • GaussianBlur, BoxBlur and Sharpen could 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

Choose a tag to compare

@github-actions github-actions released this 25 Oct 01:48

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.

v1.10.0

Choose a tag to compare

@github-actions github-actions released this 24 Oct 16:59

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.

v1.9.0

Choose a tag to compare

@github-actions github-actions released this 24 Oct 09:34

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.

v1.8.1

Choose a tag to compare

@github-actions github-actions released this 24 Oct 06:08

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.

v1.8.0

Choose a tag to compare

@github-actions github-actions released this 24 Oct 05:05

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.

v1.7.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 19:43

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.

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 15:41

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

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 09:43

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.

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 05:31

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.