A semantic backend walks the authored tree and needs no geometry, which is why + * the default is {@code false} and why the session does not compile a layout for one. + * 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 this flag should not + * be described as if it did: {@code DocumentSession} resolves a + * {@code FontMetricsProvider} in its constructor, only + * {@code graph-compose-render-pdf} registers one, and a session cannot be created + * without it whatever a backend later asks for.
+ * + *A backend that answers {@code true} is handed the same session's compiled layout + * in {@link SemanticExportContext#layoutGraph()}. It is for reading what the engine + * already worked out — a resolved width, a settled page count — not for placing + * content at coordinates; a backend that wants coordinates is a fixed-layout backend + * and should implement that contract instead.
+ * + * @return true to be given a resolved layout; false to be given the graph alone + * @since 2.5.0 + */ + @com.demcha.compose.document.api.Beta + default boolean requiresResolvedLayout() { + return false; + } + /** * Exports the semantic document graph without running a fixed-layout renderer. * diff --git a/core/src/main/java/com/demcha/compose/document/backend/semantic/SemanticExportContext.java b/core/src/main/java/com/demcha/compose/document/backend/semantic/SemanticExportContext.java index 516b30584..244d6bc1f 100644 --- a/core/src/main/java/com/demcha/compose/document/backend/semantic/SemanticExportContext.java +++ b/core/src/main/java/com/demcha/compose/document/backend/semantic/SemanticExportContext.java @@ -1,6 +1,7 @@ package com.demcha.compose.document.backend.semantic; import com.demcha.compose.document.layout.LayoutCanvas; +import com.demcha.compose.document.layout.LayoutGraph; import com.demcha.compose.document.output.DocumentOutputOptions; import com.demcha.compose.font.FontFamilyDefinition; @@ -17,12 +18,18 @@ * @param outputFile optional export output file * @param outputOptions backend-neutral document output options (metadata, * watermark, headers/footers, protection) + * @param layoutGraph the same document's compiled layout, or {@code null} — present only + * for a backend that asked for it through + * {@link SemanticBackend#requiresResolvedLayout()}, and always + * compiled from the graph handed to the same {@code export} call + * @since 2.5.0 carries {@code layoutGraph} */ public record SemanticExportContext( LayoutCanvas canvas, CollectionWritten out rather than left to the record because adding the component moved + * the canonical constructor to five arguments, and this four-argument descriptor is + * published: callers compiled against it would stop linking.
+ * + * @param canvas physical page canvas for semantic export + * @param customFontFamilies document-local font families available to the backend + * @param outputFile optional export output file + * @param outputOptions backend-neutral document output options + */ + public SemanticExportContext(LayoutCanvas canvas, + CollectionWhether the backend is handed a layout is a different question, and the + * qa suite answers it against a live session. This one answers the question that + * matters for what the feature costs: was the layout compiled at all. The two + * come apart — hoisting the call out of the ternary + * ({@code var g = context.layoutGraph(); ... ? g : null}) hands a non-asking backend + * the same {@code null} while compiling the layout anyway, which is exactly the cost the + * flag exists to avoid, and every assertion about the handed-over value stays green. + * So the session stands in as a counter here, and the assertion is on the count.
+ * + * @author Artem Demchyshyn + */ +class SemanticExportLayoutResolutionTest { + + @Test + void aBackendThatDoesNotAskShouldNotCauseALayoutToBeCompiled() throws Exception { + CountingContext context = new CountingContext(); + + new DocumentRenderingFacade(context).export(new ProbeBackend(false), null); + + assertThat(context.layoutRequests) + .as("the session was never asked to compile a layout") + .isZero(); + } + + @Test + void aBackendThatAsksShouldCauseExactlyOneCompilation() throws Exception { + CountingContext context = new CountingContext(); + + new DocumentRenderingFacade(context).export(new ProbeBackend(true), null); + + assertThat(context.layoutRequests) + .as("asked for once, not once per read") + .isEqualTo(1); + } + + /** Answers whatever it is asked, and counts how often the layout is wanted. */ + private static final class CountingContext implements DocumentRenderingFacade.Context { + + private final LayoutCanvas canvas = LayoutCanvas.from(595, 842, Margin.of(36)); + private final DocumentGraph graph = new DocumentGraph(List.of()); + private int layoutRequests; + + @Override + public void ensureOpen() { + } + + @Override + public void ensureRenderable() { + } + + @Override + public String sessionId() { + return "counting"; + } + + @Override + public long revision() { + return 1; + } + + @Override + public int rootCount() { + return 0; + } + + @Override + public LayoutCanvas canvas() { + return canvas; + } + + @Override + public ListThese are the parts that need no session: that a backend which says nothing wants + * no layout, that the published constructors still work and leave the layout absent, and + * that asking for one that was never supplied says why rather than handing back + * {@code null}. Whether the session actually compiles one, and whether it compiles the + * layout of the graph beside it, is a question about a live document and is asked in the + * qa module, where a font runtime exists to measure with.
+ * + * @author Artem Demchyshyn + */ +class SemanticExportContextTest { + + private static final LayoutCanvas CANVAS = + LayoutCanvas.from(595, 842, com.demcha.compose.engine.components.style.Margin.of(36)); + + @Test + void aBackendThatSaysNothingShouldNotBeGivenALayout() { + SemanticBackendThe second half is the part worth protecting. Compiling a layout measures text, and + * measurement needs a font runtime that lives in a render module — so if the session + * resolved a layout for every semantic export, adding a DOCX dependency alone would stop + * being enough to export DOCX. The counter is a backend that records whether it was + * handed one.
+ * + * @author Artem Demchyshyn + */ +class SemanticExportLayoutContextTest { + + /** Records what the session handed it, and optionally asks for a layout. */ + private static final class RecordingBackend implements SemanticBackend