Skip to content

fix(export): keep the caption background plate through the native bridge - #188

Merged
EtienneLescot merged 6 commits into
release/v1.8.0from
feat/auto-20260728-cc4f7262
Jul 28, 2026
Merged

fix(export): keep the caption background plate through the native bridge#188
EtienneLescot merged 6 commits into
release/v1.8.0from
feat/auto-20260728-cc4f7262

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

The caption inspector stores colour and opacity as two separate fields, and \captionBackgroundCss\ recombines them into a CSS string the editor overlay renders directly (e.g.
gba(0, 0, 0, 0.55)). The native compositor's \parse_hex\ only understood 3- or 6-char hex strings, so anything else fell through to the [0, 0, 0, 0]\ fallback — alpha 0, no plate. The text was drawn, the plate was not, and the contrast that made captions legible in the preview disappeared in the file. Depending on the recording's brightness, the user reads it as 'no captions in the export'.

Fix: teach \parse_hex\ to accept the CSS surface area the JS bridge already produces —
gba(...),
gb(...), and \ ransparent\ — alongside the existing hex format. The \None-then-caller-fallback path is preserved, so a regression in any existing annotation colour still surfaces as a missing element rather than a wrong one. The gradient stop path had the same latent issue (it sends
gba(...)\ from the parser upstream) and gets fixed by the same change.

Related issue

Fixes #178

Type of change

  • Bug fix

Release impact

  • Patch

Desktop impact

  • Windows
  • macOS
  • Linux

Testing

  • 5 new Rust unit tests in \crates/compositor/src/compositor.rs\ (\parse_hex_understands_rgba_caption_backgrounds, \parse_hex_treats_rgb_as_opaque, \parse_hex_keeps_transparent_at_alpha_zero, \parse_hex_still_understands_hex_colours, \parse_hex_rejects_malformed_colours) — all green.
  • 1 new JS test in \src/native/sceneDescription.test.ts\ (\ships the caption background as a parseable CSS colour, not a hex) — pins the bridge contract so a future rewrite can't silently drop the alpha again.
  • Full \cargo test --lib\ for the compositor: 81/81 pass.
  • Full
    pm run test\ (jsdom unit suite): 1145/1145 pass.

  • px tsc --noEmit: clean.

  • pm run lint: no new warnings (7 pre-existing, unrelated).

  • pm run format: no changes needed.

A real Windows smoke test (record a clip, transcribe, enable captions, export MP4, scrub the file) is the manual gate the user-facing side still needs — the unit tests prove the native parser is no longer dropping the alpha, but the visual confirmation has to happen on a real D3D path.

Issue #178 — captions appear in the preview (DOM overlay + native compositor) but the exported video is missing the background plate, so depending on the recording's brightness the user reads it as 'no captions in the export'.

Root cause: the caption inspector stores colour and opacity as two separate fields, and \captionBackgroundCss\ recombines them into a CSS string the editor overlay can render directly (e.g. \
gba(0, 0, 0, 0.55)\). The native side's \parse_hex\ only understood 3- or 6-char hex strings, so anything else — including the caption background — fell through to the \[0, 0, 0, 0]\ fallback (alpha 0, no plate). The text was drawn, the plate was not, and the contrast that made captions legible in the preview disappeared in the file.

Fix: teach \parse_hex\ to accept the CSS surface area the JS bridge already produces — \
gba(...)\, \
gb(...)\, and \	ransparent\ — alongside the existing hex format. The fallback to None (and from there the caller's \[0,0,0,0]\) is preserved for everything that isn't a recognised colour, so a regression in any of the existing annotation colours still surfaces as a missing element rather than a wrong one.

This also unblocks the gradient stop path, which has been sending the same \
gba(...)\ strings through \parse_hex\ for the same reason — they silently fell back to the colour preset. Not the issue's headline, but the same fix.

The JS contract is now pinned by a test in \sceneDescription.test.ts\: the caption background leaves the bridge as \
gba(0, 0, 0, 0.55)\ exactly, and the text colour remains the ColorField hex (\#ffffff\). A future rewrite of either side that drops the alpha will fail this test instead of silently regressing the user-visible output.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 49362d6a-c73b-4b93-adcf-41f3ba8fd622

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auto-20260728-cc4f7262

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ollow CSS arity

Review follow-up on the caption-plate fix.

`strip_color_fn` sliced `s[..name.len()]` without checking the UTF-8 char
boundary, so any colour string whose byte 3 or 4 lands mid-character aborted
the process instead of returning None. `parseWallpaper` hands every
`#`-prefixed string straight through to `SceneBackground::Color`, so
`#ab€cd` was enough to take out the render loop — and a panic crossing the
N-API bridge is exactly what this parser's None-then-caller-fallback
contract exists to avoid.

Taking the tail with `get` first proves the boundary, which makes the head
slice safe by construction and makes the old length guard redundant. The hex
path had the same latent bug independently (`h[i..=i]` / `h[0..2]` on a body
of exactly 3 or 6 bytes, e.g. `éa` or `€€`); an `is_ascii` guard closes it
before any slicing happens. Both predate the rgba work, which only widened
the first one's reach.

Also collapses the two component parsers into one. CSS Color 4 makes `rgb()`
and `rgba()` synonyms, both taking 3 or 4 components, so rejecting
`rgba(0, 0, 0)` was non-standard — and it failed the same silent way #178
did, by falling through to the caller's alpha-0 fallback and painting no
plate at all.

Tests: 83/83 in the compositor lib (81 before, +2 here).
@EtienneLescot
EtienneLescot changed the base branch from main to release/v1.8.0 July 28, 2026 16:49
The preview showed every caption twice, most visibly at widths where the two
copies wrapped differently — one broke to a second line, the other did not.

`PreviewCanvas` hosts the native D3D canvas as "the sole pixel source" (its
own header comment) with the DOM overlays kept interactive-only. `CaptionLayer`
was not interactive: `aria-hidden`, `pointerEvents: none`, and a visible
`<span>` painting the cue text and its background plate. Meanwhile the native
compositor already draws the same cue, because captions are emitted into the
scene as ordinary text annotations. Two painters, two line-breakers — CSS
`word-break`/`pre-wrap` against DirectWrite laying out into `box_px` — so the
same string wrapped at two different points.

The component's own comment explains how it got here: it "mirrors
annotationRenderer.renderText's box model ... so what the preview shows is what
the export writes". That mirror was right when the preview was DOM-drawn and
the exporter was a separate renderer; it became a duplicate once the native
compositor started drawing the preview too.

Deleted rather than gated: there is no non-native preview path left to fall
back to (`VITE_NATIVE_COMPOSITOR` is read nowhere in the tree, and
`NativeCompositorOverlay` mounts unconditionally). Dropping the DOM copy also
makes preview and export agree by construction instead of by two
implementations of the same box model.

`useCaptions` stays — `CaptionsPane` still uses it.

Pre-existing, not introduced by the plate fix: the native side always drew the
caption text, it just drew it without a plate, so the duplicate read as a faint
ghost. Restoring the plate made both copies solid and the doubling obvious.

Tests: 96 files / 1061 pass, tsc clean, biome clean.
The caption doc still described a DOM preview painter alongside the native
exporter, and linked to `CaptionLayer.tsx`, which no longer exists — the docs
check failed on the dead link.

Rewritten around what is actually there: one path, cues -> synthetic text
regions -> annotation plumbing -> native compositor, which draws preview and
export alike. That is a stronger version of the property the old text was
reaching for: preview and export cannot drift because they are the same
renderer, not two box models kept in sync by hand.

Kept a short note on why the DOM layer existed and why it went, so the next
reader does not re-add a "preview overlay" to fix a perceived gap. Also
recorded that `captionBackgroundCss` emits `rgba(...)`, which is what forces
the native colour parser to accept CSS colours rather than hex only.

`docs:check` OK (22 files).
@EtienneLescot
EtienneLescot merged commit 07640ce into release/v1.8.0 Jul 28, 2026
9 checks passed
@EtienneLescot
EtienneLescot deleted the feat/auto-20260728-cc4f7262 branch July 28, 2026 18:00
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.

bug: captions are not rendered

1 participant