PDF and Press
stxt-pdf converts an STXT document into an accessible PDF. stxt-press offers it as an HTTP service, with a web editor and a command line, and fills templates with JSON data.
Document (dev.stxt.pdf):
Language: en
Title: Quarterly report
Page:
Paper: A4
Margin: 50
Styles:
For element: Document
Typography:
Font: IBM Plex Sans
Size: 10
Section:
Heading 1:
Text: Quarterly report
Paragraph: Sales grew by 12% compared with the previous quarter.The namespace dev.stxt.pdf describes the document:
Page: the paper and the margins.Styles: the appearance, with rules by element, by style or by identifier.Section: the content, with headings, paragraphs, tables, images, lists...
The engine works out where everything goes, splits the content into pages and writes
the PDF. Both projects read the STXT with the Java library and are not
implementations of the language: syntax and schema errors carry the codes of the
library (NODE_NOT_DEFINED_IN_SCHEMA...), and those of the engine, its own.
The PDF
- Embedded fonts, with the characters that are used.
- Tagging for accessibility: PDF/UA-1, or PDF/UA-2 on request.
- PDF/A (2 and 3,
banda) on request, with embedded files for Factur-X. - Bookmarks, one for each heading.
- SVG: besides the PDF, one image per page.
The engine is strict. Each element only accepts the properties that make sense in
it, and none has an appearance of its own: a Heading 1 is not larger than a
Paragraph unless a style says so. What would have no effect, contradicts itself or
does not fit is an error with its line.
stxt-pdf
The engine, as a Java library. It is published on Maven Central as
dev.stxt:stxt-pdf.
Installation
Java 17 or later. The current version is 0.18.1.
<dependency>
<groupId>dev.stxt</groupId>
<artifactId>stxt-pdf</artifactId>
<version>0.18.1</version>
</dependency>
Or with Gradle:
implementation 'dev.stxt:stxt-pdf:0.18.1'
Converting a document
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import dev.stxt.InlineNode;
import dev.stxt.pdf.convert.Converter;
import dev.stxt.pdf.stxt.DocumentReader;
InlineNode doc = new DocumentReader().read(Path.of("report.stxt"));
try (OutputStream out = Files.newOutputStream(Path.of("report.pdf"))) {
Converter.create().toPdf(doc, out);
}
DocumentReader reads and validates the document. Converter lays it out and writes
the PDF. A Converter is created once and can be used from several threads at once.
Options
Converter converter = Converter.builder()
.resources(new Resources(Path.of("my-resources"))) // another resources folder
.styles(Path.of("styles.stxt")) // a separate styles file
.pdfA(PdfA.A3B) // PDF/A-3b
.build();
| Option | Default |
|---|---|
resources |
The folder resources/ of the working directory |
styles |
None. Each call adds a styles file, and the last one wins |
limits |
2000 pages, 25 million pixels per image and 20 MB per PDF, among others |
debug |
false. With true, it paints the outline of each box |
pdfUa |
1: PDF/UA-1. With 2, PDF/UA-2, in PDF 2.0 |
pdfA |
None. PdfA.A2B, A2A, A3B or A3A |
exclude |
None. The checks that are no longer an error, such as Check.CONTRAST |
cancellation |
None. When to stop every conversion, for example after a time limit |
Resources
The fonts and the images of a document are in a resources folder:
resources/
fonts/ .ttf and .otf fonts
images/ .png, .jpg and .svg images
- Only the fonts of
fonts/are used, never those of the system. Each file is recognized by the family, the weight and the style it declares inside. - A variant that has no file (a weight the family does not have) is an error: the closest one is not used.
Image: logos/acme.pngisresources/images/logos/acme.png. Absolute paths,..and links that leave the folder are not accepted.- The repository includes IBM Plex Sans, Serif and Mono, under the SIL Open Font License.
Errors
Every error is a StxtPdfException with its kind (kind()), its code (code()) and
its line (line(), 0 if it is not of a line):
kind() |
What it is |
|---|---|
INVALID_DOCUMENT |
An error of the document: syntax, schema or rules of the engine |
LIMIT |
The document exceeds a limit: pages, pixels of an image, size |
CANCELLED |
The conversion was stopped by its cancellation |
RESOURCE |
The configuration of the caller: the resources folder, a file |
INTERNAL |
An error of the engine |
Documentation
Everything is in the repository, in English and in Spanish:
- Manual: how a document is written, from the first one to each element.
- Reference: the exact rule of each behavior and the design decisions.
- Examples: real documents (invoice, letter, contract, annual report...) with their PDF.
stxt-press
The service, in a single jar: an HTTP API, a web editor and a command line. The layout is that of stxt-pdf, and the fill of the Slots, that of stxt-transform. The current version is 0.6.1.
Starting
With Docker, the image
ghcr.io/stxt-lang/stxt-press,
for linux/amd64 and linux/arm64:
docker run --rm -p 127.0.0.1:8080:8080 ghcr.io/stxt-lang/stxt-press:0.6.1
The index and the editor are at http://127.0.0.1:8080/, with example templates and
documents. The image answers without token to anyone who reaches its port:
127.0.0.1 keeps it on the machine.
Own templates, documents or resources replace those of the image:
docker run --rm -p 127.0.0.1:8080:8080 \
-v "$PWD/templates:/opt/stxt-press/templates:ro" \
-v "$PWD/documents:/opt/stxt-press/documents:ro" \
-v "$PWD/resources:/opt/stxt-press/resources:ro" \
ghcr.io/stxt-lang/stxt-press:0.6.1
Without Docker, the folder
server/ of the
repository is a whole server (the jar, its configuration, the fonts and the
examples). It needs Java 17 or later:
server/start.sh
Templates and Slots
A template is a dev.stxt.pdf document with Slots, the places that wait for a
piece of data:
Document (dev.stxt.pdf):
Language: en
Title: Invoice ${number}
Page:
Paper: A4
Margin: 50
Styles:
For element: Document
Typography:
Font: IBM Plex Sans
Size: 10
Section:
Heading 1:
Text: Invoice ${number}
Paragraph: Customer: ${customer}
Paragraph: [lines]${item}: ${amount} €With these data:
{
"number": "2026-0142",
"customer": "Athena Bookshop",
"lines": [
{ "item": "A5 notebooks", "amount": "120.00" },
{ "item": "HB pencils", "amount": "36.00" }
]
}
The filled document, the one that is laid out, ends like this:
Section:
Heading 1:
Text: Invoice 2026-0142
Paragraph: Customer: Athena Bookshop
Paragraph: A5 notebooks: 120.00 €
Paragraph: HB pencils: 36.00 €
| Slot | What it does |
|---|---|
<var> |
Puts a value, and ends the value of the node |
{object} |
Enters an object |
[list] |
Repeats the node once for every element |
${var} |
Puts a value inside text, looking it up from the element up to the root |
<?var>, {?object}, [?list], ${?var} |
Optional: with no data, the node is removed (or the text gets nothing) |
$${ |
A literal ${ |
A required Slot with no data is an error (SLOT_NO_DATA). The Slots follow
STXT Transform, which is in Genesis: they may change with it.
Templates and documents of the server
The server prints two kinds of files, each from its own folder:
- A template (
/print): it is filled with the JSON data of the request, or with its sample data. - A document (
/documents): it is printed as it is, without data.
templates/
.styles.stxt the styles of every template
invoices/
.styles.stxt the styles of the templates of invoices/
invoice.stxt the template invoices/invoice
invoice.styles.stxt its own styles
invoice.json its sample data
A template is named by its path without extension (invoices/invoice). The styles
apply from the root to the template, and the nearest one wins. Documents follow the
same rules, without data. The folders are read on every request: a file changed on
disk counts without restarting.
Routes
| Route | What it does |
|---|---|
GET /print/{path} |
The template, filled with its sample data |
POST /print/{path} |
The template, filled with the body: the JSON data as they are |
GET /documents/{path} |
The document, as it is |
POST /render |
Any document: {"document": "...", "styles": ["..."], "data": {...}, "options": {...}} |
GET /schema |
The schema dev.stxt.pdf of the engine |
GET /health |
The status and the versions, without token |
/print, /documents and /render answer with a PDF (?format=pdf, the default),
one SVG per page (?format=svg) or the filled document (?format=stxt).
&debug=true paints the outlines of the boxes. The example invoice of the server,
with its sample data and with those of a file:
curl -o invoice.pdf http://127.0.0.1:8080/print/examples/invoice
curl -o invoice.pdf -d @data.json http://127.0.0.1:8080/print/examples/invoice
An error is a JSON body with its code. Those of a conversion also carry their kind,
their line and the input the line belongs to (document, data, filled, a styles
file...):
{"kind": "INVALID_DOCUMENT", "code": "JSON_NOT_VALID", "line": 2, "source": "data", "message": "..."}
Command line
The commands of the jar have the names of the routes:
| Command | What it does |
|---|---|
start |
Starts the HTTP service with its server.stxt |
render <document> -o <file> |
Converts a document to PDF, with --styles, --data, --pdf-ua, --pdf-a... |
print <template> -o <file> |
Converts a template of the templates folder |
documents <document> -o <file> |
Converts a document of the documents folder |
fill <document> --data <file> |
Writes the document with its Slots filled |
From the root of the repository, with the invoice above:
java -jar server/stxt-press.jar fill invoice.stxt --data invoice.json
java -jar server/stxt-press.jar render invoice.stxt --data invoice.json -o invoice.pdf --config server/server.stxt
An error is written to stderr with its file and its line:
stxt-press: invoice.stxt:15: SLOT_NO_DATA: Node 'Paragraph': No data for ${customer}
The exit code is 0 if the job ends well, 1 if it fails (the document, its data, a
resource, a limit) and 2 if the invocation is not valid. A file of -o is replaced
only when the job ends well.
Configuration
A server is configured with a server.stxt file, validated with the schema
dev.stxt.press of the jar:
Server (dev.stxt.press):
Host: 0.0.0.0
Port: 8080
Token env: STXT_PRESS_TOKEN
Resource folder: resources
Template folder: templates
Document folder: documents
Routes:
Render: false
Save: true
Limits:
Timeout: 20
Max pages: 500Without a file, the server listens on 127.0.0.1:8080, without token and without
templates or documents. Relative paths are relative to the folder of server.stxt.
Every node is in the README.
Security
The server has no TLS or users: it is meant to run behind a reverse proxy that adds them.
- Token: with
Token env, every route but/healthand the pages asks forAuthorization: Bearer <token>. - Local names: without token and on a loopback address, it only answers requests
whose
Hostis local, against DNS rebinding. - Routes off:
Render: false,Files: falseandEditor: falseinRoutesleave only the printing of the templates and the documents.
Versions
Both projects are in 0.x. Until 1.0, the namespace dev.stxt.pdf, the Java API, the
routes and server.stxt may change between minor versions, and so may the PDF of a
valid document. Each change is in the CHANGELOG.md of its repository.