Skip to content

fix(core): read WebP VP8X canvas height from the correct byte offset - #5194

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
he-yufeng:fix/webp-vp8x-height-offset
Jun 18, 2026
Merged

fix(core): read WebP VP8X canvas height from the correct byte offset#5194
wenshao merged 2 commits into
QwenLM:mainfrom
he-yufeng:fix/webp-vp8x-height-offset

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

What this PR does

Fixes the canvas height read for WebP images in the extended (VP8X) format. The height was being read starting one byte too early, so the parser returned a garbage height for any VP8X image.

Why it's needed

In a VP8X chunk the canvas dimensions are two 24-bit little-endian values: width-minus-one at byte 24, height-minus-one at byte 27. The code read width correctly with readUInt32LE(24) & 0xffffff but read height from offset 26 (readUInt32LE(26) & 0xffffff), which spans bytes 26-28 instead of 27-29. So width comes out right and height is wrong.

I switched both lines to readUIntLE(offset, 3), which reads exactly the 3 bytes we want. This also avoids a subtler trap: a naive readUInt32LE(27) would touch byte 30 and throw a RangeError on a minimal 30-byte VP8X header, which is still allowed by the existing buffer.length < 30 guard. readUIntLE(27, 3) stays in bounds.

Reviewer Test Plan

How to verify

Added a unit test that builds a minimal 30-byte VP8X header for a 100x80 canvas and asserts the parsed dimensions.

npx vitest run packages/core/src/utils/request-tokenizer/imageTokenizer.test.ts

Before the fix the new test fails with expected 20225 to be 80 (width 100 is already correct). After the fix all 10 tests pass.

Evidence (Before & After)

N/A (not user-visible; covered by the unit test output above).

Tested on

OS Status
🍏 macOS N/A
🪟 Windows
🐧 Linux N/A

Environment (optional)

Unit tests only.

Risk & Scope

  • Main risk or tradeoff: none meaningful; the change is a 2-line offset correction with identical behavior for width.
  • Not validated / out of scope: real-world VP8X files with extension chunks beyond the 30-byte header (header layout is fixed, so this does not affect the dimension read).
  • Breaking changes / migration notes: none.

Linked Issues

None.

中文说明

本 PR 修正 WebP 扩展格式(VP8X)画布高度的读取偏移。VP8X chunk 里画布尺寸是两个 24 位小端值:width-1 在第 24 字节,height-1 在第 27 字节。原代码用 readUInt32LE(24) & 0xffffff 正确读出宽度,但高度从偏移 26 读(readUInt32LE(26) & 0xffffff),覆盖的是第 26-28 字节而非 27-29,导致宽度正确、高度错误。

改为两行都用 readUIntLE(offset, 3),正好读取需要的 3 个字节。这也避开了一个隐患:直接写 readUInt32LE(27) 会触及第 30 字节,在仅 30 字节的最小 VP8X 头上抛 RangeError,而这种长度仍被现有的 buffer.length < 30 守卫放行;readUIntLE(27, 3) 不会越界。

新增单元测试构造一个 100x80 画布的最小 30 字节 VP8X 头并断言解析结果。修复前该测试报 expected 20225 to be 80(宽度 100 本就正确),修复后全部 10 个测试通过。

npx vitest run packages/core/src/utils/request-tokenizer/imageTokenizer.test.ts

const width = (buffer.readUInt32LE(24) & 0xffffff) + 1;
const height = (buffer.readUInt32LE(26) & 0xffffff) + 1;
const width = buffer.readUIntLE(24, 3) + 1;
const height = buffer.readUIntLE(27, 3) + 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The byte offsets 24 and 27 are undocumented magic numbers. The surrounding codebase has an established convention: extractPngDimensions documents "Width/height at bytes 16-19 and 20-23", and extractJpegDimensions documents "Dimensions at offset +5 (height) and +7 (width)". The VP8X branch is the only format parser without such documentation.

The bug this PR fixes was precisely caused by a wrong offset (26 instead of 27) — the absence of a spec-reference comment makes this code fragile against future edits.

Suggested change
const height = buffer.readUIntLE(27, 3) + 1;
} else if (format === 'VP8X') {
// VP8X extended format: canvas dimensions are 24-bit LE values
// stored as (dimension - 1). Width at bytes 24-26, height at 27-29.
const width = buffer.readUIntLE(24, 3) + 1;
const height = buffer.readUIntLE(27, 3) + 1;
return { width, height };

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — fix confirmed correct, test has teeth

I ran a real local verification of this PR in a tmux PTY on Linux (Node 22.22.2, vitest 3.2.4), complementing the author's Windows testing. The bug, the fix, and the new test's effectiveness all check out. Recommend merge.

Root cause (byte layout confirmed)

In a VP8X chunk the canvas dimensions are two 24-bit little-endian values: width-1 at bytes 24–26, height-1 at bytes 27–29.

Field Correct bytes Old code Bytes it actually read New code Bytes read
width 24–26 readUInt32LE(24) & 0xffffff 24,25,26 ✅ readUIntLE(24, 3) 24,25,26 ✅
height 27–29 readUInt32LE(26) & 0xffffff 26,27,28 ❌ readUIntLE(27, 3) 27,28,29 ✅

The old height read started one byte early: it took byte 26 (the high byte of width) as its LSB and dropped byte 29. Width was unaffected, so the symptom was correct width + garbage height.

Verification matrix

# Scenario Result
A Fixed code — PR's 10 tests + 2 tests against real Pillow-encoded VP8X files 12/12 pass ✅
B Teeth check — revert only the 2 source lines, keep all tests 3 fail exactly as predicted ✅
C PR's exact stated command on fixed code 10/10 pass ✅
D Secondary claim — readUInt32LE(27) bounds trap on a 30-byte header Confirmed ✅

Evidence

A — fixed code, including real VP8X files (not just the synthetic header in the unit test; generated with Pillow 12.1.1 as RGBA-lossy WebP, which file(1) independently confirms as Web/P image, with alpha, 320+1x122+1 etc.):

 ✓ imageTokenizer.realvp8x.verify.test.ts (2 tests)   # real 321x123 & 640x16 read correctly
 ✓ imageTokenizer.test.ts (10 tests)
 Test Files  2 passed (2)   Tests  12 passed (12)

B — revert the source fix only, re-run the same tests. Every failure is height-only with the predicted wrong value; width is always correct:

× should extract canvas dimensions from VP8X
  → expected 20225 to be 80                                  # PR's synthetic test — matches PR description exactly
× reads 321x123 from a real Pillow-encoded VP8X file
  → expected { width: 321, height: 31233 } to deeply equal { width: 321, height: 123 }
× reads 640x16 from a real Pillow-encoded VP8X file
  → expected { width: 640, height: 3841 }  to deeply equal { width: 640, height: 16 }

This proves the new unit test genuinely catches the bug (it is not a tautological/always-green test), and the bug is real on actual encoder output, not only a hand-built header.

D — why readUIntLE and not readUInt32LE(27). On a minimal 30-byte VP8X header (still allowed by the existing buffer.length < 30 guard):

readUInt32LE(27) THREW: RangeError - offset must be >= 0 and <= 26
readUIntLE(27, 3)  = 79 -> height = 80   (in bounds, correct)

Because extractImageMetadata wraps everything in try/catch and falls back to 512×512, a naive readUInt32LE(27) fix would have silently returned 512×512 on minimal headers. readUIntLE(27, 3) is the right call — it fixes the offset and avoids that bounds trap.

Impact

extractWebpDimensions feeds image-token estimation. VP8X is the container WebP uses whenever an image has alpha, animation, or metadata — common in practice. Before this fix, every such image got a grossly inflated height (e.g. 31233 instead of 123, ~254× too large), corrupting token/budget accounting for that image. Low-risk, correct, well-covered fix.

中文说明(点击展开)

✅ 本地验证 —— 修复正确,且新增测试确实能抓到 Bug

我在 Linux(Node 22.22.2、vitest 3.2.4) 上用真实 tmux PTY 对本 PR 做了本地验证,与作者的 Windows 测试形成互补。Bug、修复、以及新测试的有效性全部得到确认。建议合并。

根因(字节布局已确认)

VP8X chunk 中画布尺寸是两个 24 位小端值:width-1 在第 24–26 字节,height-1 在第 27–29 字节。

字段 正确字节 旧代码 实际读取字节 新代码 读取字节
宽度 24–26 readUInt32LE(24) & 0xffffff 24,25,26 ✅ readUIntLE(24, 3) 24,25,26 ✅
高度 27–29 readUInt32LE(26) & 0xffffff 26,27,28 ❌ readUIntLE(27, 3) 27,28,29 ✅

旧的高度读取早了一个字节:把第 26 字节(宽度的高位字节)当成了最低位,并丢掉了第 29 字节。宽度不受影响,所以表现为宽度正确、高度乱码

验证矩阵

# 场景 结果
A 修复后代码 —— PR 的 10 个测试 + 针对 2 个真实 Pillow 编码 VP8X 文件的测试 12/12 通过 ✅
B 有效性检查 —— 只回退那 2 行源码、保留全部测试 3 个失败,与预测完全一致 ✅
C 在修复后代码上运行 PR 给出的原始命令 10/10 通过 ✅
D 次要论点 —— readUInt32LE(27) 在 30 字节头上的越界陷阱 已确认 ✅

证据

A —— 修复后代码,包含真实 VP8X 文件(不只是单测里手工构造的头;用 Pillow 12.1.1 生成的 RGBA 有损 WebP,file(1) 独立确认为 Web/P image, with alpha, 320+1x122+1 等):

 ✓ imageTokenizer.realvp8x.verify.test.ts (2 tests)   # 真实 321x123、640x16 读取正确
 ✓ imageTokenizer.test.ts (10 tests)
 Test Files  2 passed (2)   Tests  12 passed (12)

B —— 仅回退源码修复,重跑同一批测试。 每个失败都只发生在高度上、且值与预测一致;宽度始终正确:

× should extract canvas dimensions from VP8X
  → expected 20225 to be 80                                  # PR 的合成测试 —— 与 PR 描述完全吻合
× reads 321x123 from a real Pillow-encoded VP8X file
  → expected { width: 321, height: 31233 } ...
× reads 640x16 from a real Pillow-encoded VP8X file
  → expected { width: 640, height: 3841 } ...

这证明新增单测确实能抓到该 Bug(不是恒为绿的无效测试),且该 Bug 在真实编码器输出上同样存在,而非仅限于手工构造的头。

D —— 为何用 readUIntLE 而非 readUInt32LE(27) 在仅 30 字节的最小 VP8X 头上(仍被现有 buffer.length < 30 守卫放行):

readUInt32LE(27) 抛出 RangeError —— offset 必须 >= 0 且 <= 26
readUIntLE(27, 3)  = 79 -> 高度 = 80   (未越界,正确)

由于 extractImageMetadatatry/catch 包裹并回退到 512×512,直接写 readUInt32LE(27) 的修复会在最小头上静默返回 512×512readUIntLE(27, 3) 才是正确选择 —— 既修正了偏移,又避开了越界陷阱。

影响

extractWebpDimensions 为图像 token 估算提供输入。WebP 只要带 alpha、动画或元数据就会使用 VP8X 容器,实际中很常见。修复前,这类图像的高度被严重放大(例如 31233 而非 123,约 254 倍),从而破坏该图像的 token/预算计算。本修复风险低、正确、覆盖充分。


Verification performed locally in tmux on Linux; the temporary real-file test was removed afterward and the working tree left identical to the PR's committed state.

Address review: the VP8/VP8L/VP8X branches read width and height from bare
numeric offsets. Add a short comment per branch noting the byte positions and
little-endian layout, matching the convention already used by
extractPngDimensions/extractJpegDimensions/extractBmpDimensions.
@he-yufeng

Copy link
Copy Markdown
Contributor Author

Good call — added a short comment to each branch (VP8/VP8L/VP8X) spelling out the byte offsets and little-endian layout, matching the style already used in extractPngDimensions/extractJpegDimensions/extractBmpDimensions. Pushed in 09c53bb; the 10 imageTokenizer tests still pass.

@wenshao

wenshao commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @he-yufeng!

Template looks good ✓

On direction: textbook bug fix — the VP8X height offset was one byte too early (readUInt32LE(26) instead of starting at byte 27), producing garbage heights (e.g. 20225 instead of 80) for every WebP image with alpha, animation, or metadata. That corrupts token/budget accounting for those images. Clearly within core engine scope. No CHANGELOG precedent for WebP-specific fixes, but image token estimation is foundational.

On approach: exactly the right scope — 2 source lines corrected, 1 focused unit test added, 3 clarifying comments on the sibling branches (VP8/VP8L) for consistency. Nothing extraneous. The choice of readUIntLE(offset, 3) over readUInt32LE(27) & 0xffffff is the right call: it avoids a RangeError on the minimum 30-byte VP8X header that the existing buffer.length < 30 guard would otherwise let through.

Moving on to code review. 🔍

中文说明

感谢 @he-yufeng 的贡献!

模板完整 ✓

方向:教科书式 bug 修复 —— VP8X 高度读取偏移早了一个字节(readUInt32LE(26) 而非从第 27 字节开始),导致所有带 alpha、动画或元数据的 WebP 图片高度值严重错误(例如 20225 而非 80),进而破坏 token/预算计算。完全属于核心引擎范畴。CHANGELOG 中没有 WebP 相关修复的先例,但图像 token 估算是基础功能。

方案:范围恰到好处 —— 仅修正 2 行源码,新增 1 个聚焦的单元测试,并为 VP8/VP8L 分支补充了 3 行一致性注释。没有多余内容。选择 readUIntLE(offset, 3) 而非 readUInt32LE(27) & 0xffffff 是正确的:后者在 30 字节最小 VP8X 头上会触发 RangeError,而现有的 buffer.length < 30 守卫恰好会放行这种长度。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal (before reading diff): The VP8X spec puts canvas width-1 at bytes 24–26 and height-1 at bytes 27–29, both 24-bit little-endian. The old code read height with readUInt32LE(26) which covers bytes 26–29 — one byte too early. The fix should read exactly 3 bytes from the correct offsets. Using readUInt32LE(27) & 0xffffff is tempting but dangerous: on a minimum 30-byte VP8X header, readUInt32LE(27) touches byte 30 and throws RangeError. The right call is readUIntLE(offset, 3) — reads exactly 3 bytes, no boundary issue.

Diff matches my proposal exactly. The two-line fix is correct and minimal. The added comments on all three branches (VP8, VP8L, VP8X) document byte offsets consistent with the style already used in extractPngDimensions / extractJpegDimensions / extractBmpDimensions. The test constructs a minimal 30-byte VP8X header with known dimensions and asserts both width and height — good coverage including the boundary case.

No correctness bugs, no security holes, no AGENTS.md violations. Nothing to flag.

Testing

Unit tests — fixed code (10/10 pass)

$ cd packages/core && npx vitest run src/utils/request-tokenizer/imageTokenizer.test.ts

 ✓ src/utils/request-tokenizer/imageTokenizer.test.ts (10 tests) 7ms

 Test Files  1 passed (1)
      Tests  10 passed (10)
   Duration  4.42s

Teeth check — revert source fix only, keep tests (1 fails as predicted)

$ git checkout -- imageTokenizer.ts   # revert to buggy offsets
$ npx vitest run src/utils/request-tokenizer/imageTokenizer.test.ts

 × ImageTokenizer > WebP dimension extraction > should extract canvas dimensions from VP8X
     → expected 20225 to be 80

 Test Files  1 failed (1)
      Tests  1 failed | 9 passed (10)

Width 100 is already correct (not affected by the offset bug); only height is wrong — matches the PR description exactly. The new test genuinely catches the bug.

Tmux real-scenario note

This fix affects internal image-token estimation for VP8X WebP files. The symptom was silently corrupted token counts (not a crash or visible error), so there's no meaningful TUI before/after to capture in tmux. Collaborator @wenshao independently verified against real Pillow-encoded VP8X files (321×123, 640×16) and confirmed correct parsing on the fix, wrong heights on the original code.

Summary

The fix is correct, the test has teeth, and the scope is minimal. LGTM from code review and testing. ✅

中文说明

代码审查

独立方案(读 diff 之前): VP8X 规范将画布 width-1 放在第 24–26 字节,height-1 放在第 27–29 字节,均为 24 位小端。旧代码用 readUInt32LE(26) 读高度,覆盖了第 26–29 字节——早了一个字节。修复应该从正确偏移读取恰好 3 个字节。直接用 readUInt32LE(27) & 0xffffff 看似可行但有隐患:在 30 字节最小 VP8X 头上,readUInt32LE(27) 会触及第 30 字节并抛出 RangeError。正确选择是 readUIntLE(offset, 3)——精确读 3 字节,无边界问题。

Diff 与我的方案完全一致。 两行修复正确且最小化。三个分支(VP8、VP8L、VP8X)上的注释记录了字节偏移,与 extractPngDimensions / extractJpegDimensions / extractBmpDimensions 的风格一致。测试构造了一个已知尺寸的最小 30 字节 VP8X 头并断言宽高——覆盖了边界情况。

无正确性 bug、无安全漏洞、无 AGENTS.md 违规。无需标记的问题。

测试

单元测试——修复后代码(10/10 通过)

 ✓ src/utils/request-tokenizer/imageTokenizer.test.ts (10 tests) 7ms
 Test Files  1 passed (1)      Tests  10 passed (10)

有效性检查——回退源码修复,保留测试(1 个如预期失败)

 × should extract canvas dimensions from VP8X
     → expected 20225 to be 80
 Test Files  1 failed (1)      Tests  1 failed | 9 passed (10)

宽度 100 本就正确(不受偏移 bug 影响);仅高度错误——与 PR 描述完全吻合。新测试确实能抓到该 bug。

Tmux 真实场景说明

本修复影响 VP8X WebP 文件的内部图像 token 估算。症状是 token 数静默错误(非崩溃或可见错误),因此没有有意义的 TUI 前后对比可供 tmux 捕获。协作者 @wenshao 已独立用真实 Pillow 编码的 VP8X 文件(321×123、640×16)验证了修复后解析正确、原代码高度错误。

总结

修复正确,测试有效,范围最小。代码审查和测试通过。✅

Qwen Code · qwen3.7-max

@wenshao
wenshao enabled auto-merge (squash) June 17, 2026 20:17
@wenshao

wenshao commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

This is one of those PRs that makes you glad someone cares about the details. The VP8X height offset bug was subtle — width happened to be correct because readUInt32LE(24) & 0xffffff reads bytes 24–27, and the low 3 bytes are exactly bytes 24–26. But height at readUInt32LE(26) & 0xffffff reads bytes 26–29, when the spec says bytes 27–29. Off by one, silently corrupting token counts for every VP8X WebP image.

The fix is exactly what I would have written: readUIntLE(offset, 3) instead of readUInt32LE(offset) & 0xffffff. It reads exactly 3 bytes, avoids the RangeError trap on 30-byte headers, and the PR author explains why in the description — not just what changed. That kind of attention to detail is rare and appreciated.

The test has teeth: reverting the source fix produces expected 20225 to be 80, proving it genuinely catches the bug rather than just asserting what the code happens to do. @wenshao's independent verification with real Pillow-encoded VP8X files adds further confidence.

The scope is minimal — 2 lines fixed, 1 test added, 3 comments for consistency with the VP8/VP8L siblings. Nothing extraneous. No AGENTS.md concerns, no security issues, no direction questions.

If I had to maintain this in six months, I'd thank the author. The comments document the byte layout, the test is self-explanatory, and the PR description is a mini-spec for the VP8X header format.

Verdict: ship it. ✅

中文说明

反思

这个 PR 让人欣慰——有人在意这些细节。VP8X 高度偏移 bug 很隐蔽:宽度恰好正确是因为 readUInt32LE(24) & 0xffffff 读的是第 24–27 字节,低 3 字节正好是第 24–26 字节。但高度用 readUInt32LE(26) & 0xffffff 读的是第 26–29 字节,而规范要求第 27–29 字节。差一个字节,每个 VP8X WebP 图片的 token 计算都被悄悄破坏了。

修复方案和我自己会写的一样:用 readUIntLE(offset, 3) 代替 readUInt32LE(offset) & 0xffffff。精确读 3 字节,避开 30 字节头的 RangeError 陷阱,而且 PR 作者在描述中解释了为什么这样做——不只是改了什么。这种对细节的关注很难得。

测试有实际效果:回退源码修复后得到 expected 20225 to be 80,证明确实能抓到 bug,而非仅仅断言代码当前的行为。@wenshao 用真实 Pillow 编码的 VP8X 文件独立验证,进一步增强了信心。

范围最小化——2 行修复、1 个测试、3 行与 VP8/VP8L 分支一致的注释。没有多余内容。无 AGENTS.md 问题,无安全隐患,无方向疑问。

如果六个月后我要维护这段代码,我会感谢作者。注释记录了字节布局,测试自解释,PR 描述本身就是一份 VP8X 头部格式的迷你规范。

结论:可以合入。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit e290c28 into QwenLM:main Jun 18, 2026
20 checks passed

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

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.

4 participants