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.2.0</version>
</dependency>
<dependency>
  <groupId>jp.kengos</groupId>
  <artifactId>shojiku</artifactId>
  <version>0.2.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)

Bundle the font and locale packs with your app, the same way you bundle the templates. The release tarball is the easiest way to get the full set, and you can delete the packs for locales you do not use.

bash
wget https://github.com/kengos/shojiku/releases/download/v0.2.0/shojiku-0.2.0-packs.tar.gz
tar xzf shojiku-0.2.0-packs.tar.gz   # unpacks packs/fonts and packs/locale

3. Use your own images

This is for putting your own image, a logo say, into a template. Put the file in an assets/ directory next to the template file.

templates/
  receipt-ja/
    templates.yml
    assets/
      logo.png

With this layout, a relative path in the template is all it takes to draw it. There is no step where you hand the engine the bytes.

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

Paths resolve against the template file's directory; the CLI can move the root with --assets-dir. data: URIs, inline SVG and params-bound dynamic images are also available. The exact rules are in image.md.

4. Use a font beyond the bundled packs

This is for a family you picked in the Designer's font picker, or a corporate font of your own. On the template side the selection is the fontFamily style property.

yaml
# templates.yml — set it on a container and the elements below inherit it
style: { fontFamily: my-corporate }

To make that id resolve, the engine needs a font pack, registered in the locale.

If the Designer picked it, the export kit (zip) already contains the pack, license file included. Unzip it into packs/fonts/ and the pack side is done.

If you have the TTF, one command builds the pack:

bash
shojiku font add MyCorporate-Regular.ttf \
  --family my-corporate --license Proprietary
# add the bold face to the same family
shojiku font add MyCorporate-Bold.ttf \
  --family my-corporate --license Proprietary --weight bold

That writes packs/fonts/my-corporate/, copies the file in, and pins its sha256 in the manifest:

yaml
# packs/fonts/my-corporate/manifest.yml
version: 1
license: Proprietary
faces:
  - id: my-corporate
    file: MyCorporate-Regular.ttf
    sha256: <computed for you>

The sha256 and the face's embedding rights (fsType) are verified at load — and font add checks the same two things up front, so a font whose licence forbids embedding is refused there rather than at your first render. If you hold a separate embedding licence, say so with --embedding-attested. Packs are non-redistributable unless you pass --redistributable.

Either way, finish by telling the render which pack to load. Per run, that is a flag:

bash
shojiku render --templates templates.yml --params params.json \
  --output out.pdf --font-pack my-corporate

For a deployment where every render should have it, put it in the locale's uses instead. A one-file overlay is enough:

yaml
# packs/locale/ja-jp.yml (an overlay over the builtin ja-JP)
fonts:
  uses: [biz-ud, ipamj-mincho, noto-sans-mono, my-corporate]

uses restates the whole list rather than appending, so keep the bundled packs in place when you add yours — which is why --font-pack is the shorter route for one run: it adds to the list instead of replacing it. A fontFamily naming a pack that neither the locale uses nor the run names warns unknown_font_family and falls back to the locale's default font.

Pack lookup works the same in the CLI and the SDKs: the search list grows from explicit directories, to the environment, to ./packs/fonts and ./packs/locale in the current directory. In an SDK the explicit directories are client options (the font_dirs / locale_dirs passed in section 2's Python example). The environment variables are SHOJIKU_FONT_DIR / SHOJIKU_LOCALE_DIR (PATH-separated); the CLI flags are --font-dir / --locale-dir. The Dockerfiles in the next section COPY packs/ wholesale, so your own pack rides the same line. The exact rules (auto-fetch via pinned url:, fallback chains) are in fonts.md.

5. Ship it (the Dockerfile recipes)

Once the template is right, bake the app, the templates and the 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)

6. 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 — 31 pages, one per feature
  • To feel how it behaves: the playground
  • To hand the writing to an AI: the agents page