Skip to content

feat(api): resolve rendering and text metrics through a ServiceLoader backend seam#297

Merged
DemchaAV merged 4 commits into
2.0-devfrom
feat/2.0-backend-seam
Jul 5, 2026
Merged

feat(api): resolve rendering and text metrics through a ServiceLoader backend seam#297
DemchaAV merged 4 commits into
2.0-devfrom
feat/2.0-backend-seam

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Why

The 2.0 module split needs a lean core that can render and measure text without compiling against a concrete backend — otherwise graph-compose cannot be separated from the PDF backend. This PR inverts the last hard couplings in the document API onto a runtime seam and, in doing so, finishes breaking the engine → font → document package cycle.

What changed

Two ServiceLoader SPIs in core (document.backend.fixed), resolved lazily and cached by BackendProviders, each failing with MissingBackendException (naming io.github.demchaav:graph-compose-render-pdf) when no provider is on the classpath:

  • FontMetricsProviderMeasurementResources (backend-neutral FontLibrary + TextMeasurementSystem). DocumentSession now opens measurement through it instead of importing PdfMeasurementResources.
  • FixedLayoutBackendProviderFixedLayoutRenderer (render / stream-write / rasterize / section-render). DocumentChromeOptions, MultiSectionDocument, DocumentRenderingFacade, and DocumentSession resolve the backend through it; the multi-section input is the neutral SectionUnit.

The PDF backend registers PdfFontMetricsProvider + PdfFixedLayoutBackendProvider through META-INF/services; PdfFixedLayoutBackend implements FixedLayoutRenderer, and the chrome→PDF-option translation moved into its provider. render(FixedLayoutBackend) / export(SemanticBackend) remain the explicit, ServiceLoader-free paths.

Cycle break: DefaultFonts.standardLibrary() is removed (its last document import), EntityManager's defaulting constructors start from an empty FontLibrary, and com.demcha.compose.font now imports nothing from com.demcha.compose.document — frozen by a new PdfBackendIsolationGuardTest.fontCatalogStaysDocumentFree check. The font catalog becomes a pure logical registry: it names fonts, backends load them.

Verification

  • ./mvnw clean verify -pl . — green. Every layout snapshot and visual baseline passes with zero updates, so rendering, rasterization, and multi-section assembly stay behaviour- and pixel-identical; the single-jar build always discovers the PDF providers.
  • ./mvnw javadoc:javadoc -pl . — clean under doclint=all; all new public types carry @since 2.0.0 and full Javadoc.
  • New BackendProvidersTest pins the ServiceLoader resolution of both providers; the new guard freezes compose.font's document-freedom (bite-verified).
  • Hot path unchanged: BackendProviders resolves each provider once and caches it — no ServiceLoader lookup per render or per node.

The no-backend path of MissingBackendException becomes exercisable once the PDF backend is a separately installable artifact; in the single jar it is satisfied by construction.

DemchaAV added 4 commits July 5, 2026 14:24
…finish the font/document decoupling

Introduce a ServiceLoader-based font-metrics seam so the document API measures
text without naming the PDF backend, and cut the last font->document edge.

- New core SPI (com.demcha.compose.document.backend.fixed): FontMetricsProvider
  returns MeasurementResources (FontLibrary + TextMeasurementSystem, both
  backend-neutral); BackendProviders resolves it lazily via ServiceLoader and
  throws MissingBackendException (naming graph-compose-render-pdf) when absent.
- PDF side: PdfFontMetricsProvider (registered in META-INF/services) delegates to
  the existing PdfMeasurementResources, which now implements MeasurementResources.
  DocumentSession opens measurement through the provider instead of importing
  PdfMeasurementResources.
- DefaultFonts drops standardLibrary() (its last PdfFontLibraryFactory import);
  EntityManager's defaulting constructors now start from an empty FontLibrary
  instead of a PDF-backed standard set. compose.font is now free of any
  com.demcha.compose.document import — the engine->font->document cycle is broken.
- Tests that need the standard families call PdfFontLibraryFactory.standardLibrary()
  directly. A new guard (fontCatalogStaysDocumentFree) freezes the invariant.

Single-jar builds always discover the PDF provider, so measurement and layout are
behaviour-identical; every layout snapshot passes with zero baseline updates.
Introduce the backend-neutral surface the document API's convenience output
methods will drive, ahead of rewiring them off the concrete PDF backend:

- FixedLayoutRenderer extends FixedLayoutBackend<byte[]> with the convenience
  operations the single render(...) contract does not cover: write(...),
  renderToImages(...), renderSections(...), writeSections(...).
- SectionUnit is the backend-neutral multi-section input; its chrome is a
  FixedLayoutRenderer rather than a concrete backend.
- FixedLayoutBackendProvider (ServiceLoader SPI) creates a FixedLayoutRenderer
  from a session's DocumentOutputOptions + DocumentDebugOptions.

No behaviour change yet: these types are not wired in until the PDF backend
implements FixedLayoutRenderer and the convenience paths resolve the provider.
… a ServiceLoader seam

Wire the document API's convenience output methods onto the neutral render seam
so they no longer import the concrete PDF backend.

- FixedLayoutBackendProvider (ServiceLoader) creates a FixedLayoutRenderer from a
  session's DocumentOutputOptions + DocumentDebugOptions; BackendProviders gains
  a fixedLayout() locator alongside fontMetrics().
- PdfFixedLayoutBackend now implements FixedLayoutRenderer; its multi-section
  API takes the neutral SectionUnit (downcasting the section chrome back to the
  PDF backend it owns), and the PDF-only Section record is removed.
- PdfFixedLayoutBackendProvider (registered in META-INF/services) absorbs the
  chrome -> PDF-option translation that lived in DocumentChromeOptions.
- DocumentChromeOptions, MultiSectionDocument, DocumentSession, and
  DocumentRenderingFacade resolve the backend through the provider and expose
  FixedLayoutRenderer / SectionUnit instead of PdfFixedLayoutBackend.

Single-jar builds discover the PDF provider, so rendering, rasterization, and
multi-section assembly are behaviour-identical: every layout snapshot and visual
baseline passes with zero updates.
…impls

Add BackendProvidersTest asserting the bundled PDF providers resolve through
BackendProviders (font-metrics + fixed-layout) and produce usable output — the
explicit check behind the seam's transitive render/measurement coverage.

Mark PdfFixedLayoutBackend.write / renderToImages / renderSections / writeSections
@OverRide now that they implement FixedLayoutRenderer.
@DemchaAV DemchaAV merged commit dbe7c1a into 2.0-dev Jul 5, 2026
11 checks passed
@DemchaAV DemchaAV deleted the feat/2.0-backend-seam branch July 5, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant