feat(api): let a semantic export backend ask for the compiled layout - #702
Merged
Merged
Conversation
A semantic backend walks the authored tree and is handed no geometry. That is right for most of them and wrong for the ones that need a number the engine has already worked out — a resolved column width, a settled page count — and today there is no way to ask: the backend either re-derives it, which means measuring text it has no font runtime to measure, or does without. SemanticBackend.requiresResolvedLayout() defaults to false. A backend that returns true is handed the same session's compiled layout in SemanticExportContext.layoutGraph(), and DocumentRenderingFacade resolves one only for a backend that asked. The default is the load-bearing half. Compiling a layout measures text, and measurement needs a font runtime that lives in a render module — so resolving one for every semantic export would quietly make a render backend a hard requirement of exports that do not render, and adding the DOCX artifact alone would stop being enough to export DOCX. A backend that says nothing causes no layout to be compiled at all. Consistency comes free rather than by convention: the graph, the canvas and the layout are all read off the same session state at the same revision, so a backend given both is given a layout compiled from the graph beside it. An edit between two exports is visible to the second, which the qa test asserts by watching the page count grow. SemanticExportContext gains the component as a record, which moves the canonical constructor to five arguments and would remove the published four-argument descriptor. That one is written out by hand instead, so callers compiled against it keep linking; its Javadoc says why it exists rather than leaving the next reader to delete it as redundant. requireLayoutGraph() is for a backend that asked and would rather the absence explained itself than hand back null. All three members are @beta and named in docs/api-stability.md. The seam is new and what a semantic backend should be *given* is still settling — a later minor may narrow it to the measurements a backend actually needs rather than the whole graph — and Experimental is what keeps that possible without a major. Verification: ./mvnw -B -ntp test -Dtest=SemanticExportContextTest -pl :graph-compose-core (5 tests) and -Dtest=SemanticExportLayoutContextTest -pl :graph-compose-qa -am (4 tests), both BUILD SUCCESS. The core tests cover what needs no session — the default, both published constructors, the explained absence, the copied font collection; the qa tests cover what needs a live document and a font runtime — a layout is handed over, is withheld from a backend that did not ask, tracks an edit between exports, and describes the canvas the export was configured with. Binary compatibility: ./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templates, BUILD SUCCESS against the 2.x floor, with a non-empty core/target/japicmp/japicmp-against-baseline.xml — an execution that does not run writes no report and fails nothing, so the green step alone would not have proved the comparison happened. Knowledge pack regenerated in this change; docs/api-stability.md names the new @beta members, which check-stability-doc.mjs verifies.
…s it Three defects in what 85a5ab2 shipped: a justification that is not true, the feature's central behaviour untested, and two normalisations nothing exercised. The Javadoc, the facade comment and the CHANGELOG all said that resolving a layout for every semantic export would make a render backend a hard requirement of exports that do not render. It would not, because it already is one. DocumentSession's constructor calls refreshMeasurementServices(), which resolves a FontMetricsProvider through BackendProviders, and the only registration in the repository ships in graph-compose-render-pdf — so a session cannot be created at all without that artifact, before a node is added and whatever a backend later asks for. The saving is real but it is measurement and pagination over the whole document, not the dependency. All three now say that, and say explicitly what is not saved, so the next reader does not reinstate the wrong reason. The flag's whole purpose had no test. The qa suite asserted the value handed to the backend, never that no layout was compiled — so hoisting the call out of the ternary (var g = context.layoutGraph(); ... ? g : null) restores the exact cost the flag exists to avoid with every assertion still green. SemanticExportLayoutResolutionTest implements DocumentRenderingFacade.Context and counts how often the layout is asked for; the counter was verified by making that edit and watching the test go red on "but was: 1" before reverting it. Two more revert-proof gaps: requireLayoutGraph() was only ever driven down its throwing path, so gutting it to return null broke nothing, and the compact constructor's null-to-EMPTY normalisation of outputOptions was never exercised — the three-argument constructor passes EMPTY itself, so a caller writing null explicitly would have handed a backend outputOptions() == null and failed inside the backend. Both are covered now. Also records two consequences the first commit did not name. Source compatibility is not preserved in two narrow ways: a record pattern destructuring the four-component form no longer compiles, and getRecordComponents().length is 5 rather than 4 — binary linkage is intact and japicmp-verified, source recompilation of those two forms is not. And the record's equals, hashCode and toString now traverse the layout, so on a long document they walk every placed node and fragment; nothing in the repository hashes a context, but a backend logging one would notice. Verification: ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa -am, BUILD SUCCESS across all eight modules. Thirteen tests now cover this seam, nine in core and four in qa.
DemchaAV
added a commit
that referenced
this pull request
Sep 22, 2026
Forward integration so the 2.5 line carries the 2.4.1 release and everything after it. Without it every branch cut from develop and aimed at 2.5-dev carries the difference into its own diff: PR #701 showed 388 files and 46k lines of website, CI and dependency work that is not its own, and could not be merged or even built, because a pull_request runs against a merge ref GitHub cannot create for a conflicting branch. Two conflicts, both resolved against what the 2.5 line already decided. assets/readme/examples/emoji-shortcodes.pdf keeps 2.5-dev's copy: it was re-rendered by #698 so its emoji copy out of the text layer, and develop's is the older render from the v2.4.1 cut. CHANGELOG.md auto-merged into a claim that was not true. The 2.5 line's entries -- the layout opt-in from #702, the SVG icon text and the PDF text layer from #697 and #698, and their tests -- had been written under a heading that said "Planned" on this branch and says "2026-09-21" on develop, so the merge filed them under a release that shipped before any of them existed. They move to a new "v2.5.0 - Planned" section; the released v2.4.1 section is now byte-identical to develop's, which is the check that says nothing else moved with them. The version is left as the merge brought it, 2.4.2-SNAPSHOT. What the 2.5 line calls itself is a release decision, not a merge resolution.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A semantic export backend walks the authored tree and is handed no geometry. That is right for most of them and wrong for the ones that need a number the engine has already worked out — a resolved column width, a settled page count. Today there is no way to ask: the backend either re-derives it, which means measuring text it has no font runtime to measure, or does without.
The concrete case waiting on this:
DocxSemanticBackendwrites no table width at all — notblW, nogridCol— so Word shrinks every table to its content while the reference PDF spans the text width. The column split it needs is a resolved width, not something a semantic backend can compute.What changed
SemanticBackend.requiresResolvedLayout(), defaultfalse. A backend that returnstrueis handed the same session's compiled layout inSemanticExportContext.layoutGraph(). It is for reading geometry the engine settled, not for placing content at coordinates — a backend that wants coordinates is a fixed-layout backend and should implement that contract instead.DocumentRenderingFacade.exportresolves a layout only for a backend that asked. Compiling one runs measurement and pagination over the whole document, work that grows with the document and that an export ignoring geometry has no use for.It does not save the render-module dependency, and the Javadoc says so explicitly rather than leaving the next reader to assume otherwise:
DocumentSessionresolves aFontMetricsProviderin its constructor, onlygraph-compose-render-pdfregisters one, and a session cannot be created without it whatever a backend later asks for. The first draft of this claimed the dependency as the saving; it is wrong, and the wrong version is the kind that gets restored by someone tidying up.Consistency is structural, not conventional. The graph, the canvas and the layout are all read off the same session state at the same revision, so a backend given both is given a layout compiled from the graph beside it. An edit between two exports is visible to the second.
SemanticExportContextgains the component as a record. That moves the canonical constructor to five arguments, so the published four-argument one is written out by hand and callers compiled against it keep linking; its Javadoc says why it exists, so it does not get deleted as redundant.requireLayoutGraph()is for a backend that asked and would rather the absence explain itself than hand backnull.All three members are
@Betaand named indocs/api-stability.md. The seam is new and what a semantic backend should be given is still settling — a later minor may narrow it to the measurements a backend needs rather than the whole graph. Experimental is what keeps that possible without a major.Verification
./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa -am→ BUILD SUCCESS across all eight modules.13 tests cover the seam. Nine in
graph-compose-core, for what needs no session: thefalsedefault, both published constructors, the explained absence and the returned layout, thenull-to-EMPTYnormalisation, the copied font collection — andSemanticExportLayoutResolutionTest, which implementsDocumentRenderingFacade.Contextand counts how often a layout is asked for. That counter exists because asserting the value handed to the backend does not prove the work was skipped: hoisting the call out of the ternary hands a non-asking backend the samenullwhile compiling anyway. The counter was verified by making exactly that edit and watching it fail onbut was: 1.Four in
graph-compose-qa, for what needs a live document and a font runtime: a layout is handed over, is withheld from a backend that did not ask, tracks an edit between exports by page count, and describes the canvas the export was configured with. Plus an empty document, which the render methods refuse outright and a layout-asking semantic export must not inherit that refusal from.Binary compatibility:
./mvnw -B -ntp -DskipTests -P japicmp verify -pl :graph-compose-core,:graph-compose-templates→ BUILD SUCCESS against the 2.x floor, with a non-emptycore/target/japicmp/japicmp-against-baseline.xml. An execution that does not run writes no report and fails nothing, so the green step alone would not have proved the comparison happened.Knowledge pack regenerated in this change;
check-stability-doc.mjsverifies the new@Betamembers are named.Notes
Source compatibility is not fully preserved, in two narrow ways japicmp cannot see — both recorded in the CHANGELOG. A record pattern destructuring the four-component form (
case SemanticExportContext(var canvas, var fonts, var out, var opts)) no longer compiles, andgetRecordComponents().lengthis 5 rather than 4. Binary linkage is intact.The record's
equals,hashCodeandtoStringnow traverse the layout, so on a long document they walk every placed node and fragment. Nothing in the repository hashes a context, but a backend logging one would notice.CHANGELOG placement needs a decision that is not this PR's to make. The entry sits under the existing
## v2.4.1 — Plannedheading, which is what this branch already carries — as do #697 and #698, both of which added public API under it. The branch is still on2.4.1-SNAPSHOT. If the 2.5 line is meant to open, the heading and the version move together in one commit, and this entry moves with them; putting that decision inside a feature PR seemed worse than naming it here.Lane: canonical (
document.api,document.backend.semantic) — new public API,@since 2.5.0,@Beta, stability document and knowledge pack updated alongside.