diff --git a/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java b/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java
index 412d638eb..4ba97ae2c 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentChromeOptions.java
@@ -1,7 +1,7 @@
package com.demcha.compose.document.api;
-import com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend;
-import com.demcha.compose.document.backend.fixed.pdf.PdfOutputOptionsTranslator;
+import com.demcha.compose.document.backend.fixed.BackendProviders;
+import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
import com.demcha.compose.document.backend.fixed.pdf.options.*;
import com.demcha.compose.document.output.*;
@@ -100,34 +100,16 @@ DocumentOutputOptions snapshot() {
}
/**
- * Builds a configured {@link PdfFixedLayoutBackend} for the session's
- * convenience PDF methods. When no debug overlay is enabled and no
- * chrome is attached, returns the bare default backend so callers do
- * not pay for empty option arrays.
+ * Resolves and configures the fixed-layout backend for the session's
+ * convenience output methods, translating the attached chrome through the
+ * registered
+ * {@link com.demcha.compose.document.backend.fixed.FixedLayoutBackendProvider}.
*
- * @param debug debug overlay options for the convenience PDF backend;
- * never {@code null}
- * @return ready-to-use PDF backend
+ * @param debug debug overlay options; never {@code null}
+ * @return a configured renderer
*/
- PdfFixedLayoutBackend toConveniencePdfBackend(DocumentDebugOptions debug) {
- if (!debug.enabled() && isEmpty()) {
- return new PdfFixedLayoutBackend();
- }
- PdfFixedLayoutBackend.Builder builder = PdfFixedLayoutBackend.builder()
- .debug(debug)
- .metadata(PdfOutputOptionsTranslator.toPdf(metadata))
- .watermark(PdfOutputOptionsTranslator.toPdf(watermark))
- .protect(PdfOutputOptionsTranslator.toPdf(protection))
- .viewerPreferences(PdfOutputOptionsTranslator.toPdf(viewerPreferences));
- for (DocumentHeaderFooter entry : headersAndFooters) {
- PdfHeaderFooterOptions translated = PdfOutputOptionsTranslator.toPdf(entry);
- if (entry.getZone() == DocumentHeaderFooterZone.FOOTER) {
- builder.footer(translated);
- } else {
- builder.header(translated);
- }
- }
- return builder.build();
+ FixedLayoutRenderer toConveniencePdfBackend(DocumentDebugOptions debug) {
+ return BackendProviders.fixedLayout().create(snapshot(), debug);
}
// PDF-flavoured compatibility setters -----------------------------------
diff --git a/src/main/java/com/demcha/compose/document/api/DocumentRenderingFacade.java b/src/main/java/com/demcha/compose/document/api/DocumentRenderingFacade.java
index cd6ca3c21..9b10f2981 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentRenderingFacade.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentRenderingFacade.java
@@ -2,7 +2,7 @@
import com.demcha.compose.document.backend.fixed.FixedLayoutBackend;
import com.demcha.compose.document.backend.fixed.FixedLayoutRenderContext;
-import com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend;
+import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
import com.demcha.compose.document.backend.semantic.SemanticBackend;
import com.demcha.compose.document.backend.semantic.SemanticExportContext;
import com.demcha.compose.document.layout.DocumentGraph;
@@ -224,6 +224,6 @@ interface Context {
DocumentOutputOptions outputOptions();
- PdfFixedLayoutBackend conveniencePdfBackend();
+ FixedLayoutRenderer conveniencePdfBackend();
}
}
diff --git a/src/main/java/com/demcha/compose/document/api/DocumentSession.java b/src/main/java/com/demcha/compose/document/api/DocumentSession.java
index 8ae52b00f..19b1b3e42 100644
--- a/src/main/java/com/demcha/compose/document/api/DocumentSession.java
+++ b/src/main/java/com/demcha/compose/document/api/DocumentSession.java
@@ -1,9 +1,11 @@
package com.demcha.compose.document.api;
import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.backend.fixed.BackendProviders;
import com.demcha.compose.document.backend.fixed.FixedLayoutBackend;
-import com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend;
-import com.demcha.compose.document.backend.fixed.pdf.PdfMeasurementResources;
+import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
+import com.demcha.compose.document.backend.fixed.MeasurementResources;
+import com.demcha.compose.document.backend.fixed.SectionUnit;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfHeaderFooterOptions;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfMetadataOptions;
import com.demcha.compose.document.backend.fixed.pdf.options.PdfProtectionOptions;
@@ -89,7 +91,7 @@ public final class DocumentSession implements AutoCloseable {
private DocumentDebugOptions debug = DocumentDebugOptions.none();
private List pageBackgrounds = List.of();
private List pageMargins = List.of();
- private PdfMeasurementResources measurementResources;
+ private MeasurementResources measurementResources;
private boolean closed;
@@ -1239,7 +1241,7 @@ private void refreshMeasurementServices() {
// Best-effort close while reloading measurement resources.
}
- measurementResources = PdfMeasurementResources.open(customFontFamilies);
+ measurementResources = BackendProviders.fontMetrics().openMeasurement(customFontFamilies);
}
private void invalidate() {
@@ -1293,10 +1295,10 @@ public NodeRegistry register(NodeDefinition definiti
*
* @return a render unit for this section
*/
- com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend.Section toSectionRenderUnit() {
+ SectionUnit toSectionRenderUnit() {
ensureOpen();
ensureRenderable();
- return new com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend.Section(
+ return new SectionUnit(
layoutGraph(),
canvas,
List.copyOf(customFontFamilies),
@@ -1359,7 +1361,7 @@ public DocumentOutputOptions outputOptions() {
}
@Override
- public PdfFixedLayoutBackend conveniencePdfBackend() {
+ public FixedLayoutRenderer conveniencePdfBackend() {
return chromeOptions.toConveniencePdfBackend(debug);
}
}
diff --git a/src/main/java/com/demcha/compose/document/api/MultiSectionDocument.java b/src/main/java/com/demcha/compose/document/api/MultiSectionDocument.java
index 33e535f04..5ae7f530a 100644
--- a/src/main/java/com/demcha/compose/document/api/MultiSectionDocument.java
+++ b/src/main/java/com/demcha/compose/document/api/MultiSectionDocument.java
@@ -1,7 +1,11 @@
package com.demcha.compose.document.api;
-import com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackend;
+import com.demcha.compose.document.backend.fixed.BackendProviders;
+import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
+import com.demcha.compose.document.backend.fixed.SectionUnit;
import com.demcha.compose.document.exceptions.DocumentRenderingException;
+import com.demcha.compose.document.output.DocumentDebugOptions;
+import com.demcha.compose.document.output.DocumentOutputOptions;
import com.demcha.compose.document.snapshot.LayoutSnapshot;
import java.io.OutputStream;
@@ -43,7 +47,8 @@ public final class MultiSectionDocument implements AutoCloseable {
private final Path defaultOutputFile;
private final List sections;
- private final PdfFixedLayoutBackend backend = new PdfFixedLayoutBackend();
+ private final FixedLayoutRenderer backend =
+ BackendProviders.fixedLayout().create(DocumentOutputOptions.EMPTY, DocumentDebugOptions.none());
private boolean closed;
MultiSectionDocument(Path defaultOutputFile, List sections) {
@@ -143,7 +148,7 @@ public void close() {
}
}
- private List renderUnits() {
+ private List renderUnits() {
ensureOpen();
return sections.stream().map(DocumentSession::toSectionRenderUnit).toList();
}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/BackendProviders.java b/src/main/java/com/demcha/compose/document/backend/fixed/BackendProviders.java
new file mode 100644
index 000000000..468200b61
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/BackendProviders.java
@@ -0,0 +1,76 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.document.exceptions.MissingBackendException;
+
+import java.util.ServiceLoader;
+import java.util.function.Supplier;
+
+/**
+ * Locates the fixed-layout backend service providers registered on the
+ * classpath.
+ *
+ * Since GraphCompose 2.0 the render/measurement backend ships as a separate
+ * artifact discovered through {@link ServiceLoader}. This locator resolves each
+ * provider once, lazily, and caches it. When no provider is registered it throws
+ * {@link MissingBackendException} naming the artifact to add. The explicit
+ * {@code render(FixedLayoutBackend)} / {@code export(SemanticBackend)} paths do
+ * not go through here — they use a caller-supplied backend.
+ *
+ * @since 2.0.0
+ */
+public final class BackendProviders {
+
+ private static final String MISSING_BACKEND_MESSAGE =
+ "No fixed-layout render backend on the classpath: add the "
+ + "io.github.demchaav:graph-compose-render-pdf artifact (or the "
+ + "io.github.demchaav:graph-compose-bundle aggregate, or another "
+ + "provider implementation) to render, rasterize, or measure a document.";
+
+ private static volatile FontMetricsProvider fontMetrics;
+ private static volatile FixedLayoutBackendProvider fixedLayout;
+
+ private BackendProviders() {
+ }
+
+ /**
+ * Returns the registered fixed-layout backend provider, resolving and caching
+ * it on first use.
+ *
+ * @return the fixed-layout backend provider
+ * @throws MissingBackendException if no provider is registered on the classpath
+ */
+ public static FixedLayoutBackendProvider fixedLayout() {
+ FixedLayoutBackendProvider provider = fixedLayout;
+ if (provider == null) {
+ provider = load(FixedLayoutBackendProvider.class);
+ fixedLayout = provider;
+ }
+ return provider;
+ }
+
+ /**
+ * Returns the registered font-metrics provider, resolving and caching it on
+ * first use.
+ *
+ * @return the font-metrics provider
+ * @throws MissingBackendException if no provider is registered on the classpath
+ */
+ public static FontMetricsProvider fontMetrics() {
+ FontMetricsProvider provider = fontMetrics;
+ if (provider == null) {
+ provider = load(FontMetricsProvider.class);
+ fontMetrics = provider;
+ }
+ return provider;
+ }
+
+ private static T load(Class service) {
+ return ServiceLoader.load(service, service.getClassLoader())
+ .findFirst()
+ .orElseThrow(missingBackend());
+ }
+
+ private static Supplier missingBackend() {
+ return () -> new MissingBackendException(MISSING_BACKEND_MESSAGE);
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/FixedLayoutBackendProvider.java b/src/main/java/com/demcha/compose/document/backend/fixed/FixedLayoutBackendProvider.java
new file mode 100644
index 000000000..3ed96c83c
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/FixedLayoutBackendProvider.java
@@ -0,0 +1,38 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.document.output.DocumentDebugOptions;
+import com.demcha.compose.document.output.DocumentOutputOptions;
+
+/**
+ * Service-provider that supplies a fixed-layout render backend to the document
+ * API's convenience output methods without exposing a concrete backend type.
+ *
+ * Implementations are discovered through {@link java.util.ServiceLoader};
+ * each render backend artifact registers one (for example the PDF backend in
+ * {@code io.github.demchaav:graph-compose-render-pdf}). The core resolves a
+ * provider lazily via {@link BackendProviders#fixedLayout()} and fails with a
+ * {@link com.demcha.compose.document.exceptions.MissingBackendException} naming
+ * the artifact to add when none is registered.
+ *
+ * @since 2.0.0
+ */
+public interface FixedLayoutBackendProvider {
+
+ /**
+ * Returns the output format this provider renders, used for diagnostics and
+ * manifests.
+ *
+ * @return a stable format identifier such as {@code "pdf"}
+ */
+ String format();
+
+ /**
+ * Creates a backend configured with the session's chrome and debug overlay.
+ *
+ * @param chrome document-level chrome (metadata, watermark, protection,
+ * repeating headers and footers); never {@code null}
+ * @param debug debug overlay options; never {@code null}
+ * @return a configured, ready-to-use renderer
+ */
+ FixedLayoutRenderer create(DocumentOutputOptions chrome, DocumentDebugOptions debug);
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/FixedLayoutRenderer.java b/src/main/java/com/demcha/compose/document/backend/fixed/FixedLayoutRenderer.java
new file mode 100644
index 000000000..11f095ed5
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/FixedLayoutRenderer.java
@@ -0,0 +1,71 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.document.layout.LayoutGraph;
+
+import java.awt.image.BufferedImage;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * A configured fixed-layout backend ready to render a resolved document — the
+ * backend-neutral surface the document API's convenience output methods drive.
+ *
+ * A {@link FixedLayoutBackendProvider} produces one of these from a session's
+ * chrome, so {@code DocumentSession} and {@code MultiSectionDocument} can render
+ * to bytes, stream, rasterize, or concatenate sections without importing a
+ * concrete backend. It extends {@link FixedLayoutBackend} with the extra
+ * convenience operations the single {@code render(...)} contract does not
+ * cover.
+ *
+ * @since 2.0.0
+ */
+public interface FixedLayoutRenderer extends FixedLayoutBackend {
+
+ /**
+ * Renders one resolved graph and writes the result to the stream carried by
+ * the render context.
+ *
+ * @param graph resolved layout graph
+ * @param context render-pass configuration and stream target
+ * @throws Exception if rendering fails
+ */
+ void write(LayoutGraph graph, FixedLayoutRenderContext context) throws Exception;
+
+ /**
+ * Rasterizes a resolved graph to one image per page (or a single page).
+ *
+ * @param graph resolved layout graph
+ * @param context render-pass configuration
+ * @param dpi raster resolution in dots per inch
+ * @param transparent {@code true} for an alpha channel instead of a white background
+ * @param pageIndex zero-based page to render, or a negative value for all pages
+ * @return one image per rendered page
+ * @throws Exception if rendering or rasterization fails
+ * @throws IndexOutOfBoundsException if {@code pageIndex} is out of range
+ */
+ List renderToImages(LayoutGraph graph,
+ FixedLayoutRenderContext context,
+ int dpi,
+ boolean transparent,
+ int pageIndex) throws Exception;
+
+ /**
+ * Renders several sections concatenated into a single document and returns
+ * the bytes.
+ *
+ * @param sections ordered sections to concatenate
+ * @return rendered document bytes
+ * @throws Exception if rendering fails
+ */
+ byte[] renderSections(List sections) throws Exception;
+
+ /**
+ * Renders several sections concatenated into a single document and streams
+ * the result to the caller-owned stream (not closed here).
+ *
+ * @param sections ordered sections to concatenate
+ * @param output destination stream
+ * @throws Exception if rendering fails
+ */
+ void writeSections(List sections, OutputStream output) throws Exception;
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/FontMetricsProvider.java b/src/main/java/com/demcha/compose/document/backend/fixed/FontMetricsProvider.java
new file mode 100644
index 000000000..c15b8d9e8
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/FontMetricsProvider.java
@@ -0,0 +1,31 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.font.FontFamilyDefinition;
+
+import java.util.Collection;
+
+/**
+ * Service-provider that supplies backend-specific font metrics to the layout
+ * pipeline without exposing backend font types to the document API.
+ *
+ * Implementations are discovered through {@link java.util.ServiceLoader};
+ * each render backend artifact registers one (for example the PDF backend in
+ * {@code io.github.demchaav:graph-compose-render-pdf}). The core resolves a
+ * provider lazily via {@link BackendProviders#fontMetrics()} and fails with a
+ * {@link com.demcha.compose.document.exceptions.MissingBackendException} naming
+ * the artifact to add when none is registered.
+ *
+ * @since 2.0.0
+ */
+public interface FontMetricsProvider {
+
+ /**
+ * Opens measurement resources for the given document-local font families.
+ *
+ * @param customFontFamilies document-local font families to register in
+ * addition to the bundled catalog; never
+ * {@code null} (may be empty)
+ * @return owned, document-scoped measurement resources
+ */
+ MeasurementResources openMeasurement(Collection customFontFamilies);
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/MeasurementResources.java b/src/main/java/com/demcha/compose/document/backend/fixed/MeasurementResources.java
new file mode 100644
index 000000000..218cc8e5d
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/MeasurementResources.java
@@ -0,0 +1,42 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.engine.measurement.TextMeasurementSystem;
+import com.demcha.compose.font.FontLibrary;
+
+/**
+ * Backend-neutral bundle of the font library and text-measurement service the
+ * canonical layout pipeline needs to size text before rendering.
+ *
+ * A {@link FontMetricsProvider} produces these resources for a session. Both
+ * members are backend-neutral types ({@link FontLibrary}, {@link
+ * TextMeasurementSystem}); the concrete font metrics live behind the provider,
+ * so the document API can lay out a document without naming a rendering backend.
+ * The instance is document-scoped and owns any caches it created, released by
+ * {@link #close()}.
+ *
+ * @since 2.0.0
+ */
+public interface MeasurementResources extends AutoCloseable {
+
+ /**
+ * Returns the resolved document-scoped font library.
+ *
+ * @return the font library bound to this session's fonts
+ */
+ FontLibrary fontLibrary();
+
+ /**
+ * Returns the text-measurement service backed by the provider's font metrics.
+ *
+ * @return the measurement service used during layout
+ */
+ TextMeasurementSystem textMeasurementSystem();
+
+ /**
+ * Releases any caches held by the measurement service.
+ *
+ * @throws Exception if the underlying resources fail to close
+ */
+ @Override
+ void close() throws Exception;
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/SectionUnit.java b/src/main/java/com/demcha/compose/document/backend/fixed/SectionUnit.java
new file mode 100644
index 000000000..54c4c06cf
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/SectionUnit.java
@@ -0,0 +1,42 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.document.layout.LayoutCanvas;
+import com.demcha.compose.document.layout.LayoutGraph;
+import com.demcha.compose.font.FontFamilyDefinition;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * One section of a multi-section fixed-layout render: its resolved layout graph,
+ * canvas, document-local fonts, and the configured {@link FixedLayoutRenderer}
+ * that carries the section's own chrome (page size, headers, footers, numbering).
+ *
+ * Sections are concatenated inside a single backend render so navigation and
+ * outlines resolve across section boundaries. This type is backend-neutral —
+ * {@code chrome} is a {@link FixedLayoutRenderer}, not a concrete backend — so
+ * the document API can assemble a multi-section document without naming a
+ * rendering backend.
+ *
+ * @param graph resolved layout graph for this section
+ * @param canvas resolved canvas geometry for this section
+ * @param customFonts document-local font families for this section (never
+ * {@code null}; defensively copied)
+ * @param chrome configured backend supplying this section's chrome
+ * @since 2.0.0
+ */
+public record SectionUnit(LayoutGraph graph,
+ LayoutCanvas canvas,
+ List customFonts,
+ FixedLayoutRenderer chrome) {
+
+ /**
+ * Validates required members and defensively copies the font list.
+ */
+ public SectionUnit {
+ Objects.requireNonNull(graph, "graph");
+ Objects.requireNonNull(canvas, "canvas");
+ Objects.requireNonNull(chrome, "chrome");
+ customFonts = customFonts == null ? List.of() : List.copyOf(customFonts);
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java
index 26a36191a..36a9a013f 100644
--- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java
@@ -1,8 +1,9 @@
package com.demcha.compose.document.backend.fixed.pdf;
import com.demcha.compose.document.api.Beta;
-import com.demcha.compose.document.backend.fixed.FixedLayoutBackend;
import com.demcha.compose.document.backend.fixed.FixedLayoutRenderContext;
+import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
+import com.demcha.compose.document.backend.fixed.SectionUnit;
import com.demcha.compose.document.backend.fixed.pdf.handlers.*;
import com.demcha.compose.document.backend.fixed.pdf.options.*;
import com.demcha.compose.document.exceptions.UnsupportedNodeCapabilityException;
@@ -48,7 +49,7 @@
*
* @author Artem Demchyshyn
*/
-public final class PdfFixedLayoutBackend implements FixedLayoutBackend {
+public final class PdfFixedLayoutBackend implements FixedLayoutRenderer {
private static final Logger RENDER_LOG = LoggerFactory.getLogger("com.demcha.compose.engine.render");
private final Map, PdfFragmentRenderHandler>> handlers;
@@ -255,6 +256,7 @@ public byte[] render(LayoutGraph graph, FixedLayoutRenderContext context) throws
* @param context fixed-layout render configuration with a non-null output stream
* @throws Exception if PDF creation, rendering, or saving fails
*/
+ @Override
public void write(LayoutGraph graph, FixedLayoutRenderContext context) throws Exception {
Objects.requireNonNull(graph, "graph");
Objects.requireNonNull(context, "context");
@@ -318,6 +320,7 @@ private int renderToOutput(LayoutGraph graph, FixedLayoutRenderContext context,
* @throws Exception if PDF creation, rendering, or rasterization fails
* @throws IndexOutOfBoundsException if {@code pageIndex} is out of range
*/
+ @Override
public List renderToImages(LayoutGraph graph,
FixedLayoutRenderContext context,
int dpi,
@@ -412,7 +415,7 @@ private void renderGraph(LayoutGraph graph, PdfRenderEnvironment environment) th
}
/**
- * Concatenates several {@link Section sections} into one PDF and returns its
+ * Concatenates several {@link SectionUnit sections} into one PDF and returns its
* bytes. Each section keeps its own page geometry, fonts, and chrome
* (watermark, header/footer); anchors, links, and bookmarks resolve across
* section boundaries against the combined document.
@@ -420,7 +423,7 @@ private void renderGraph(LayoutGraph graph, PdfRenderEnvironment environment) th
* This is the low-level assembly seam;
* {@link com.demcha.compose.document.api.MultiSectionDocument} (via
* {@link com.demcha.compose.GraphCompose#documents()}) is the public entry
- * point that builds the {@link Section}s for you.
+ * point that builds the {@link SectionUnit}s for you.
*
* @param sections ordered, non-empty list of sections
* @return rendered combined-document bytes
@@ -428,7 +431,8 @@ private void renderGraph(LayoutGraph graph, PdfRenderEnvironment environment) th
* @since 1.9.0
*/
@Beta
- public byte[] renderSections(List sections) throws Exception {
+ @Override
+ public byte[] renderSections(List sections) throws Exception {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
writeSections(sections, output);
return output.toByteArray();
@@ -436,7 +440,7 @@ public byte[] renderSections(List sections) throws Exception {
}
/**
- * Concatenates several {@link Section sections} into one PDF written to the
+ * Concatenates several {@link SectionUnit sections} into one PDF written to the
* caller-owned stream (never closed by the backend).
*
* @param sections ordered, non-empty list of sections
@@ -445,14 +449,15 @@ public byte[] renderSections(List sections) throws Exception {
* @since 1.9.0
*/
@Beta
- public void writeSections(List sections, OutputStream output) throws Exception {
+ @Override
+ public void writeSections(List sections, OutputStream output) throws Exception {
Objects.requireNonNull(output, "output");
try (PDDocument document = buildSectionsDocument(sections)) {
document.save(output);
}
}
- private PDDocument buildSectionsDocument(List sections) throws Exception {
+ private PDDocument buildSectionsDocument(List sections) throws Exception {
Objects.requireNonNull(sections, "sections");
if (sections.isEmpty()) {
throw new IllegalArgumentException("A multi-section document needs at least one section.");
@@ -464,15 +469,16 @@ private PDDocument buildSectionsDocument(List sections) throws Exceptio
List links = new ArrayList<>();
List bookmarks = new ArrayList<>();
int pageOffset = 0;
- for (Section section : sections) {
+ for (SectionUnit section : sections) {
LayoutGraph graph = section.graph();
+ PdfFixedLayoutBackend chrome = (PdfFixedLayoutBackend) section.chrome();
List pages = createPages(document, graph);
try (PdfRenderSession renderSession = new PdfRenderSession(document, pages)) {
// Each section renders with its OWN backend's handlers/debug, but
// records navigation against the combined document via the page offset.
PdfRenderEnvironment environment =
new PdfRenderEnvironment(document, fonts, renderSession, pageOffset);
- section.chrome().renderGraph(graph, environment);
+ chrome.renderGraph(graph, environment);
bookmarks.addAll(environment.bookmarkRecords());
links.addAll(environment.deferredInternalLinks());
environment.anchorDestinations().forEach((name, destination) -> {
@@ -482,7 +488,6 @@ private PDDocument buildSectionsDocument(List sections) throws Exceptio
}
});
}
- PdfFixedLayoutBackend chrome = section.chrome();
PdfDocumentPostProcessor.applySectionChrome(
document,
section.canvas(),
@@ -504,31 +509,32 @@ private PDDocument buildSectionsDocument(List sections) throws Exceptio
}
}
- private static void applyDocumentMetadataAndProtection(PDDocument document, List sections)
+ private static void applyDocumentMetadataAndProtection(PDDocument document, List sections)
throws IOException {
// Metadata, protection, and viewer preferences are document-global in PDF;
// the first section that declares each wins for the combined document.
PdfMetadataOptions metadata = null;
PdfProtectionOptions protection = null;
PdfViewerPreferencesOptions viewerPreferences = null;
- for (Section section : sections) {
+ for (SectionUnit section : sections) {
+ PdfFixedLayoutBackend chrome = (PdfFixedLayoutBackend) section.chrome();
if (metadata == null) {
- metadata = section.chrome().metadataOptions;
+ metadata = chrome.metadataOptions;
}
if (protection == null) {
- protection = section.chrome().protectionOptions;
+ protection = chrome.protectionOptions;
}
if (viewerPreferences == null) {
- viewerPreferences = section.chrome().viewerPreferencesOptions;
+ viewerPreferences = chrome.viewerPreferencesOptions;
}
}
PdfDocumentPostProcessor.applyDocumentMetadataAndProtection(document, metadata, protection);
PdfDocumentPostProcessor.applyViewerPreferences(document, viewerPreferences);
}
- private static List unionCustomFonts(List sections) {
+ private static List unionCustomFonts(List sections) {
Map byName = new LinkedHashMap<>();
- for (Section section : sections) {
+ for (SectionUnit section : sections) {
for (FontFamilyDefinition family : section.customFonts()) {
byName.putIfAbsent(family.name(), family);
}
@@ -536,31 +542,6 @@ private static List unionCustomFonts(List section
return List.copyOf(byName.values());
}
- /**
- * One section of a multi-section document: a fully laid-out graph plus the
- * geometry, fonts, and chrome of the {@link com.demcha.compose.document.api.DocumentSession}
- * that produced it. The {@code chrome} backend carries that section's
- * watermark, header/footer, metadata, and protection options.
- *
- * @param graph the section's resolved layout graph
- * @param canvas the section's page canvas (size + margins)
- * @param customFonts the section's custom font families
- * @param chrome the section's configured backend
- * @since 1.9.0
- */
- @Beta
- public record Section(LayoutGraph graph,
- LayoutCanvas canvas,
- List customFonts,
- PdfFixedLayoutBackend chrome) {
- public Section {
- Objects.requireNonNull(graph, "graph");
- Objects.requireNonNull(canvas, "canvas");
- Objects.requireNonNull(chrome, "chrome");
- customFonts = customFonts == null ? List.of() : List.copyOf(customFonts);
- }
- }
-
private int renderTableRowGroup(List fragments,
int startIndex,
PdfTableRowFragmentRenderHandler handler,
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendProvider.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendProvider.java
new file mode 100644
index 000000000..7e443692c
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendProvider.java
@@ -0,0 +1,56 @@
+package com.demcha.compose.document.backend.fixed.pdf;
+
+import com.demcha.compose.document.backend.fixed.FixedLayoutBackendProvider;
+import com.demcha.compose.document.backend.fixed.FixedLayoutRenderer;
+import com.demcha.compose.document.backend.fixed.pdf.options.PdfHeaderFooterOptions;
+import com.demcha.compose.document.output.DocumentDebugOptions;
+import com.demcha.compose.document.output.DocumentHeaderFooter;
+import com.demcha.compose.document.output.DocumentHeaderFooterZone;
+import com.demcha.compose.document.output.DocumentOutputOptions;
+
+/**
+ * PDFBox-backed {@link FixedLayoutBackendProvider}, registered through
+ * {@code META-INF/services} so the document API can render, rasterize, and
+ * concatenate documents without importing the PDF backend.
+ *
+ * {@link #create} translates the backend-neutral chrome into PDF option types
+ * and assembles a configured {@link PdfFixedLayoutBackend} — the translation that
+ * previously lived in the session's chrome holder.
+ *
+ * @since 2.0.0
+ */
+public final class PdfFixedLayoutBackendProvider implements FixedLayoutBackendProvider {
+
+ /**
+ * Creates the provider. Invoked reflectively by {@link java.util.ServiceLoader}.
+ */
+ public PdfFixedLayoutBackendProvider() {
+ }
+
+ @Override
+ public String format() {
+ return "pdf";
+ }
+
+ @Override
+ public FixedLayoutRenderer create(DocumentOutputOptions chrome, DocumentDebugOptions debug) {
+ if (!debug.enabled() && !chrome.hasAny()) {
+ return new PdfFixedLayoutBackend();
+ }
+ PdfFixedLayoutBackend.Builder builder = PdfFixedLayoutBackend.builder()
+ .debug(debug)
+ .metadata(PdfOutputOptionsTranslator.toPdf(chrome.metadata()))
+ .watermark(PdfOutputOptionsTranslator.toPdf(chrome.watermark()))
+ .protect(PdfOutputOptionsTranslator.toPdf(chrome.protection()))
+ .viewerPreferences(PdfOutputOptionsTranslator.toPdf(chrome.viewerPreferences()));
+ for (DocumentHeaderFooter entry : chrome.headersAndFooters()) {
+ PdfHeaderFooterOptions translated = PdfOutputOptionsTranslator.toPdf(entry);
+ if (entry.getZone() == DocumentHeaderFooterZone.FOOTER) {
+ builder.footer(translated);
+ } else {
+ builder.header(translated);
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFontMetricsProvider.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFontMetricsProvider.java
new file mode 100644
index 000000000..a034b7fd7
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFontMetricsProvider.java
@@ -0,0 +1,28 @@
+package com.demcha.compose.document.backend.fixed.pdf;
+
+import com.demcha.compose.document.backend.fixed.FontMetricsProvider;
+import com.demcha.compose.document.backend.fixed.MeasurementResources;
+import com.demcha.compose.font.FontFamilyDefinition;
+
+import java.util.Collection;
+
+/**
+ * PDFBox-backed {@link FontMetricsProvider}, registered through
+ * {@code META-INF/services} so the core layout pipeline can obtain text metrics
+ * without importing the PDF backend.
+ *
+ * @since 2.0.0
+ */
+public final class PdfFontMetricsProvider implements FontMetricsProvider {
+
+ /**
+ * Creates the provider. Invoked reflectively by {@link java.util.ServiceLoader}.
+ */
+ public PdfFontMetricsProvider() {
+ }
+
+ @Override
+ public MeasurementResources openMeasurement(Collection customFontFamilies) {
+ return PdfMeasurementResources.open(customFontFamilies);
+ }
+}
diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfMeasurementResources.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfMeasurementResources.java
index 44797bbce..a8aedaae0 100644
--- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfMeasurementResources.java
+++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfMeasurementResources.java
@@ -1,5 +1,6 @@
package com.demcha.compose.document.backend.fixed.pdf;
+import com.demcha.compose.document.backend.fixed.MeasurementResources;
import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem;
import com.demcha.compose.engine.measurement.TextMeasurementSystem;
import com.demcha.compose.engine.render.pdf.PdfFont;
@@ -15,7 +16,7 @@
* PDFBox classes directly. The implementation remains in the PDF backend layer
* until GraphCompose has a backend-neutral font measurement implementation.
*/
-public final class PdfMeasurementResources implements AutoCloseable {
+public final class PdfMeasurementResources implements MeasurementResources {
private final FontLibrary fontLibrary;
private final TextMeasurementSystem textMeasurementSystem;
@@ -47,6 +48,7 @@ public static PdfMeasurementResources open(Collection cust
*
* @return document-scoped fonts
*/
+ @Override
public FontLibrary fontLibrary() {
return fontLibrary;
}
@@ -56,6 +58,7 @@ public FontLibrary fontLibrary() {
*
* @return text measurement service
*/
+ @Override
public TextMeasurementSystem textMeasurementSystem() {
return textMeasurementSystem;
}
diff --git a/src/main/java/com/demcha/compose/document/exceptions/MissingBackendException.java b/src/main/java/com/demcha/compose/document/exceptions/MissingBackendException.java
new file mode 100644
index 000000000..9ebbf9be8
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/exceptions/MissingBackendException.java
@@ -0,0 +1,33 @@
+package com.demcha.compose.document.exceptions;
+
+/**
+ * Raised when a document operation needs a fixed-layout render/measurement
+ * backend but none is registered on the classpath.
+ *
+ * Since GraphCompose 2.0 the engine and document model ship without a bundled
+ * backend: rendering and text measurement are provided by a separate artifact
+ * (for example {@code io.github.demchaav:graph-compose-render-pdf}) discovered at
+ * runtime through {@link java.util.ServiceLoader}. Calling a convenience output
+ * method — {@code toPdfBytes()}, {@code buildPdf()}, {@code toImages()} — or
+ * requesting {@code layoutSnapshot()} without such an artifact on the classpath
+ * fails with this exception. The message names the artifact to add.
+ *
+ * This is an unchecked exception: a missing backend is a build/classpath
+ * configuration problem, not a per-call argument error, so user code is not
+ * forced to catch it. The explicit {@code render(FixedLayoutBackend)} /
+ * {@code export(SemanticBackend)} paths never throw it — they use a
+ * caller-supplied backend instead of the ServiceLoader lookup.
+ *
+ * @since 2.0.0
+ */
+public class MissingBackendException extends IllegalStateException {
+
+ /**
+ * Creates a missing-backend exception.
+ *
+ * @param message diagnostic message naming the artifact to add
+ */
+ public MissingBackendException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/demcha/compose/engine/core/EntityManager.java b/src/main/java/com/demcha/compose/engine/core/EntityManager.java
index 2dbd3c64b..8fb2345fd 100644
--- a/src/main/java/com/demcha/compose/engine/core/EntityManager.java
+++ b/src/main/java/com/demcha/compose/engine/core/EntityManager.java
@@ -1,6 +1,5 @@
package com.demcha.compose.engine.core;
-import com.demcha.compose.font.DefaultFonts;
import com.demcha.compose.font.FontLibrary;
import com.demcha.compose.engine.components.core.Component;
import com.demcha.compose.engine.components.core.Entity;
@@ -66,7 +65,7 @@ public EntityManager() {
}
public EntityManager(@NonNull List systems) {
- this(systems, DefaultFonts.standardLibrary(), true);
+ this(systems, new FontLibrary(), true);
}
public EntityManager(@NonNull List systems, FontLibrary fonts, boolean markdown) {
@@ -81,7 +80,7 @@ public EntityManager(@NonNull List systems, FontLibrary fonts, boolea
}
public EntityManager(boolean markdown) {
- this(new ArrayList<>(), DefaultFonts.standardLibrary(), markdown);
+ this(new ArrayList<>(), new FontLibrary(), markdown);
}
public EntityManager(FontLibrary fonts, boolean markdown) {
diff --git a/src/main/java/com/demcha/compose/font/DefaultFonts.java b/src/main/java/com/demcha/compose/font/DefaultFonts.java
index 66223c07f..88c91ecd9 100644
--- a/src/main/java/com/demcha/compose/font/DefaultFonts.java
+++ b/src/main/java/com/demcha/compose/font/DefaultFonts.java
@@ -1,7 +1,5 @@
package com.demcha.compose.font;
-import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory;
-
import java.util.ArrayList;
import java.util.List;
@@ -65,15 +63,6 @@ public class DefaultFonts {
private DefaultFonts() {
}
- /**
- * Creates a PDF-backed font library containing the standard 14 families.
- *
- * @return font library with standard fonts
- */
- public static FontLibrary standardLibrary() {
- return PdfFontLibraryFactory.standardLibrary();
- }
-
/**
* Returns bundled font family definitions.
*
diff --git a/src/main/resources/META-INF/services/com.demcha.compose.document.backend.fixed.FixedLayoutBackendProvider b/src/main/resources/META-INF/services/com.demcha.compose.document.backend.fixed.FixedLayoutBackendProvider
new file mode 100644
index 000000000..397e39671
--- /dev/null
+++ b/src/main/resources/META-INF/services/com.demcha.compose.document.backend.fixed.FixedLayoutBackendProvider
@@ -0,0 +1 @@
+com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackendProvider
diff --git a/src/main/resources/META-INF/services/com.demcha.compose.document.backend.fixed.FontMetricsProvider b/src/main/resources/META-INF/services/com.demcha.compose.document.backend.fixed.FontMetricsProvider
new file mode 100644
index 000000000..192791096
--- /dev/null
+++ b/src/main/resources/META-INF/services/com.demcha.compose.document.backend.fixed.FontMetricsProvider
@@ -0,0 +1 @@
+com.demcha.compose.document.backend.fixed.pdf.PdfFontMetricsProvider
diff --git a/src/test/java/com/demcha/compose/document/backend/fixed/BackendProvidersTest.java b/src/test/java/com/demcha/compose/document/backend/fixed/BackendProvidersTest.java
new file mode 100644
index 000000000..58bc65a8f
--- /dev/null
+++ b/src/test/java/com/demcha/compose/document/backend/fixed/BackendProvidersTest.java
@@ -0,0 +1,35 @@
+package com.demcha.compose.document.backend.fixed;
+
+import com.demcha.compose.document.backend.fixed.pdf.PdfFixedLayoutBackendProvider;
+import com.demcha.compose.document.backend.fixed.pdf.PdfFontMetricsProvider;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Pins the ServiceLoader wiring of the backend seam: in a single-jar build the
+ * bundled PDF providers must be discoverable through {@link BackendProviders},
+ * and each must produce usable output. This is the explicit check behind the
+ * transitive coverage the render/measurement suites give the seam.
+ */
+class BackendProvidersTest {
+
+ @Test
+ void resolvesThePdfFontMetricsProviderAndOpensMeasurementResources() throws Exception {
+ FontMetricsProvider provider = BackendProviders.fontMetrics();
+ assertThat(provider).isInstanceOf(PdfFontMetricsProvider.class);
+ try (MeasurementResources resources = provider.openMeasurement(List.of())) {
+ assertThat(resources.fontLibrary()).isNotNull();
+ assertThat(resources.textMeasurementSystem()).isNotNull();
+ }
+ }
+
+ @Test
+ void resolvesThePdfFixedLayoutBackendProvider() {
+ FixedLayoutBackendProvider provider = BackendProviders.fixedLayout();
+ assertThat(provider).isInstanceOf(PdfFixedLayoutBackendProvider.class);
+ assertThat(provider.format()).isEqualTo("pdf");
+ }
+}
diff --git a/src/test/java/com/demcha/compose/engine/measurement/FontLibraryTextMeasurementSystemTest.java b/src/test/java/com/demcha/compose/engine/measurement/FontLibraryTextMeasurementSystemTest.java
index 30c5d7313..4350127ac 100644
--- a/src/test/java/com/demcha/compose/engine/measurement/FontLibraryTextMeasurementSystemTest.java
+++ b/src/test/java/com/demcha/compose/engine/measurement/FontLibraryTextMeasurementSystemTest.java
@@ -4,8 +4,8 @@
import com.demcha.compose.engine.components.geometry.ContentSize;
import com.demcha.compose.engine.font.FontBase;
import com.demcha.compose.engine.font.FontLineMetrics;
+import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory;
import com.demcha.compose.engine.render.pdf.PdfFont;
-import com.demcha.compose.font.DefaultFonts;
import com.demcha.compose.font.FontLibrary;
import com.demcha.compose.font.FontName;
import org.junit.jupiter.api.Test;
@@ -36,7 +36,7 @@ void shouldNotKeepUserTextInStaticWidthCache() {
@Test
void clearCachesShouldDiscardSessionTextWidthCache() {
FontLibraryTextMeasurementSystem measurement = new FontLibraryTextMeasurementSystem(
- DefaultFonts.standardLibrary(),
+ PdfFontLibraryFactory.standardLibrary(),
PdfFont.class);
assertThat(measurement.sessionTextWidthCacheSize()).isZero();
@@ -56,7 +56,7 @@ void resolvesBackendLineMetricsPolymorphicallyWithoutPdfSpecialCase() {
// A backend font that is NOT a PdfFont but supplies first-class metrics by
// overriding Font#lineMetrics. The shared measurement system must honour
// them via the contract, with no instanceof PdfFont fast-path.
- FontLibrary library = DefaultFonts.standardLibrary();
+ FontLibrary library = PdfFontLibraryFactory.standardLibrary();
library.addFont(FontName.HELVETICA, FirstClassMetricsFont.class, new FirstClassMetricsFont());
FontLibraryTextMeasurementSystem measurement =
new FontLibraryTextMeasurementSystem(library, FirstClassMetricsFont.class);
@@ -76,7 +76,7 @@ void resolvesBackendLineMetricsPolymorphicallyWithoutPdfSpecialCase() {
void defaultLineMetricsDeriveFromLineHeightWithZeroDescentAndLeading() {
// A backend font that does NOT override Font#lineMetrics falls back to the
// contract default: ascent = line height, descent = leading = 0.
- FontLibrary library = DefaultFonts.standardLibrary();
+ FontLibrary library = PdfFontLibraryFactory.standardLibrary();
library.addFont(FontName.HELVETICA, DefaultMetricsFont.class, new DefaultMetricsFont(20.0));
FontLibraryTextMeasurementSystem measurement =
new FontLibraryTextMeasurementSystem(library, DefaultMetricsFont.class);
@@ -94,7 +94,7 @@ void defaultLineMetricsDeriveFromLineHeightWithZeroDescentAndLeading() {
void globalMetricsCacheIsNamespacedByBackendFontType() {
// Two different backend font types that return the SAME measurementCacheKey
// must not collide in the process-wide cache — the multi-backend invariant.
- FontLibrary library = DefaultFonts.standardLibrary();
+ FontLibrary library = PdfFontLibraryFactory.standardLibrary();
library.addFont(FontName.HELVETICA, BackendAFont.class, new BackendAFont());
library.addFont(FontName.HELVETICA, BackendBFont.class, new BackendBFont());
FontLibraryTextMeasurementSystem a = new FontLibraryTextMeasurementSystem(library, BackendAFont.class);
diff --git a/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java b/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java
index f41b38a85..aebbcb290 100644
--- a/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java
+++ b/src/test/java/com/demcha/compose/engine/render/pdf/ecs/handlers/PdfTableRowRenderHandlerTest.java
@@ -1,6 +1,6 @@
package com.demcha.compose.engine.render.pdf.ecs.handlers;
-import com.demcha.compose.font.DefaultFonts;
+import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory;
import com.demcha.compose.engine.components.content.table.TableCellLayoutStyle;
import com.demcha.compose.engine.components.content.shape.Side;
import com.demcha.compose.engine.components.content.shape.Stroke;
@@ -105,7 +105,7 @@ void shouldKeepSingleSeparatorWhenRowsStayOnSamePage() {
@Test
void shouldResolveTopAndMiddleAnchorsForMultilineContent() {
- PdfFont font = DefaultFonts.standardLibrary()
+ PdfFont font = PdfFontLibraryFactory.standardLibrary()
.getFont(TextStyle.DEFAULT_STYLE.fontName(), PdfFont.class)
.orElseThrow();
@@ -148,7 +148,7 @@ void shouldResolveTopAndMiddleAnchorsForMultilineContent() {
@Test
void shouldApplyCellLineSpacingWhenResolvingMultilineContent() {
- PdfFont font = DefaultFonts.standardLibrary()
+ PdfFont font = PdfFontLibraryFactory.standardLibrary()
.getFont(TextStyle.DEFAULT_STYLE.fontName(), PdfFont.class)
.orElseThrow();
double lineSpacing = 2.5;
diff --git a/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java b/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java
index e81b4664a..ae13c181a 100644
--- a/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java
+++ b/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java
@@ -73,7 +73,7 @@ void shouldMaterializeStandard14FontsWithoutBundledFontsArtifact() {
// The standard-14 families embed nothing and need no PDF document and no
// graph-compose-fonts jar — this is the baseline an engine-only consumer
// (no bundled fonts on the classpath) still gets.
- FontLibrary standard = DefaultFonts.standardLibrary();
+ FontLibrary standard = PdfFontLibraryFactory.standardLibrary();
assertThat(standard.getFont(FontName.HELVETICA, PdfFont.class)).isPresent();
assertThat(standard.getFont(FontName.TIMES_ROMAN, PdfFont.class)).isPresent();
diff --git a/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java b/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java
index 3effe5158..3a2d1ccb2 100644
--- a/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java
+++ b/src/test/java/com/demcha/compose/testsupport/engine/assembly/BlockTextBuilderTest.java
@@ -16,6 +16,7 @@
import com.demcha.compose.engine.core.EntityManager;
import com.demcha.compose.engine.render.pdf.PdfFont;
import com.demcha.compose.engine.measurement.TextMeasurementSystem;
+import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory;
import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -30,7 +31,7 @@ class BlockTextBuilderTest {
@BeforeEach
void setUp() {
- entityManager = new EntityManager();
+ entityManager = new EntityManager(PdfFontLibraryFactory.standardLibrary(), false);
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
entityManager.setMarkdown(true);
}
diff --git a/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java b/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java
index 258a4d0a1..e769d3d98 100644
--- a/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java
+++ b/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java
@@ -1,6 +1,7 @@
package com.demcha.compose.testsupport.engine.assembly;
import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory;
import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem;
import com.demcha.compose.engine.components.content.table.TableLayoutData;
import com.demcha.compose.engine.components.content.table.TableResolvedCell;
@@ -284,7 +285,7 @@ void shouldIncludeCellLineSpacingInMultilineCellHeight() throws Exception {
@Test
void shouldBuildWithoutLayoutSystemWhenTextMeasurementSystemIsRegistered() {
- EntityManager entityManager = new EntityManager();
+ EntityManager entityManager = new EntityManager(PdfFontLibraryFactory.standardLibrary(), false);
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
Entity table = new TableBuilder(entityManager)
diff --git a/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java b/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java
index 10ad24f44..47f2d6816 100644
--- a/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java
+++ b/src/test/java/com/demcha/compose/testsupport/engine/assembly/TextBuilderTest.java
@@ -1,5 +1,6 @@
package com.demcha.compose.testsupport.engine.assembly;
+import com.demcha.compose.document.backend.fixed.pdf.PdfFontLibraryFactory;
import com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem;
import com.demcha.compose.engine.components.content.text.TextStyle;
import com.demcha.compose.engine.components.core.Entity;
@@ -20,7 +21,7 @@ class TextBuilderTest {
@BeforeEach
void setUp() {
- entityManager = new EntityManager();
+ entityManager = new EntityManager(PdfFontLibraryFactory.standardLibrary(), false);
entityManager.getSystems().registerTextMeasurement(new FontLibraryTextMeasurementSystem(entityManager.getFonts(), PdfFont.class));
}
diff --git a/src/test/java/com/demcha/documentation/PdfBackendIsolationGuardTest.java b/src/test/java/com/demcha/documentation/PdfBackendIsolationGuardTest.java
index 8e4989ab9..fca159fb5 100644
--- a/src/test/java/com/demcha/documentation/PdfBackendIsolationGuardTest.java
+++ b/src/test/java/com/demcha/documentation/PdfBackendIsolationGuardTest.java
@@ -28,6 +28,10 @@ class PdfBackendIsolationGuardTest {
private static final Path PROJECT_ROOT = Path.of("").toAbsolutePath().normalize();
private static final String PDFBOX_IMPORT_PREFIX = "import org.apache.pdfbox.";
private static final String PDFBOX_REFERENCE = "org.apache.pdfbox.";
+ private static final String DOCUMENT_IMPORT_PREFIX = "import com.demcha.compose.document.";
+
+ private static final Path FONT_ROOT =
+ PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/font");
private static final List CANONICAL_ROOTS = List.of(
PROJECT_ROOT.resolve("src/main/java/com/demcha/compose/GraphCompose.java"),
@@ -68,6 +72,49 @@ void pdfboxShouldStayOutOfCanonicalAndNonPdfBackendContracts() throws IOExceptio
.isEmpty();
}
+ @Test
+ void fontCatalogStaysDocumentFree() throws IOException {
+ // compose.font is the logical font catalog: it names fonts, it never loads
+ // them. It must import nothing from the document layer (backends, options,
+ // api) — the machine-checkable form of "the engine -> font -> document
+ // cycle is broken". Backends materialize fonts behind the seam
+ // (FontMetricsProvider / PdfFontLibraryFactory); the catalog only stores them.
+ Map> violations = new LinkedHashMap<>();
+ try (var stream = Files.walk(FONT_ROOT)) {
+ List files = stream.filter(Files::isRegularFile)
+ .filter(p -> p.toString().endsWith(".java"))
+ .toList();
+ for (Path file : files) {
+ Set references = documentImportsIn(file);
+ if (!references.isEmpty()) {
+ violations.put(relative(file), references);
+ }
+ }
+ }
+
+ assertThat(violations)
+ .describedAs("The com.demcha.compose.font catalog must not import "
+ + "com.demcha.compose.document.* — it names logical fonts and never "
+ + "loads them. A render backend (e.g. graph-compose-render-pdf) "
+ + "materializes fonts behind the FontMetricsProvider / "
+ + "PdfFontLibraryFactory seam.")
+ .isEmpty();
+ }
+
+ private Set documentImportsIn(Path file) throws IOException {
+ Set references = new TreeSet<>();
+ for (String line : Files.readAllLines(file)) {
+ String trimmed = line.trim();
+ if (trimmed.startsWith(DOCUMENT_IMPORT_PREFIX)) {
+ int semicolon = trimmed.indexOf(';');
+ if (semicolon >= 0) {
+ references.add(trimmed.substring("import ".length(), semicolon).trim());
+ }
+ }
+ }
+ return references;
+ }
+
private List canonicalJavaFiles() throws IOException {
List files = new ArrayList<>();
for (Path root : CANONICAL_ROOTS) {