Repository navigation
Conversation
- port overlay to rust & backport improvements to js
- port fill & flip - fix negative overlay
- port gaussian blur
- port rotate to rust - remove wasm simd - fix rotate overflow in js
TX_MODE_SELECT at effort 2: the best luma mode is also tried with transforms of depth 1 and 2 (each predicted from the previous ones' reconstruction). BD-rate at effort 2: photos -0.7%, readme -1.2%, svg -4%, ~+10% time. At effort 1 it gave only ~-0.5% for +10-17% time, so it stays off there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
PARTITION_H/V for 64x64 to 16x16 blocks, tried only where the whole block and its quarters cost within 0.9-1.3x of each other and at most one quarter splits further (where nearly all of their gains were, measured per case). Effort 1 tries the halves whose luma means differ more, effort 2 both. BD-rate: effort 1 photos -2.3..-2.5%, readme -1.9%, graphics -0.5..-0.9%, ~+20% time; effort 2 -1.9..-3.2%, ~+40% time. Trying both directions without the gate gave -3% for +80% time at effort 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
encodeAVIF() and encodeJXL() take metadata like the other encoders. AVIFs get an Exif item that describes the primary image (cdsc), JPEG XLs the container with an Exif box; the codestream is unchanged. The AVIF handler name is now empty, so software: '' leaves ImageScript out of the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
The rate-distortion score only grows with each term, so a mode whose header bits (intra 4x4) or distortion (intra 16x16, chroma) already reach the best score is dropped before the reconstruction, texture distortion or rate is computed. Quantized intra 4x4 blocks without levels are the prediction, without an inverse transform; tTransform keeps its sums in locals. 23-28% faster at effort 3-6, output unchanged (140 hashes over 5 images x efforts 0-6 x 4 qualities, cwebp tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
The paeth filter shrinks photos by about a third, but graphics often compress better unfiltered, since repeated shapes stay the same bytes: the benchmark's graphic was 20% larger paeth filtered (25% at level 6), potato.png 36-42%. 32 rows of every 512 are compressed both ways at the target level and the smaller wins (images of at most 32 rows: the smaller whole). Graphics get 17-29% smaller and faster, photos are unchanged and take about 8% longer. encode() and the APNG frames share compressRows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
Effort 3 and up add the full rate-distortion search of the intra 4x4 modes: on a 1920x1080 photo at quality 75, effort 2 takes ~245 ms for 101.6 kB (38.87 dB), effort 4 ~905 ms for 96.6 kB (39.32 dB). The default becomes 2 for images and animations; effort 4 still encodes like cwebp's default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
3.6.0 wasn't installable 2.5 minutes after publishing, so the bench job failed until it was rerun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
encodeJXL({ quality }) encodes lossy JPEG XLs, quality mapping to the
Butteraugli distance like cjxl -q; without it images stay lossless. The
encoder follows libjxl's effort 3: XYB, 8x8 DCTs, the constant
quantization field of the distance with libjxl's global scale, LF
quantizer and dead zones, default matrices, block contexts and chroma
from luma, EPF iterations by distance. LF coefficients use the weighted
predictor with libjxl's fixed tree per channel, HF metadata the left
predictor, alpha (lossless) the gradient, in the global stream or the
pass groups.
The new entropy encoder (entropy-enc.ts) clusters contexts, writes the
context map and codes tokens with ANS (libjxl's count normalization and
histogram format; the alias table comes from the decoder's) or prefix
codes. Bit writer and image header moved out of fast-lossless.ts, the
lossless output is unchanged.
1920x1080 photo at d1: 240.8 kB at the PSNR of cjxl -e 3's 240.2 kB,
0.9 s. BD-rate vs cjxl -e 3: +0.8 to +2.9% (photos, screenshots); vs
-e 7: -1 to +10% on photos, +31-42% on screenshots. Every size, distance
and alpha case tried decodes in djxl within 1 of our decoder.
Also exports AVIFEncodeOptions, which index.ts was missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
encodeJXL({ quality, effort }): effort 1 (the default) chooses the
varblocks like cjxl -e 5 (libjxl's ProcessRectACS: the best 8x8
transform of each block, merged up to 64x64 where the estimated cost is
lower, with the masking of libjxl's initial quantization field), 2 also
tries the 4x8 and AFV transforms, 0 keeps 8x8 DCTs. Effort 1 and 2 also
compute chroma from luma per 64x64 tile (libjxl's fast least squares).
Forward transforms for every varblock type (transforms-enc.ts): the
decoder's DCTs forward, unrolled up to 64 points (scripts/jxl-fdct.py,
and idct64 for the decoder), libjxl's DCT2X2, DCT4X4 and IDENTITY, the
other 8x8 ones by inverting their inverse as a sparse matrix; all
roundtrip within 1e-7. Quantization, LF and token writing handle any
varblock (its order, contexts, non-zero prediction).
Measured on 4 images x 5 distances (BD-rate by SSIMULACRA2 / PSNR /
butteraugli): effort 1 vs cjxl -e 3 -5.6 / -6.0 / -11.4%, vs -e 5
-1.5 / +0.9 / +4.4%; effort 2 vs -e 5 -3.6%, vs -e 7 +1.3% (SSIMULACRA2).
libjxl's adaptive quantization of the HF, AdjustQuantBlockAC and
Gaborish were tried and made files larger at equal SSIMULACRA2.
1920x1080 photo: ~1 s at effort 0, ~5 s at 1, ~10 s at 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
The SATD keeps its 4x4 Hadamard transform in locals; the transform type search of a block of one transform predicts once and restores the prediction for each type (both output-identical over 46 configurations); 16x16 blocks whose coefficients take fewer than 30 bits aren't split at effort 1 (BD -0.05%). CPU time at effort 1 -8% on 4 images x 5 qualities. Tried and dropped: comparing the transform types by their error in the transform domain (no faster), fewer CDEF strengths or loop filter levels, one chroma candidate (each 5-10% faster for 0.2-0.5% larger files). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
Frames shown as they are (replacing the canvas, not referenced, straight alpha, no black or spot channels) go from XYB to interleaved sRGB bytes in one pass: linear light to bytes by 255 thresholds found by bisection of byte(fround(srgbEncode(v))), which only grows with v, so the bytes are exactly those of before. Gaborish carries its 3x3 window along the row (the same order of operations); the edge preserving filter's passes of 2 tap pairs have an unrolled path for inner blocks. 2048x1536 d1: 0.87 -> 0.73 s, 4000x3000 d1: 3.1 -> 2.6 s; the 49 conformance and bench files decode to identical pixels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
The VP8L encoder (lossless WebPs and the alpha of lossy ones) now - groups the main image's 16x16 blocks by their symbols (k-means by the bits their clusters' codes would take, 4-16 groups, from the blocks farthest apart) and writes an entropy image with a prefix group each, if that is estimated to be smaller; - picks the color cache's size (0-10 bits) by the estimated bits of the literals and hits, all sizes in one pass; - delays a match by a pixel if the next one starts a longer one; - tries predicting images of 17-256 colors instead of indexing them, and doesn't subtract green from images without red and blue (alpha planes). Sizes vs cwebp: lossless 113.7% -> 107.2% (5 images), lossy alpha at effort 2 113% -> 94%, at effort 4 134% -> 111%; every file decodes exactly in dwebp. Lossless encoding takes about 65% longer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
A prefix code with one used symbol that can't be a simple code (a length code, >= 256) is written as a normal code with one non-zero length. Decoders read such a code with zero bits per symbol, the encoder wrote one, so every following bit was off by one. Only prefix groups (since 3.7.0) produce such histograms. It broke lossless WebPs and the alpha of lossy ones, which most optimized animations have. Fixes #82 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
…ocks Optimized lossy frames made each exactly unchanged pixel transparent. Where most pixels change a little (cross-fades), only scattered pixels stay the same: the alpha masks were speckled and could cost more than the color data they saved, so optimized files got larger than unoptimized ones (reported: 8.9 MB instead of 7.0 MB). Like libwebp's WebPAnimEncoder (QualityToMaxDiff, FlattenSimilarBlocks), lossy frames now - count pixels whose channels changed by up to a tolerance from the quality (3 at quality 85, 1 at 100) as unchanged, for the frame rectangle too (compared with what's shown, so errors don't add up); - make unchanged pixels transparent only in whole 8x8 blocks (colored like the average of what's shown), and are sparse whenever a block is. Six cross-faded radar-like animations at quality 85: 2% larger to 13% smaller than before, 7-46% smaller than unoptimized, PSNR 0.1 dB below unoptimized (before: 0.8-1 dB). A layer whose color changes by 1-2 per frame: 16 kB instead of 158 kB (unoptimized: 246 kB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
Normal mode with an opacity went through __blend__, per channel in floats with the blend function called for each. It is source over with the source alpha scaled by the opacity: the fast path's formula. The fast path also indexes by offsets incremented per pixel and copies bytes instead of 32-bit words, which was no faster. A 1100x680 layer (two thirds at alpha 150) onto an opaque image, median of 100 runs: opacity 0.85 12.6 -> 7.6 ms, opacity 1 9.4 -> 7.6 ms; an opaque layer 4.4 -> 4.2 ms. The results are byte-identical to __blend__ on random pixels at random opacities; at exact halves (opacity 0.5 of opaque pixels) they now round like the fast path did, by 1 at most. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
Like crop on a clone, without copying the whole image first (it was done as new Image(w, h).composite(source, -x, -y)). Clipped like crop, whose bounds it shares; on a frame it gives a plain image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
The feature list read like a changelog (bit-exactness, sub-format details, maxPixels, ports): the formats are a table now, the functions a short list linking the documentation, the credits one paragraph (the FreeType notice stays, its license asks for it; the full notices are in THIRD_PARTY_NOTICES.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
The README example (tests/readme.js) draws a frosted glass weather card over an SVG landscape: renderSVG with gradients, region and blur for the glass, a blurred shadow, composite with an opacity, roundCorners, text in Open Sans and Source Sans 3 with Arabic, Devanagari and color emoji fallbacks, and a chart of gradient bars. It replaces the profile card with the photo and the badge SVGs only it used. The old image stays as tests/targets/profile-card.png: the lossless JPEG XL test compares it with cjxl's output and the fixture scripts crop it. averageColor checks the new image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
tests/targets/external.png (and external.jpg) are now a square of bench/inputs/photo.jpg (Carlos Varela, CC BY 2.0, in the third party notices), scaled to 638x638; profile-card.png, the old README image, is replaced by screen.png, a settings window drawn with ImageScript (flat areas, text, rounded corners). Everything made from them is written again, by the fixture scripts and the tools they name: - tests/avif: aomenc 3.8.2, references by libavif 1.4.2 (no libyuv, as avifdec -u bilinear; reproduces every reference of the old files) - tests/jxl: cjxl/djxl 0.11.1 (the parts of the photo and the screen; the circle, patches, animations and conformance files are unchanged) - tests/ico, tests/hdr, tests/qoi: scripts/format-fixtures.py - tests/tiffs: scripts/tiff-fixtures.py, which now also writes the TIFFs Pillow writes (the photo scaled to 157x101 bicubic, as before). i16b stores v * 257: its high bytes, which ImageScript reads, were 0 - references: cwebp 1.6.0, cjxl -e 1 with AVX2 and without SIMD (qemu) - targets: fisheye, flip, effects (OVERWRITE_TEST) Every tool was checked against the old files first: they reproduce the old references. Two thresholds depend on the photo: monochrome AVIF is 13% smaller instead of 30% (the photo has little color), and JPEG quality 10 reaches 27.7 dB (as libjpeg does) instead of 28. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
renderSVG paints with <pattern>: tiles in user space or relative to the bounding box (patternUnits), content in either (patternContentUnits), viewBox and preserveAspectRatio, patternTransform (ignored if it can't be inverted), attributes and children through href chains. Like resvg, the tile is rendered once in device resolution (its size rounded, at most 4M pixels) and repeated: bicubic (Mitchell, B = C = 1/3), or the nearest pixel where it's only translated. Patterns painting their own content paint nothing, decided up front in document order like usvg's fix_recursive_patterns. resvg's pattern tests: 32/33 pass (was 4; the other needs text), copied to tests/svgs/resvg. Marker and image tests unchanged; filters 81 -> 71, all of them invalid filters on pattern-filled shapes that resvg doesn't render (they passed only because the pattern was missing; filters are still ignored). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
The pattern rendering follows resvg's (the tile, its filtering) and usvg's fix_recursive_patterns; tests/svgs/resvg holds resvg's test cases. svg.ts's header moves above the imports, so the build keeps it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
renderSVG draws marker-start/mid/end (inherited, and the marker shorthand in CSS only, like resvg): orient (any angle unit, auto, auto-start-reverse), markerUnits, refX/refY, markerWidth/markerHeight, viewBox, clipped unless overflow is visible. Placement and orientation are ported from usvg's marker.rs, on the segments as tiny-skia's path builder makes them (repeated moves/closes merged, a move after a close). Arcs now split into as many cubics as kurbo makes for usvg (tolerance 0.1), so mid markers sit at the same vertices. resvg's marker tests: 61/64 (was 11; the others have text or images), copied to tests/svgs/resvg. Patterns 32/33, filters 71 unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
renderSVG draws <image> with data URIs: rasters are decoded first (the decoders may be asynchronous; GIFs' first frame, the rest through Image.decode), then passed to the synchronous renderer by href; SVGs (gzipped too, via a new synchronous gunzip) render as nested documents. Fitted like usvg: missing or auto width/height from the image, preserveAspectRatio (clipped with slice), image-rendering; sampled like tiny-skia (bicubic, nearest where only translated, padded edges), sharing the pattern's sampler. An image's bounding box is its rectangle for objectBoundingBox clips and masks. External files and URLs aren't loaded. Nested SVG elements count towards MAX_INSTANCES (shared budget), each href is collected once: 7 levels of 20 gzipped self-copies (1.28e9 elements) stop after 1.3 s. resvg's image tests: 17/56 by the suite's threshold (was 0), 37/56 with the frame's pixels ignored (tiny-skia rounds edges to quarter pixels, the frame's 3.75 px strokes differ by more than 24); most others load files. Patterns 32/33, markers 61/64, filters 71 unchanged; a test checks PNG, WebP, GIF, JPEG and SVGZ data URIs pixel-exactly at 1:1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.