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) {