Skip to content

fix(capture): Linux headless 截图中 ASCII 字母被裁成上半部分#53

Merged
Shadow-Azure merged 2 commits into
mainfrom
fix/headless-glyph-baseline-clipping
Jul 19, 2026
Merged

fix(capture): Linux headless 截图中 ASCII 字母被裁成上半部分#53
Shadow-Azure merged 2 commits into
mainfrom
fix/headless-glyph-baseline-clipping

Conversation

@Shadow-Azure

Copy link
Copy Markdown
Owner

Problem

Linux headless 沙箱的截图里,所有 ASCII 字母都被裁成只剩上半部分,标点/制表符正常,与 macOS 体验差距很大(见 release_test/2026-07-19-19-40-45/claude-07-after-trust.png)。

根因在 crates/cli-box-core/src/capture/headless.rsrender_png()

let ascent_px = font.ascent_unscaled() / upem * line_h;   // ← 错误分母
let base_y = y0 + ascent_px;

ab_glyph 的 PxScale.y 映射的是字体的 height_unscaled(= ascent − descent),不是 units_per_em(文档明说的"非标准 scale")。代码用 upem 做分母,对任何 ascent − descent > 1em 的字体(所有 CJK 字体:Linux 的 Noto CJK、Arial Unicode)都会高估基线偏移,把基线推到 cell 底边以下。又因为渲染是自上而下的,下一行的背景填充会覆盖上一行溢出的像素,于是每个高字母的下半部分被擦除。

macOS 没事只是因为它走 Electron/xterm.js GUI 渲染器,根本不经过这条 headless 路径;headless 渲染器的垂直定位此前从未被验证过(原有测试只查尺寸和"有没有墨迹")。

Solution

一行修复0048e2d):用与光栅化器一致的 as_scaled(scale).ascent() 推导基线偏移——它内部除以 height_unscaled,任意字体下字形都落在 cell 内:

let ascent_px = font.as_scaled(scale).ascent();

根因定位(systematic-debugging 四阶段,无猜测):

  • 逐行测 Linux 截图墨迹(880×432 = 80×11px × 24×18px):每行墨迹只在 rel=[10,17](cell 底部 8px),顶部 10px 全空 → 基线被推到 cell 下方。
  • metrics probe 实测 ab_glyph scale 语义(Arial Unicode):旧代码偏移 19.24px > cell 18px(裁剪),正确值 14.36px

回归测试 render_png_tall_glyphs_span_cell_height:断言高字形墨迹到达 cell 顶部——旧代码 y=9 失败(字形挤在 cell 底部),修复后通过。

修复后实测:墨迹从 rel=[10,17](8px 残缺)→ rel=[4,14](10px 全高),与 macOS 参考渲染(~14px/19px 行)一致。

Test Plan

  • 新增回归测试 render_png_tall_glyphs_span_cell_height:旧代码 FAIL(y=9),修复后 PASS
  • cargo test -p cli-box-core -p cli-box-cli:core 166 + cli 33 + 7 个集成套件全过
  • cargo clippy -p cli-box-core --all-targets -- -D warnings:无警告
  • cargo fmt --all -- --check:通过
  • metrics probe 验证 ab_glyph PxScale.yheight_unscaled(非 upem),Arial Unicode 偏移 19.24→14.36
  • Linux 实机复测:在 Ubuntu 服务器重跑 release 测试,确认 ASCII 字母恢复全高(本地已用同属 CJK metric 族的 Arial Unicode 验证机制,但实机 Noto CJK 复测为最终权威确认)

🤖 Generated with Claude Code

The headless renderer derived each glyph's baseline offset from
`ascent_unscaled / units_per_em`, but ab_glyph's `PxScale.y` maps the
font's full `height_unscaled` (ascent − descent) — not units_per_em — to
pixels. For CJK-capable fonts (Noto CJK on Linux, Arial Unicode) whose
ascent−descent exceeds 1em, the offset was too large and pushed the
baseline below the cell. Because rows are painted top-to-bottom, the next
row's background fill then erased the lower half of every tall glyph — on
Linux screenshots every ASCII letter showed only its top half.

Use `as_scaled(scale).ascent()`, which divides by height_unscaled and so
matches the outline rasterization. Add a regression test asserting a tall
glyph reaches into the top of its cell (fails at y=9 on the old code,
passes after the fix).

Root-caused via systematic-debugging: per-row ink in the Linux screenshot
only occupied rel=[10,17] of each 18px cell; a metrics probe confirmed the
ab_glyph scale semantics (Arial Unicode: 19.24px offset > 18px cell with
the bug, 14.36px correct).

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

🔒 门禁检查结果

检查项 状态
Rust 格式化 ✅ success
Rust Clippy ✅ success
Rust 测试 & 覆盖率 ✅ success
前端测试 & 覆盖率 ✅ success
Playwright E2E ✅ success
统一测试 (test.sh) ✅ success
安全检查 ✅ success
发布模拟验证 ✅ success
升级流程测试 ✅ success
cli-box-skill 单元测试 ✅ success

Rust 测试覆盖率

指标 覆盖率
行覆盖率 67.3%
分支覆盖率 0.0%
模块 行覆盖率
crates.cli-box-core.src.capture ████████████████░░░░ 84.2%
crates.cli-box-core.src.daemon ██████████░░░░░░░░░░ 52.3%
crates.cli-box-core.src.server █████████████████░░░ 87.7%
crates.cli-box-core.src.process ███████████░░░░░░░░░ 57.7%
crates.cli-box-core.src.automation ███████████░░░░░░░░░ 58.3%
crates.cli-box-core.src ███████████░░░░░░░░░ 55.3%
crates.cli-box-core.src.instance ███████████████████░ 99.1%
crates.cli-box-core.src.sandbox ███████████████████░ 98.8%

详细报告见 Rust 覆盖率 artifact

前端测试覆盖率

指标 覆盖率
行覆盖率 ████████████████░░░░ 78.0%
分支覆盖率 70.4%
函数覆盖率 74.2%
语句覆盖率 77.5%
文件 行覆盖率
src/tests/mocks/websocket.ts ██████████████████░░ 88.0%
src/tests/mocks/xterm.ts ████████████████████ 100.0%
src/main/daemon-bridge.ts ████████░░░░░░░░░░░░ 40.0%
src/renderer/api.ts ██████████████░░░░░░ 68.2%
src/renderer/screenshotSync.ts ████████████████████ 100.0%
src/renderer/scrollback.ts ██████████████████░░ 88.9%
src/renderer/tabState.ts ████████████████████ 100.0%
src/renderer/terminalBuffer.ts ████████████████████ 100.0%

详细报告见前端覆盖率 artifact

✅ 所有检查通过,可以合入

点击 Squash and merge 合并此PR

…atin fonts

Code review (#53) noted the regression test would silently pass for the
wrong reason on a host whose `load_font()` resolves a Latin-only font
(height≈upem), where the clipping bug never manifests — so it couldn't
actually guard the regression there.

Resolve by computing the *buggy* baseline offset (ascent_unscaled/upem*18)
up front and skipping loudly (with a clear reason) when it does not exceed
the cell height, rather than false-passing. On Arial Unicode / Noto CJK the
offset is 19.24 > 18, so the test still runs and discriminates bug vs fix.

Co-Authored-By: Claude <noreply@anthropic.com>
@Shadow-Azure
Shadow-Azure merged commit 726ae5c into main Jul 19, 2026
14 checks passed
@Shadow-Azure Shadow-Azure mentioned this pull request Jul 19, 2026
5 tasks
Shadow-Azure added a commit that referenced this pull request Jul 19, 2026
Patch release for the Linux headless screenshot glyph-clipping fix
(#53): ASCII letters were rendered at half height because the baseline
offset divided by units_per_em instead of height_unscaled. Also resyncs
Cargo.lock (was stale at 0.3.0).

Co-authored-by: ZN-Ice <zn-ice@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
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