Sign inSign up

dabde/docx2pdf

By dabde

Updated about 2 months ago

python docx converter API service with template function of docxtpl to output rendered pdfs

Image
Integration & delivery
0

450

dabde/docx2pdf repository overview

Document Generation Service

Generates DOCX and PDF documents by merging JSON data into a DOCX template.

Payload values are plain JSON — the template decides presentation: literal text, markdown rendered as real Word formatting, or a QR code. Pandoc is baked into the image; PDF output additionally needs a reachable Collabora Online instance.

linux/amd64 and linux/arm64. Runs as nobody, listens on 8000, writes only to /tmp.

Quick start

docker run -p 8000:8000 NAMESPACE/docx2pdf:2.1.0

Interactive API docs: http://localhost:8000/docs — health check: /health.

DOCX generation works out of the box. For PDF, point the service at Collabora:

services:
  app:
    image: NAMESPACE/docx2pdf:2.1.0
    ports: ["8000:8000"]
    environment:
      - COLLABORA_URL=http://collabora:9980/cool/convert-to/pdf
    depends_on: [collabora]

  collabora:
    image: collabora/code:latest
    environment:
      - aliasgroup1=http://app:8000
      - DONT_GEN_SSL_CERT=true
      - extra_params=--o:ssl.enable=false
    cap_add: [MKNOD]

Example request

curl -X POST http://localhost:8000/generate \
  -F 'template=@your_template.docx' \
  -F 'data={
    "name": "John Doe",
    "date": "2026-03-02",
    "case_ref": "AB-1234",
    "biography": "# Biography\nJohn is a **software engineer**."
  }' \
  -F 'output_format=pdf' \
  --output generated.pdf

output_format is docx (default) or pdf.

Template syntax

TagPurpose
{{ name }}a value as literal text
{{p notes|rich }}markdown rendered as headings, bold, lists, tables
{{ case_ref|qr }} / {{ case_ref|qr(15, 2) }}QR code — width in mm, quiet zone in modules
{%p if ... %}{%p endif %}conditionals, each tag alone in its paragraph
{%tr for c in cases %}{%tr endfor %}repeat a table row — each tag in its own row

Two traps worth knowing before you build a template:

  • |rich needs the {{p …}} form. A plain {{ notes|rich }} renders fine in DOCX and is silently dropped from the PDF. Nothing detects it — check the PDF when adding a rich field.
  • A missing key is only silent when used bare. {{ office }} renders empty, but {{ office.city }} on a payload without office raises and returns 400.

Configuration

All environment variables are optional.

VariableDescriptionDefault
COLLABORA_URLCollabora convert-to endpointhttp://localhost:9980/cool/convert-to/pdf
COLLABORA_USERBasic-auth user; empty sends no auth header(empty)
COLLABORA_PASSBasic-auth password(empty)
COLLABORA_READ_TIMEOUTRead timeout in seconds (connect is fixed at 5)60
MAX_UPLOAD_BYTESRejects larger templates or data payloads with 41320000000
PANDOC_TIMEOUTSeconds before one markdown conversion is abandoned30
PANDOC_CONCURRENCYMax simultaneous pandoc processes4
PANDOC_SANDBOXRun pandoc with --sandbox; see Security belowtrue
MAX_MARKDOWN_FIELDSMax |rich conversions per document50
MAX_QR_CODESMax |qr codes per document50
QR_BORDERDefault QR quiet zone, in modules4
SENTRY_DSNError reporting; empty disables Sentry entirely(empty)
SENTRY_ENVIRONMENTEnvironment tag on reported events(empty)
SENTRY_RELEASERelease tag on reported events(empty)
SENTRY_TRACES_SAMPLE_RATEPerformance tracing sample rate; 0 reports errors only0

Request bodies are never sent to Sentry — the data field carries application data. Exception messages, the URL and the stack trace are.

Security

Templates are uploaded by callers, so the service treats them as untrusted input:

  • Templates render in a Jinja2 SandboxedEnvironment. With a plain environment, a template containing {{ ''.__class__.__mro__… }} would achieve arbitrary code execution.
  • Markdown image links cannot reach the network or filesystem. ![x](http://169.254.169.254/…) would be an SSRF and ![x](/etc/passwd) a file read, because Pandoc fetches and embeds image targets. Pandoc runs with --sandbox; startup verifies this with a real conversion, and where it is not usable the service rejects non-data: image links itself. Inline images as base64 data: URIs. PANDOC_SANDBOX=false disables both mechanisms — trusted callers only.
  • Per-document limits (MAX_MARKDOWN_FIELDS, MAX_QR_CODES) bound the work one request can cause, since the template controls how many conversions happen. Counters are per request.

Still open: the sandbox stops code execution but not unbounded computation — a template containing {% for i in range(10**9) %} will occupy a worker until something kills it. Enforce a request timeout at the ingress.

The image is Alpine-based with no package manager and no pip; dependencies are installed in a build stage. trivy image reports 0 vulnerabilities.

Breaking change in 2.0.0

The rich-text object is gone. Send markdown as a plain string and choose the rendering in the template:

- "biography": {"content": "# Bio\nA **dev**.", "is_markdown": true}      → {{p biography }}
+ "biography": "# Bio\nA **dev**."                                       → {{p biography|rich }}

A payload still containing "is_markdown" is rejected with a 400 explaining the migration, rather than rendering a literal dict into your document.

Tag summary

Content type

Image

Digest

sha256:596bcbfd7

Size

101.1 MB

Last updated

about 2 months ago

docker pull dabde/docx2pdf