Skip to content

fix(spf): apply the origin and pitch rules to the writer and the view - #6

Merged
Caeldeth merged 1 commit into
mainfrom
fix/spf-colorized-write-and-view
Jul 23, 2026
Merged

Caeldeth merged 1 commit into
mainfrom
fix/spf-colorized-write-and-view

Conversation

@Caeldeth

Copy link
Copy Markdown
Collaborator

Fixes findings 1–3 from the review of #2.

The 741 reconciliation corrected SpfFile.readColorized — bounds give the size, rows advance by the frame's own pitch — but left two other places on the old convention, where the size comes from right/bottom and rows advance by the row width. The reader and its writer, and the eager and lazy parsers, disagreed from the moment #2 landed.

What was broken

1. writeColorized indexed colorData with right/bottom. The reader sizes colorData as (right−left) × (bottom−top); the writer looped y < frame.bottom, x < frame.right. For a frame with a non-zero origin that runs off the end of the array:

TypeError: Cannot read properties of undefined (reading 'r')

Serializing a frame the reader had just produced threw. 190 of 982 frames in the client have a non-zero origin.

2. writeColorized recorded a byte count it did not write. startAddress advanced by the source frame's byteCount — which includes row padding — while the payload written is tightly packed. Every frame after the first pointed into the wrong place, silently, with no error. 78 of 982 frames have a pitch that differs from the width.

3. SpfView.get was never updated. The colorized branch still read t.bottom * t.right pixels sequentially, ignoring byteWidth:

RangeError: Offset is outside the bounds of the DataView

Where it didn't throw, it returned different pixels than SpfFile for the same bytes — on exactly the frames #2 set out to fix.

Fixes

  • writeColorized sizes from the bounds, like the reader, and normalizes byteWidth/byteCount/imageByteCount to describe the bytes it actually emits.
  • SpfView.get mirrors SpfFile.readColorized exactly: bounds for size, byteWidth for stride, transparent past the end of the payload.

Why the existing tests missed all of it

Every colorized fixture in the suite is built by fromColorizedRgbaFrames, which always produces left = top = 0 and byteWidth = width × 2. That is precisely the case where the correct and the incorrect conventions agree. The parity test added in #4 — "slices colors that match the eager parser" — therefore could not fail, no matter how wrong SpfView was. Line coverage was already 99.5%: every one of these lines executed, just never with a non-zero origin.

The 8 new cases use hand-built fixtures with left=2, top=1, right=6, bottom=4 and 4 bytes of row padding, with a distinct colour per row so a pitch error decodes visibly wrong rather than merely differently.

All 8 fail against the previous code (verified by stashing the source fixes and re-running):

× serializes a frame it just read, without running off the end
× round-trips an offset padded frame back to the same pixels
× normalizes the pitch and byte counts to the bytes it writes
× keeps every frame addressable when several are written
× reads the same pixels as the eager parser
× sizes the frame from the bounds and honours the pitch
  → TypeError: Cannot read properties of undefined (reading 'r')
  → RangeError: Offset is outside the bounds of the DataView

Verification

Type check, build, and the full suite in both modes: 537 tests without a client, 539 with. Coverage holds at 99.52% lines / 93.28% branches / 99.74% functions / 99.15% statements.

Note on ordering

This branches off main, so it does not include #5. Both touch CHANGELOG.md under [Unreleased]; whichever merges second may need a trivial rebase.

Remaining review findings not addressed here (4–10): the MetaFile EUC-KR read/write asymmetry, SotpFile aliasing the archive buffer, the semver/breaking-change classification, eager LftFile allocation, renderTile double-pass, and the measureLftText signature.

🤖 Generated with Claude Code

The 741 reconciliation corrected SpfFile.readColorized but left two other
places on the old convention, where the size comes from right and bottom and
rows advance by the row width.

- writeColorized indexed colorData as `y * frame.right + x` for `y <
  frame.bottom`. For a frame with a non-zero origin that runs past the end of
  the array, and serializing a frame that had just been read threw
  "TypeError: Cannot read properties of undefined". 190 of 982 frames in the
  client have a non-zero origin.
- writeColorized advanced startAddress by the source frame's byteCount, which
  includes row padding, while the payload it writes is tightly packed. Every
  frame after the first pointed into the wrong place. The writer now sets
  byteWidth, byteCount and imageByteCount to describe what it emits.
- SpfView.get read `t.bottom * t.right` pixels and ignored the pitch. It threw
  "RangeError: Offset is outside the bounds of the DataView", or returned
  different pixels than SpfFile for the same bytes.

Tests: 8 cases with a non-zero origin and a padded pitch, in spfFile and
virtualizedViews. All 8 fail against the previous code. The existing parity
test could not catch any of this, because fromColorizedRgbaFrames always
produces left = top = 0 and byteWidth = width * 2, which is where the correct
and the incorrect conventions agree.

Co-Authored-By: Fintan (Claude Opus) <noreply@anthropic.com>
@Caeldeth
Caeldeth merged commit 5c2a06b into main Jul 23, 2026
1 check passed
@Caeldeth
Caeldeth deleted the fix/spf-colorized-write-and-view branch July 23, 2026 17:15
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.

1 participant