Skip to content

feat: use Typst for pdf generation, detach pipeline from browser - #1687

Draft
shivoomiess wants to merge 6 commits into
developfrom
feat/typst-pdf-pipeline
Draft

feat: use Typst for pdf generation, detach pipeline from browser#1687
shivoomiess wants to merge 6 commits into
developfrom
feat/typst-pdf-pipeline

Conversation

@shivoomiess

@shivoomiess shivoomiess commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces Typst for PDF generation, completely detaching the PDF rendering pipeline from the browser dependency.

Motivation and Context

This change was necessary to remove the dependency on Chromium for PDF rendering, which was causing performance and security concerns. It also simplifies the PDF rendering process by introducing Typst, a standalone package for PDF rendering with no express, no database, no DOM, and no Chromium. This results in a more efficient and secure PDF rendering pipeline.

Changes

  • Modified the .github/workflows/test-build.yml to cache the pdf-renderer node_modules and to install and build the pdf-renderer before the backend installs.
  • Created a new file PDF_TYPST_MIGRATION_PLAN.md that contains a comprehensive plan to replace the browser-based PDF engine with Typst.
  • Introduced Typst for PDF rendering, which takes { templates, data } as input and outputs a PDF Buffer.
  • The rendering stages are now: Handlebars -> HTML -> Typst markup -> PDF.
  • Added a CLI for debugging with --dump-typst and --dump-html options.
  • Updated apps/backend/src/factory/pdf/sendPdf.ts as the single dispatch point for PDF rendering.
  • Added PDF_ENGINE and PDF_PAGE_SIZE variables, documented in apps/backend/example.development.env.
  • Updated documentation and added tests for the new PDF rendering process.

How Has This Been Tested?

Fixes Jira Issue

https://jira.ess.eu//browse/

Depends On

Tests included/Docs Updated?

  • I have added tests to cover my changes.
  • All relevant doc has been updated

shivoomiess and others added 4 commits July 30, 2026 10:35
Adds apps/pdf-renderer with handlebars, dom-typst and the typst.ts node
compiler. No source yet, only the package skeleton and the migration plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the four stages of the renderer:

  handlebars  template + data -> HTML
  dom-typst   HTML            -> Typst markup
  assembly    fragments       -> one Typst document
  typst.ts    Typst           -> PDF bytes

Nothing in this package needs a browser, a subprocess or the network. The
Typst compiler is a native addon and assets are passed to it as in-memory
shadow files.

Handling around the converter keeps parity with the old Chromium engine:

- data: URL images become Typst assets, so header logos still render
- the pageNumber and totalPages spans become Typst page counters
- CSS colour functions are tightened, which the converter otherwise
  mis-parses inside shorthand declarations
- image sizing falls back to the intrinsic size read from the file header

Includes a CLI so the pipeline can run with no server or database, and 64
tests covering each stage plus end-to-end renders of the templates the
frontend seeds a new PDF template with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Routes the two PDF types that use a user-officer defined template through
the local typst renderer instead of the browser-based factory service.
XLSX, ZIP, sample and shipment-label documents are untouched.

sendPdf is the single decision point. It renders locally when every entity
in the request carries a template, and delegates to the factory service
otherwise or if a local render throws, so a template the new engine cannot
handle still produces a PDF.

PDF_ENGINE=factory pins every request back to the old service.

Also adds multi-document rendering to the renderer, so a bulk download of
several proposals becomes one Typst document with a per-entity running
header, replacing the old merge of separately generated PDFs. Page numbers
now run continuously across such a collection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds documentation/docs/developer-guide/pdf_generation.md with the pipeline
for each flow, the library used at every step, what still goes to the
factory service, and the behaviour differences against the browser engine.

Corrects the factory description in architecture.md, which claimed it
generates all PDFs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

const scale = options.targetPageWidthPt / options.canvasWidthPx;

let processed = normalizeStyles(html.replace(NON_RENDERABLE, ''));
shivoomiess and others added 2 commits July 30, 2026 11:58
localRenderer imported the renderer package at module scope, and index.ts
reaches that module through the factory middleware. A missing build output
or a native addon that fails to load therefore stopped the whole backend
from starting, which shows up in the UI as a blank page everywhere, not
just a missing PDF.

The renderer is now loaded on first use. If it cannot be loaded the backend
starts normally, logs why, and every PDF goes to the factory service.

canRenderLocally is split into isLocallyRenderable, which is about the
request, and the availability check, so the request logic stays testable.

Also adds a prepare script to the renderer so npm install in that package
always leaves a build behind, and passes factoryBaseUrl to templates, which
the factory service used to inject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants