In an earlier post about GraphCompose I described how I borrowed ideas from unrelated corners of software development and tried to apply them to document generation: a declarative DSL, a separate layout pass, snapshot tests, and a bit of ECS thinking.

Back then GraphCompose mostly looked like a PDF engine. That was fair. The only real backend was PDF, PDFBox did the work underneath, and support for other formats lived somewhere between the roadmap and the classic developer promise of yes, I’ll definitely get to that later. But I never set out to build another library that can only produce PDF. The original idea was different:

A developer should describe a document in one language. The engine should resolve structure, sizes, line breaks and coordinates once. And the specific format should only be a way of drawing a result that already exists.

Which means PDFBox should not be the architecture.

Apache POI should not be the architecture either.

They should be backends.

Normally, every format makes you start over

Take an ordinary task: produce a business report. It contains

  • a heading;
  • a few KPIs;
  • a table;
  • a chart;
  • explanatory text;
  • a footer;
  • links;
  • possibly several pages.

If we build the PDF through PDFBox, we have to learn its model:

contentStream.beginText();
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();

If the same report is then wanted in PowerPoint, a new life begins:

XSLFTextBox textBox = slide.createTextBox();
textBox.setAnchor(rectangle);
textBox.setText(text);

Now we have different classes, different units, DrawingML, relationships inside an OPC container, PowerPoint-specific quirks, and a few more evenings where the documentation looks at you as if the problem were obvious to everyone but you.

For SVG there will be a third API.

For an image, a fourth.

For some future format, a fifth.

But the application-level task has not changed at all. We still want to say:

Here is a heading. Below it, a table. Next to it, a chart. If a block doesn’t fit, move it. And don’t leave a section title stranded alone at the bottom of a page.

Low-level libraries are good at producing a specific format. They are not obliged to solve the whole document-layout problem for us. The trouble starts when we mix the two levels.

If every backend computes layout, you have several engines

Suppose I want to support PDF and PowerPoint. I could write two separate renderers:

Document model
    ├── PDF renderer: measures, wraps, places
    └── PPTX renderer: measures, wraps, places

At first glance this is fine. After a while, PDF and PowerPoint start drifting apart. In one format the text wrapped to the next line; in the other it did not. In one the table fitted on the page; in the other it moved.

In the PDF the heading stayed with its first paragraph, while in PowerPoint it ended up on one slide with its content already living on the next. You can fix each case separately. Then one more. Then add a condition for a specific font. Then write PptxTableLayoutFixFinal2. And at some point discover that you do not have two backends. You have two document engines that happen to share a similar public API.

That is precisely what I wanted to avoid.

One language for describing a document

A GraphCompose user does not work with PDF operators or PowerPoint shapes. They describe the document itself:

document.pageFlow(page -> page
        .addSection("Quarterly Results", section -> section
                .keepWithNext()
                .addParagraph(p -> p
                        .text("Revenue increased by 18%.")
                        .textStyle(bodyStyle))
                .addTable(table -> buildResultsTable(table))));

There is no

  • PDPageContentStream;
  • XSLFSlide;
  • DrawingML;
  • PDF operator;
  • text coordinate;
  • line-height calculation;
  • manual table splitting.

What there is, is document structure and the author’s intent. That is the language a developer should interact with. It describes:

section
paragraph
table
image
chart
shape
spacing
alignment
pagination rules

And the engine decides how all of it is physically placed.

The coordinates did not disappear

Declarative APIs are sometimes presented as if coordinates were no longer necessary. That is obviously untrue. Coordinates are always necessary. PDF uses coordinates. PowerPoint uses coordinates. SVG uses coordinates. An image, in the end, is also made of pixels which for some reason refuse to work out on their own where the heading belongs.

The only difference is who does the arithmetic. In GraphCompose, the engine does:

Document DSL
Semantic document tree
Text measurement
Layout
Pagination
Resolved coordinates
LayoutGraph

After that stage the document is already placed. The engine knows

  • page sizes;
  • the position of every element;
  • block widths and heights;
  • text baselines;
  • table boundaries;
  • the results of line wrapping;
  • draw order;
  • clipping regions;
  • element continuations after a page break.

The backend does not get to ask:

Where should this paragraph go?

It receives an answer:

Here is the paragraph. Here are its coordinates. Here are its dimensions. Here are the measured lines. Now express that in your format.

That is where the architectural boundary sits.

A backend is a translator, not a second author

Simplified, a fixed-layout backend does roughly this:

Resolved paragraph → native text object
Resolved image     → native image object
Resolved line      → native line
Resolved path      → native vector path
Resolved link      → native hyperlink
Resolved table     → fills, borders and text fragments

It translates the GraphCompose intermediate representation into the primitives of a specific format. The PDF backend uses PDFBox. The PPTX backend uses Apache POI. A future SVG backend could emit XML elements. Some backend for a format that does not exist yet will use a library nobody has written and then rewritten three times.

But the document language stays the same. The engine stays the same. The layout stays the same. Only the final translation changes.

PowerPoint is not the headline feature

GraphCompose recently gained a PPTX backend. It interests me not because the README can now carry one more badge. PowerPoint was the first serious test of the idea. Before it, I could say:

The architecture doesn’t depend on PDF. In theory, another fixed-layout backend could be plugged in.

In theory is a very convenient phrase. It lets an architecture stay beautiful right up until it meets reality.

Now a single DocumentSession can produce two results:

Path pdf = Path.of("report.pdf");
Path pptx = Path.of("report.pptx");

try (DocumentSession document = GraphCompose.document(pdf)
        .pageSize(DocumentPageSize.SLIDE_16_9)
        .create()) {

    composeReport(document);

    document.buildPdf();
    document.buildPptx(pptx);
}

composeReport(document) is called once. The engine measures the content once. It computes layout once. It makes placement decisions once. Then the PDF backend and the PPTX backend receive the same resolved layout graph. PowerPoint here is not a new document model. It is a different compilation target.

GraphCompose DSL
Semantic document model
Measurement + layout + pagination
Resolved LayoutGraph
        ├── PDF backend  → PDFBox     → PDF
        └── PPTX backend → Apache POI → editable PPTX

Everything above LayoutGraph has no idea whether the result will be PDF, PowerPoint, or some other fixed-layout format. The backend receives geometry that has already been computed and translates it into the primitives of its library. I wrote about how that landed as a release in GraphCompose 2.1: One Document, Two Real Outputs.

The result has to stay native

The simplest way to support PowerPoint is to render the page as an image and stretch it across the slide. It looks identical. Task closed. You can go for coffee and pretend that a .pptx extension automatically means a presentation.

But the user receives a picture:

  • the text cannot be properly selected;
  • the text cannot be copied;
  • a typo cannot be quickly fixed;
  • a block cannot be moved;
  • a panel colour cannot be changed;
  • links inside the content are lost;
  • scaling depends on raster resolution.

I wanted a different result. If GraphCompose has a paragraph, the PPTX backend creates a text frame. If there is a panel, it creates a shape. If there is a line, a native line. If there is a path or a polygon, vector geometry. If there is a hyperlink, it has to become a PowerPoint hyperlink. What comes out should not be a photograph of a document, but the document itself, expressed in the target format’s own terms.

The same document rendered as a PDF page and as a PowerPoint slide, with a close-up showing the slide text selected as an editable text frame

One DocumentSession, one resolved LayoutGraph, two native outputs. Above, the same document as PDF and as PPTX. Below, you can see that the text in PowerPoint is still an ordinary editable text frame rather than part of an image. Open the PDF · Download the PPTX · View the source.

This does not mean every element will be a hundred per cent editable in every format. But a native result is the default goal, not a pleasant bonus.

What happens to charts

Charts are a good example of why a shared primitive layer matters. I could have written a PdfChartRenderer. Then a separate PptxChartRenderer. Then a separate renderer for every subsequent backend. After a while, half the project would be busy drawing the same bars in slightly different ways.

In GraphCompose a chart turns into ordinary engine primitives during composition:

  • rectangles;
  • lines;
  • paths;
  • text;
  • fills;
  • gradients;
  • groups.

The backend never sees a special “bar chart” object. It sees resolved vector geometry.

If the data looks like this:

Product A — 20%
Product B — 35%
Product C — 45%

the engine computes the real bar proportions. At generation time, the visualisation matches the data. In PowerPoint the user gets editable shapes and text rather than a PNG.

Yes, this is not a native PowerPoint chart with an embedded workbook. If you manually change the 45% label to 70%, the rectangle will not grow by force of human optimism. But the original render is correct, stays vector, scales, and lets you copy text, change colours and move elements. To me that is a good baseline contract:

First guarantee a consistent, native representation through shared primitives. Then let an individual backend add deeper integration with a specific format’s features where it makes sense.

In future the PPTX backend could in theory support a NATIVE_CHART_WHEN_SUPPORTED mode. But the general vector fallback would still be useful, because it behaves the same across target formats.

A new format should not require a new language

Imagine a fictional format, PCP4X. Suppose it

  • uses fixed layout;
  • supports text, vector graphics and images;
  • has a decent Java library;
  • weighs less than PDF;
  • works better with accessibility;
  • can project a hologram of the quarterly report above your desk.

The last point is optional, but investors will like it.

If the PCP4X model is based on placing objects in a coordinate space, GraphCompose should not have to become a new project. There is no need to build again:

  • the DSL;
  • the document tree;
  • text wrapping;
  • tables;
  • pagination;
  • keepTogether;
  • keepWithNext;
  • layout dependencies;
  • chart layout;
  • the theming system.

What is needed is a backend:

graph-compose-render-pcp4x

It receives a finished LayoutGraph and translates elements into PCP4X primitives. Once the dependency is on the classpath, the user keeps writing the same document code. That is the main principle:

A new format should require a new backend, not a new document-authoring language for every developer.

Why open source belongs here

I cannot personally implement every possible format. Even with the best intentions, a day stubbornly continues to contain only 24 hours, and format specifications are usually written by people who clearly expect to live forever.

But the architecture does not require every backend to live in the main repository or to be written by one person.

Imagine a developer working at a company that needs a specialised format. They already know that format’s low-level library. They have two options.

Option one: a one-off solution. They write their own generation inside the product:

  • computing sizes;
  • placing text;
  • wrapping lines;
  • building tables;
  • fixing pagination;
  • adding a few dozen special cases;
  • being afraid to open that package a year later.

That company’s problem is solved. Nobody else will ever see the code.

Option two: implement a GraphCompose backend. The developer uses the existing layout engine and concentrates on translation:

LayoutGraph text fragment → target text primitive
LayoutGraph path          → target vector path
LayoutGraph image         → target image object
LayoutGraph link          → target hyperlink

Then they can publish it as an independent module:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>graph-compose-render-special-format</artifactId>
    <version>1.0.0</version>
</dependency>

Their work problem is solved. But at the same time a new backend exists that other developers can use. One person studied one format and wrote a translator. Everyone else keeps using the familiar GraphCompose DSL.

That is how a library can potentially become an ecosystem. Not because the main repository contains a hundred modules, but because the core provides a stable language and a stable extension point.

One interface is not enough for that

Obviously you cannot just declare

interface Backend {
    void render(Object something);
}

and announce that the ecosystem is ready. For third-party backends to be realistic, the project needs a proper Backend Development Kit.

A stable SPI

A backend author needs to understand

  • what data they receive;
  • which coordinate systems are used;
  • which elements have already been measured;
  • what guarantees LayoutGraph provides;
  • which APIs are stable;
  • which ones may change.

A capability model

Formats differ. For example:

Text frames       — native
Vector paths      — native
Internal links    — native
Clipping          — raster fallback
Font embedding    — partial
Native charts     — unsupported

A backend should state honestly what it can do. And not all partial statuses are the same. Sometimes an element stays native but a styling detail is approximated. Sometimes only one region has to be rasterised. Sometimes the format physically cannot preserve the required semantics. Better to show that up front than to discover it just before a client demo.

Conformance tests

There needs to be a shared test suite:

paragraph placement
multiline text
tables
repeated headers
images
paths
transforms
links
clipping
fonts
deterministic output

A backend author runs the suite and sees where their implementation matches the contract and where it does not yet. Without that, every new module will interpret the same model in its own way. And we would quietly arrive back at several engines — only this time distributed across Maven Central.

Not every format is fixed-layout

It matters not to pretend that one model describes absolutely everything. PDF, PowerPoint slides, SVG and images map well onto the fixed-layout approach. For them this scheme is natural:

LayoutGraph → native target primitives

But DOCX and HTML work differently. They participate in layout themselves. Word may wrap text differently depending on version, installed fonts, printer-metrics settings and, most likely, the position of the Moon.

So flow-based formats need a different kind of backend:

Semantic document tree → semantic target structure

Such a backend translates

  • section to section;
  • paragraph to paragraph;
  • heading to heading;
  • table to table.

and leaves final placement to the target format itself.

Which means GraphCompose can host two families of backends.

Fixed-layout backends receive resolved coordinates and reproduce geometry.

Semantic backends receive document structure and hand layout over to the target format.

The DOCX export is already closer to the second kind. And that is fine. An architecture is not obliged to pretend that Word is a PDF with a stranger API.

Where the idea has already worked

It is too early to say that GraphCompose has become a full multi-format platform. It is still a young open-source project where I do most of the work. But an important check has already happened.

The PDF backend uses PDFBox. The PPTX backend uses Apache POI. The formats have different object models, different capabilities and different limitations. Yet they receive the same resolved layout graph.

PPTX did not require a second DSL. It did not require a second table layout engine. It did not require separate chart layout. It did not require pagination to be implemented again.

That does not mean the backend was easy. Translating finished geometry into another format is still hard. Fonts, clipping, links, transforms and DrawingML explain fairly quickly that the word simply was used prematurely. But the complexity stayed in the right place. The backend decides how to express a finished layout. It does not decide again what the document should be.

The most revealing bug

While implementing the PPTX backend I ran into a text-width discrepancy. The layout engine measured one font face. PowerPoint received settings that made the viewer effectively draw a wider face. The difference was a few per cent.

That does not sound alarming. Until a short piece of text stops fitting into its computed frame, and two words in a heading start to look like one long German noun.

With independent layout implementations, this could have been filed as a quirk of the formats:

Well, PDF looks like this, and PowerPoint a bit different.

But with a shared LayoutGraph it is unambiguously a backend bug. The geometry was already computed. If the backend draws an element wider than the space allocated to it, it violates the contract.

Bugs like that are exactly what demonstrates the value of a shared intermediate representation. Differences stop being that’s just how it turned out. They become verifiable defects.

What I actually want to build

The goal of GraphCompose is not for me to personally support every document format that exists on Earth and inside the enterprise systems nobody has dared switch off since 2008. The goal is different. GraphCompose should provide

  1. a single language for describing documents;
  2. an independent layout engine;
  3. a stable LayoutGraph;
  4. a contract for fixed-layout and semantic backends;
  5. a capability model;
  6. a test suite for authors of new modules;
  7. a few high-quality reference implementations.

All of it inside the Java ecosystem.

Then a developer learns one way to create documents. And support for a particular format becomes a pluggable detail.

GraphCompose DSL
Document model
Layout engine
LayoutGraph
PDF / PPTX / SVG / Image / PCP4X / whatever shows up tomorrow

Not every backend will have the same functionality. Not every format will be able to preserve every capability. Somewhere there will be native output. Somewhere an approximation. Somewhere a local raster fallback. Somewhere an honest unsupported. But the user will not have to relearn how to describe a document and re-solve layout problems for each new format.

Instead of a conclusion

When I started GraphCompose, I just wanted to make a CV in Java. Then it turned out that moving coordinates by hand is not much fun. Then came a layout engine. Then a semantic DSL. Then a separate core. Then a second fixed-layout backend, which finally proved that PDF really is only one of the ways out.

Right now the main idea of the project sounds like this to me:

The developer describes the document once. The engine resolves the layout. The backend translates the result into native objects of the target format.

PowerPoint is not the final destination here. It is the first proof that the boundary works. If a new format appears tomorrow and someone writes a backend for it, existing documents should never find out. They will simply gain one more way of being drawn.

And perhaps that is what a document engine should look like: not a library for one file type, but a language that does not have to start every conversation with coordinates.


A Russian version of this article was originally published on Habr.