Skip to content

Tutorials — from a first PDF to a production release

This page runs in a straight line from rendering one PDF to baking your template into a Docker image and shipping it. Every command is transcribed from something CI actually runs.

1. One PDF, from nothing but Docker

The image carries the CLI, the MCP server, the font and locale packs, and every bundled example.

bash
docker run --rm ghcr.io/kengos/shojiku:edge > receipt.pdf

Lifting a template out of the image, editing it and previewing the result is the quickstart's job — the edit → validate → preview loop lives there.

2. Embed it in your app (the published SDKs)

Packages for five languages are on the public registries. Every SDK has the same shape: construct a client, pass a template name and params, write out the returned bytes. Errors come back as a typed failure, not an exception.

bash
pip install shojiku
bash
gem install shojiku
bash
npm install shojiku
bash
dotnet add package Shojiku
xml
<!-- the jar + the classifier of the platform you RUN on
     (the Netty/LWJGL convention) -->
<dependency>
  <groupId>jp.kengos</groupId>
  <artifactId>shojiku</artifactId>
  <version>0.1.0</version>
</dependency>
<dependency>
  <groupId>jp.kengos</groupId>
  <artifactId>shojiku</artifactId>
  <version>0.1.0</version>
  <classifier>linux-x64</classifier>
</dependency>

Rendering is the same three lines in every language. In Python:

python
import json, shojiku

client = shojiku.Client(
    templates="templates/", font_dirs=["packs/fonts"], locale_dirs=["packs/locale"]
)
result = client.generate("receipt-ja", json.load(open("params.json")))
if not result.success:
    raise SystemExit(f"{result.failure.kind} | {result.failure.message}")
open("out.pdf", "wb").write(result.artifact.bytes)

Vendor the repository's packs/ next to your templates — trimmed to the packs your locales actually use.

3. Add a logo and your own font

The first two things a real template needs.

Images sit next to the template and are referenced by src. The root is the template file's directory (the CLI can move it with --assets-dir); data: URIs, inline SVG and params-bound dynamic images work too. The normative page is image.md.

yaml
- type: image
  box: { w: 120, h: 40 }
  src: assets/logo.svg

Fonts are packs. Put the font files and a manifest.yml (one license

  • a sha256 per face) under packs/fonts/<id>/, then add the pack to the locale's uses via an overlay. The sha256 and the face's embedding rights (fsType) are verified at load. The normative page is fonts.md.
yaml
# packs/fonts/my-corporate/manifest.yml
version: 1
license: Proprietary
redistributable: false
faces:
  - id: my-corporate
    file: MyCorporate-Regular.ttf
    sha256: <output of sha256sum>
yaml
# packs/locale/ja-jp.yml (an overlay over the builtin ja-JP)
fonts:
  uses: [biz-ud, ipamj-mincho, noto-sans-mono, my-corporate]

Now fontFamily: my-corporate resolves. Two things to watch: uses REPLACES the list (state the whole set, not just your addition), and a fontFamily naming a pack the locale doesn't use silently falls back. The Dockerfile recipes below COPY packs/ wholesale, so your own pack rides the same line.

4. Ship it (the Dockerfile recipes)

Once the template is right, bake app + template + packs into one image. These are the real recipe files for all five languages — the same files make proof-deploy builds and renders against the public registries.

docker
# Production shape: your app + its template + the packs, rendered by the
# shojiku wheel from PyPI. The params come out of a SQLite database built
# into the image (seed.py) — swap that for your real database connection.
#
#   docker build -t receipt-renderer .
#   docker run --rm receipt-renderer > receipt.pdf
FROM python:3.12-slim-bookworm
RUN pip install --no-cache-dir shojiku
WORKDIR /app
# Vendor the template set and the font/locale packs your documents use
# (packs/ comes from the Shojiku repository; trim it to your locale's packs).
COPY templates/ templates/
COPY packs/ packs/
COPY params-base.json seed.py render.py ./
RUN python seed.py
CMD ["python", "render.py"]
docker
# Production shape: your app + its template + the packs, rendered by the
# platform gem from RubyGems.
#
#   docker build -t receipt-renderer .
#   docker run --rm receipt-renderer > receipt.pdf
FROM ruby:3.3-slim-bookworm
RUN gem install -N shojiku
WORKDIR /app
COPY templates/ templates/
COPY packs/ packs/
COPY render.rb ./
CMD ["ruby", "render.rb"]
docker
# Production shape: your app + its template + the packs, rendered by the
# shojiku npm package (napi addon resolved per platform).
#
#   docker build -t receipt-renderer .
#   docker run --rm receipt-renderer > receipt.pdf
FROM node:22-bookworm-slim
WORKDIR /app
RUN npm init -y >/dev/null && npm install --no-audit --no-fund shojiku
COPY templates/ templates/
COPY packs/ packs/
COPY render.mjs ./
CMD ["node", "render.mjs"]
docker
# Production shape: publish a trimmed console app with the Shojiku NuGet
# package (native engine per RID under runtimes/), then run it on the
# runtime-only image.
#
#   docker build -t receipt-renderer .
#   docker run --rm receipt-renderer > receipt.pdf
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY Renderer.csproj Program.cs ./
RUN dotnet publish -c Release -o /out

FROM mcr.microsoft.com/dotnet/runtime:10.0
WORKDIR /app
COPY --from=build /out ./
COPY templates/ templates/
COPY packs/ packs/
CMD ["dotnet", "Renderer.dll"]
docker
# Production shape: a Maven build with the Shojiku jar + the PLATFORM
# CLASSIFIER jar (the one deliberate line — Maven never resolves a
# classifier on its own; pick the classifier of the image you run on).
#
#   docker build -t receipt-renderer .
#   docker run --rm receipt-renderer > receipt.pdf
FROM maven:3-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
COPY src/ src/
RUN mvn -q package dependency:copy-dependencies -DoutputDirectory=/deps

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/target/renderer-1.jar app.jar
COPY --from=build /deps/ deps/
COPY templates/ templates/
COPY packs/ packs/
CMD ["java", "-cp", "app.jar:deps/*", "Render"]

The Python recipe goes one step further and pulls its params out of a SQLite database inside the image: static document facts (the issuer block, the QR) stay in the template-side params, and only the transactional rows come from the DB:

python
"""The production shape: static document facts (issuer block, QR) stay in
the vendored base params; the transactional half (header + lines + totals)
comes out of SQLite. Writes the PDF to stdout."""

import json
import sqlite3
import sys

import shojiku

con = sqlite3.connect("params.db")
head = con.execute("select number, issued_at, recipient, purpose from receipt").fetchone()
lines = con.execute("select name, quantity, unit_price from line").fetchall()

params = json.load(open("params-base.json"))
items = [
    {"name": n, "quantity": q, "unit_price": p, "amount": q * p} for (n, q, p) in lines
]
total_ex = sum(i["amount"] for i in items)
tax = total_ex // 10
params.update(
    {
        "receipt": {"number": head[0], "issued_at": head[1]},
        "recipient": {"name": head[2]},
        "purpose": head[3],
        "items": items,
        "amount": {"total_in_tax": total_ex + tax, "total_ex_tax": total_ex, "tax": tax},
    }
)

client = shojiku.Client(
    templates="templates/", font_dirs=["packs/fonts"], locale_dirs=["packs/locale"]
)
result = client.generate("receipt-ja", params)
if not result.success:
    raise SystemExit(f"render failed: {result.failure.kind} | {result.failure.message}")
sys.stdout.buffer.write(result.artifact.bytes)

5. Sign it, verify it

Sign before you distribute; the receiving side verifies. Neither touches the network, and there is deliberately no flag that takes a passphrase on the command line (argv is readable by other processes).

bash
shojiku sign --input out.pdf --key signer.pem --cert signer.crt --output signed.pdf
shojiku verify --input signed.pdf --anchor signer.crt

verify prints a JSON report that includes the byte range the signature actually covers, and exits non-zero when the document does not verify. The certs you trust are named with --anchor every time — the machine's trust store is never consulted.

Next

  • The template language itself: the reference — 32 pages, one per feature
  • To feel how it behaves: the playground
  • To hand the writing to an AI: the agents page