ZuloOne
English
Sign in
Menu

🖨 Print form PDF

Updated

When what you need is a file, not paper.

← Developer wiki

Printing and PDF are two different things#

Printing has always worked. The render endpoint returns HTML with window.print() embedded in it, the human picks "Save as PDF" in the browser's own dialog and gets a file. No service is needed for that.

What did not work is the other thing: a PDF the server holds. Bytes, to attach an invoice to a message, to store on a document, to hand to an integration. EmailMessage has carried attachments from the very start — there was nothing to produce them with.

A browser does the rendering, and that is a decision, not an accident#

Option Why not
A lightweight engine (wkhtmltopdf and its like) the HTML renderer leans on flexbox; engines like that will mangle the layout
Our own renderer over the ZR model a second implementation of one layout next to the 2000 lines of the existing one: the PDF and the preview would drift apart, and every edit to a form would cost two edits
The same browser as in Preview the only thing that makes "the PDF matches what I saw" a fact

The browser lives in a separate container (chromium in compose). That way the kernel image does not grow by 400 MB for everyone who has no need of PDF.

The capability is optional#

With no service the endpoint answers 503 with a hint rather than failing with an explanation:

{ "error": "PDF rendering is not configured on this stand",
  "hint": "Start the 'chromium' compose service and set Printing:Pdf:ServiceUrl,
           or print from the browser: /render/{documentId}?autoprint=1" }

The configuration:

"Printing": { "Pdf": {
  "ServiceUrl": "http://chromium:3000",
  "Path": "/pdf",
  "TimeoutSeconds": 60,
  "Token": ""
}}

The address comes from the configuration only, not from metadata and not from a script: it is the address the server will send the document to, the same reason the host allow-list does not live in the database.

From a script#

This is what the whole thing was done for — attaching an invoice to a message:

var pdf = ScriptServices.Get<IDocumentPdfService>();
if (!pdf.IsAvailable) return;                       // a stand with no browser

var bytes = await pdf.RenderAsync(formMetaId, invoiceId);
var mail = new EmailMessage { AddressTo = customer.Email, Subject = "Счёт" };
mail.AttachFile($"invoice-{invoice.ID}.pdf", bytes, "application/pdf");
await ScriptServices.Get<IEmailService>().SendAsync(mail);

RenderBoundAsync(documentTypeMetaId, documentId) returns the form bound to the document type — but only when there is exactly one. With two or more it returns null: picking "the first" would mean the customer gets a form chosen alphabetically.

From the interface#

The print dialog → Save PDF. Separate from Print, because printing hands the job over to the local dialog and leaves no file behind.

What is checked and what is not#

The unit tests (PdfRenderServiceTests, with no browser and no container) close off three ways of lying to the caller: failing silently when the service is not configured, presenting a remote error as silence, and returning a "PDF" that is in fact an HTML error page. The last is the most dangerous of them — otherwise it comes to light at the counterparty. That is why the response is checked for a %PDF header.

What these tests do NOT check: that Chromium makes a correct PDF. That is Chromium's job, and an end-to-end check requires the container to be running.

Where next#

Was this page helpful?