Skip to content

fix(resize): stop corrupting the caller's image, and read offset views correctly - #109

Open
ckellet wants to merge 1 commit into
jamsinclair:mainfrom
ckellet:up/resize-correctness
Open

ckellet wants to merge 1 commit into
jamsinclair:mainfrom
ckellet:up/resize-correctness

Conversation

@ckellet

@ckellet ckellet commented Aug 10, 2026

Copy link
Copy Markdown

Three defects in @jsquash/resize, all reachable from the public API and all live in the published package.

crop() writes through to the caller's ImageData

crop() copies within the caller's own buffer. The comment describes it as a speed and memory optimisation, but inputPixels is a Uint32Array view onto the caller's ImageData, so copyWithin writes straight through it. Any resize with fitMethod: 'contain' destroys the image it was given. The function then slices a copy out anyway, so the aliasing bought nothing.

Reproducible on a 4×4 gradient — the caller's own pixels afterwards:

before crop: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
after  crop: 5,6,9,10,4,5,6,7,8,9,10,11,12,13,14,15

contain + hqx crops the wrong region

Contain fit computes its offsets from the original dimensions but crops input, which the hqx branch may already have upscaled by 2–4×. Offsets now come from the image actually being cropped, and are clamped so rounding cannot exceed its bounds.

byteOffset is ignored in three places

new Uint8Array(data.data.buffer) discards byteOffset/byteLength. An ImageData whose data is a view into a larger allocation — tiling, a pooled buffer, anything sliced out of a bigger read — silently resizes the wrong bytes. Uint32Array views additionally require 4-byte alignment, so an unaligned source is copied rather than mis-viewed.

Also

The resize module is no longer instantiated for magic-kernel-only calls, which never touch it.

Adds regression tests for the mutation and the offset view. Depends on #108 for green CI.

…s correctly

Three defects in the resize path, all reachable from the public API.

crop() copies within the caller's own buffer. The comment describes it as
a speed and memory optimisation, but `inputPixels` is a Uint32Array view
onto the caller's ImageData, so copyWithin writes straight through it -
any resize with `fitMethod: 'contain'` destroys the image it was given.
The function then slices a copy out anyway, so the aliasing bought
nothing. Reproducible on a 4x4 gradient: the caller's first four pixels
come back holding the cropped region.

Contain fit computes its offsets from the original dimensions but crops
`input`, which the hqx branch may already have upscaled by 2-4x, so
contain + hqx crops the wrong region. Offsets now come from the image
actually being cropped, and are clamped so rounding cannot exceed it.

`new Uint8Array(data.data.buffer)` ignores byteOffset and byteLength in
three places. An ImageData whose `data` is a view into a larger
allocation - tiling, a pooled buffer, anything sliced from a bigger read -
silently resizes the wrong bytes. Uint32Array views additionally require
4-byte alignment, so an unaligned source is copied rather than mis-viewed.

Also: the resize module is no longer instantiated for magic-kernel-only
calls, which never touch it.

Adds regression tests for the mutation and the offset view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nRtc2GmmpARzSXa2y7ABs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants