Skip to content

Render a media=print capture under emulated print media - #6139

Draft
lukemelia wants to merge 2 commits into
cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-getfrom
cs-12978-implement-mediaprint-emulation-and-css-driven-paper-size
Draft

lukemelia wants to merge 2 commits into
cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-getfrom
cs-12978-implement-mediaprint-emulation-and-css-driven-paper-size

Conversation

@lukemelia

@lukemelia lukemelia commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6121 (pdf persist/serve) in the Cards to PDF chain — base is that branch, not main.

What

Gives the media axis its behavior. A media=print capture now settles under print media, so the card's print CSS applies during the render:

  • @page { size: … } sets the paper (via preferCSSPageSize), Chrome's default otherwise
  • @media print rules and break-after: page / break-inside: avoid pagination

The emulation is lifted to wrap the whole render in captureScreenshot — before the settle and the image-paint wait — so a print-only layout's fonts and background images load and are waited on, not just painted at capture time. It is cleared in the render-level finally, the same save/restore discipline the viewport already gets, so a print capture leaves no media override on the pooled page for the indexing HTML path or the next screenshot to inherit.

Details

  • media=print is no longer refused at parse (the UNSUPPORTED_CAPTURE_MEDIA gate is gone). It parses and round-trips on both the POST body and the GET URL DSL, and is servable (a print-media raster is still an image).
  • Emulation is page-level and a batch shares one settled render, so a batch must render under one media. A mixed-media batch is refused at the shared parse and guarded again at the engine, rather than settling later entries under the wrong media.
  • The pdf leg keeps its own page.pdf media pin: page.pdf forces print for the print job whatever the page emulates, so a media=screen pdf still has to emulate screen to override that default. The two restores are idempotent.

Tests

  • Parse: media=print parses/round-trips on both surfaces; invalid media still refused; mixed-media batch refused; uniform-media batch allowed.
  • Render (prerendering-test.ts): a raster media=print capture shows a print-only background color and the next default capture does not — the pdf leg pins its own media at capture time, so the raster path is what proves the settle-level emulation and the render-level restore; a new paged fixture with @page { size: A4 } + break-after: page produces a multi-page A4 PDF via media=print (asserted against the PDF's /MediaBox); a regression test confirms the next pooled capture renders under screen media again (no print-media bleed-through).

🤖 Generated with Claude Code

https://claude.ai/code/session_014GsYGsuCqHJz9GAti4jheG

@lukemelia lukemelia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] This review traced the media-emulation lifecycle (settle → capture → restore) across every capture path — raster single and batch, pdf, envelope re-renders, the declared-screenshots path, and error exits — plus the parse/engine twin guards on mixed-media batches, and what the new tests actually pin.

Bottom line: no blocking issues. The mechanism is correct on every path traced; the one real ask is coverage — both new render tests stay green without the captureScreenshot half of this change, and the raster media=print shape (the only media=print form the GET DSL can request) is untested.

  1. Add a raster media=print test — the pdf leg's own media pin masks the engine change from both new render tests; see the comment on the print-pdf test in prerendering-test.ts.
  2. The engine's mixed-media guard message can interpolate undefined — mechanical one-liner; see the comment in prerender/utils.ts.
  3. Two new comments use change-log wording ("now honored" / "no longer gated") — see the comment in capture-spec.ts.

CI was still in flight at review time; nothing red.

);
});

test('a media=print pdf paginates to the CSS paper size', async function (assert) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Neither of the new render tests would fail if the engine half of this change were lost, because both capture via type=pdf and captureOneEntry's pdf leg independently emulates the spec's media immediately before page.pdf() and clears it in its own finally. So this test's A4/pagination assertions hold even when the settle ran under screen media (the print job re-lays-out under print media with preferCSSPageSize either way), and the bleed test below finds no leak to catch because the pdf leg's inner restore already cleaned up.

The two behaviors captureScreenshot gains here — settling under print, and the outer finally restore — are only load-bearing for a raster media=print capture, which no test exercises. That is also the only media=print shape the GET DSL can request (it refuses type=pdf), so it's the axis's primary production form.

A raster pair pins both: a media=print png of a card with a print-only style (say a @media print background color, asserted via decodePngRGBA), followed by a default capture asserting the screen layout is back. The raster path has no inner pin, so that pair fails without both the settle-time emulation and the outer restore.

Test-coverage gap; non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 4713261. print-probe gains a print-only background color, and a new raster test asserts the color is present in a media=print png (the settle ran under print media) and absent from the next default capture — the render-level restore, which the raster path alone depends on. Ran the prerender suite locally: the new raster test and both print-pdf tests pass.

if ((entry.media ?? 'screen') !== firstMedia) {
return buildInvalidRenderResponseError(
page,
`capture batch mixes media values (${firstMedia} and ${entry.media}); a batch renders under one media`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] When the mismatching entry carries no explicit media (a defaulted-screen entry against a print batch), this interpolates undefined into the error message. Cosmetic; non-blocking.

Suggested change
`capture batch mixes media values (${firstMedia} and ${entry.media}); a batch renders under one media`,
`capture batch mixes media values (${firstMedia} and ${entry.media ?? 'screen'}); a batch renders under one media`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 4713261 — a defaulted entry now interpolates as screen.

Comment thread packages/runtime-common/capture-spec.ts Outdated
Comment on lines +151 to +153
// corresponding leg: a jpeg/webp encode on the on-demand path. (The `print`
// media value is now honored — the engine emulates print media across the
// settle — so both `CaptureMedia` values are supported and none is gated.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] "is now honored" reads as a change-log entry rather than the current contract — "now" has no anchor for a later reader. The new parse test's comment in media-cache-dsl-test.ts has the same wording ("The engine now emulates… no longer gated"). State both timelessly, e.g. here: "Both CaptureMedia values are supported — the engine emulates the spec's media across the settle — so only output types are gated." Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Fixed in 4713261 — both comments now state the contract timelessly.

@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files      1 suites   2h 4m 35s ⏱️
4 806 tests 4 792 ✅ 14 💤 0 ❌
4 821 runs  4 807 ✅ 14 💤 0 ❌

Results for commit 4713261.

Realm Server Test Results

    1 files    225 suites   1h 20m 0s ⏱️
3 078 tests 3 078 ✅ 0 💤 0 ❌
3 117 runs  3 117 ✅ 0 💤 0 ❌

Results for commit 4713261.

@lukemelia
lukemelia changed the base branch from cs-12976-pagepdf-capture-leg-with-pagebyte-bounds-capture-only-via to cs-12977-persist-pdfs-to-mediacache-and-serve-from-the-get September 15, 2026 23:04
@lukemelia
lukemelia added this pull request to stack #6122 September 15, 2026 23:04
@lukemelia
lukemelia force-pushed the cs-12978-implement-mediaprint-emulation-and-css-driven-paper-size branch from 9b3d602 to c1a8fe7 Compare September 15, 2026 23:15
@lukemelia lukemelia closed this Sep 15, 2026
@lukemelia lukemelia reopened this Sep 15, 2026
ylm and others added 2 commits September 15, 2026 19:23
The `media` axis gains its behavior: a `media=print` capture now settles
under print media, so the card's print CSS applies during the render —
`@page` paper size (via preferCSSPageSize), `@media print` rules, and
`break-*` pagination. The emulation is lifted to wrap the whole render in
captureScreenshot, before the settle and the image-paint wait, so a
print-only layout's resources load and are waited on, not just painted at
capture time. It is restored in the render-level `finally`, the same
save/restore discipline the viewport already gets, so a print capture
leaves no media override on the pooled page for the indexing HTML path or
the next screenshot to inherit.

`media=print` is no longer refused at parse. Because emulation is
page-level and a batch shares one settled render, a batch must render
under one media — a mixed-media batch is refused at the shared parse and
guarded again at the engine, rather than settling later entries under the
wrong media. The pdf leg keeps its own `page.pdf` media pin: page.pdf
forces print for the print job whatever the page emulates, so a
media=screen pdf must emulate screen to override that default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GsYGsuCqHJz9GAti4jheG
The pdf leg's own media pin masks the render-level emulation from
pdf-based tests, so pin it with a raster: print-probe gains a print-only
background color, and a new test asserts the color is present in a
media=print png (the settle ran under print media) and absent from the
next default capture (the render-level restore, which the raster path
alone depends on). Also interpolate the defaulted media value into the
engine's mixed-media guard message instead of 'undefined', and restate
two change-log-style comments timelessly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lukemelia
lukemelia force-pushed the cs-12978-implement-mediaprint-emulation-and-css-driven-paper-size branch 2 times, most recently from ce0e4e8 to 4713261 Compare September 15, 2026 23:25
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