Sign inSign up

dabde/docx2pdf

By dabde

•Updated 18 days ago

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

Image
Integration & delivery
0

667

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.2.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.2.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
{{ date|local }} / {{ date|local('long') }} / {{ date|local('long', 'fr') }}ISO date/datetime in a language's format — language from the argument, else the data's locale, else de; format short/medium/long/full or a CLDR pattern
{%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.

⁠Changelog

⁠2.2.0 (2026-10-01)
  • {{ date|local }} filter: ISO date/datetime in the receiver's language (Babel/CLDR). Language from the filter argument, else the data's locale, else de; ua → uk, dari → fa_AF. Non-dates pass through unchanged.
⁠2.1.0 (2026-08-09)
  • Sentry error reporting via SENTRY_DSN / SENTRY_ENVIRONMENT / SENTRY_RELEASE; request bodies are never sent.
  • Alpine base image: trivy image findings 181 → 0.
⁠2.0.0 (2026-08-09) — breaking
  • Templates render in a sandbox; markdown images can no longer reach the network or filesystem.
  • {{ value|qr }} filter for QR codes.
  • Rich-text object removed, see below:
⁠Migrating to 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:8ee41d86d…

Size

75.4 MB

Last updated

18 days ago

docker pull dabde/docx2pdf