Note
Vibe coding warning. Every line of code and documentation in this repository was generated by an AI model, with a human directing it and deciding what to keep. Read it as such: the code is workmanlike at best, the prose is more confident than the engineering behind it deserves, and nobody has reviewed it the way a maintained project would be reviewed.
The certificates it produces, on the other hand, are real. They were designed carefully, checked against Japanese practice and are printed and issued by an actual dōjō. If you are here for the documents rather than the software, that is the part to trust.
Generates print-ready dan-grade certificates (段位免状), kyū-grade certificates (級位認定証, for the grades below first dan) and teaching licences (免許状) for a martial-arts dōjō. The dan certificates and the licences are A3 or A4 landscape, vertical Japanese, in the manner of a traditional 賞状 (award certificate); the kyū certificate is a simpler A4 portrait sheet in horizontal lines, with the crest as a light watermark. The grade, the recipient and the date can each be printed or left blank for a calligrapher to brush in.
Three of the documents as an unconfigured build produces them.
Everything that identifies a school lives in a TOML file. Out of the box the generator names nobody: it prints 〇〇流柔術 and 〇〇道場 – the ordinary Japanese placeholder – so an unconfigured build produces a correctly typeset certificate that grants nothing.
uv run menkyo --list # what a default build would write
uv run menkyo # write it
uv run menkyo --paper a4 # the same certificates on A4
Needs Python 3.11+ (for tomllib) and nothing else: uv run installs
Pillow, NumPy, SciPy, fontTools and pypdfium2, and the fonts ship inside the
package. No other program and no system font is used. Without uv,
pip install -e . and then plain menkyo.
From a checkout, files land in build/ as <award>-<variant>-<size>.<ext>.
The size is a3 or a4, and any suffix comes after it, so the example
writes dan-overlay-a3-sample.pdf and, for its kyū sheet,
kyu-overlay-a4-sample.pdf.
This repository is the generator and its menkyo command, and nothing else.
The front end is dojo-portal, a
separate Django application that runs the command in a subprocess.
Build the wheel from a checkout:
uv build --wheel # dist/menkyo-<version>-py3-none-any.whl
It holds the generator, its fonts, the example configuration and the menkyo
command. The consumer installs that file, for example as a uv path source or
with pip:
# from the consumer's project, next to this checkout
uv add ../menkyo-menjo-builder/dist/menkyo-<version>-py3-none-any.whl
pip install --force-reinstall ../menkyo-menjo-builder/dist/menkyo-<version>-py3-none-any.whl
The file name carries the version, so after a bump point the path source at
the new wheel. A rebuilt wheel of the same version needs a nudge: with uv,
run uv lock --upgrade-package menkyo && uv sync, because uv.lock pins the
wheel's hash (a plain uv sync stops on a hash mismatch); with pip,
--force-reinstall replaces it, which pip would otherwise skip. Or bump the
version for every build the consumer takes.
The version lives in pyproject.toml and nowhere else: bump it, commit and
build the wheel again. menkyo --version reports which one is installed.
An installed copy has no checkout to sit in, so it writes to
~/Documents/Menkyo unless given --out, and reads a dōjō's settings from
~/Library/Application Support/Menkyo/dojo.toml (~/.config/Menkyo/ on
Linux) when no --config is given.
Nothing needs installing besides the wheel and its own dependencies: no program, no font, no system library. The text is drawn from the two fonts inside the package, and pdfium (pypdfium2) draws the PNGs.
Run it as a command, not as a library: the generator keeps its state in module
globals, and an inked certificate takes a couple of hundred MB, which a
separate process gives back when it ends. Write the dōjō's settings to a TOML
file (menkyo --example-config prints the template), then:
menkyo --config=dojo.toml --awards=dan --variants=overlay --formats=pdf \
--name=ヤン・ヤンセン --rank=二段 --date=2026-09-05 --out=/tmp/xyz --json
Pass every fill-in flag, '' included, so a [sample] in the configuration
can never reach a real certificate, and use the --flag=value form so a value
that starts with - is not read as a flag. With --json, every line on
stdout is one JSON object, written as it happens:
| event | fields | when |
|---|---|---|
progress |
step, total, award, variant, stage, label |
a slow step starts; step counts from 1 |
file |
path |
a file has been written |
done |
out, files |
the run finished; the exit code is 0 |
error |
message, optionally detail |
the run failed; the exit code is 1 |
planned |
file |
with --list: what would be written |
The command above writes, in part:
{"event": "progress", "step": 1, "total": 3, "award": "dan", "variant": "overlay", "stage": "text", "label": "dan overlay: rendering the text"}
{"event": "file", "path": "/tmp/xyz/dan-overlay-a3-二段-ヤン-ヤンセン.pdf"}
{"event": "done", "out": "/tmp/xyz", "files": ["/tmp/xyz/dan-overlay-a3-二段-ヤン-ヤンセン.pdf"]}
and a failure ends in one line such as
{"event": "error", "message": "unknown award: nope …"}.
A character the fonts do not have is a failure too, found before anything
is written. Only what will be drawn is checked, each string against the
font it is printed in: the selected awards, the honorific only with a name,
a [sample] grade only where there is a grade column, the precept only on a
sheet that draws it and, where there is a crest, the art's name against both
fonts, since it is also set on the crest ring. The kyū sheet is set in Noto
Serif JP, so everything it prints, its body included, is checked against
Noto Serif JP rather than Yuji Boku. It prints no precept and no formula, so
a run that builds only kyū sheets checks neither, and with crest = false
it does not check the ring either. The error event names the character,
its code point and where it came from, and asks for a replacement, for
example the character 𠮷 (U+20BB7) in --name is not in Yuji Boku, ….
There is no fallback face: a kanji from another font among brush strokes
would be visibly wrong. Ask the dōjō for a variant the font has (吉 for 𠮷).
stage is one of text, ink, pdf or png, for a caller that
wants its own wording; label says the same in English. total is known from
the first event, so step / total is a progress bar. A certificate takes a
few seconds, most of it in ink. Without --json, the same
progress goes to stderr when that is a terminal, and stdout keeps its wrote
lines.
Every failure ends the output with exactly one error, a rejected flag or an
unwritable --out included, and never a done. --list --json writes one
planned per file and no done; its exit code says it worked. --help,
--version and --example-config print plain text even with --json: they
are for a person, and stop before anything runs.
Copy the example and edit it:
uv run menkyo --example-config > dojo.toml
dojo.toml in the repository root is picked up automatically and is
git-ignored, so your school's identity never lands in a fork. --config PATH
chooses between several. A per-user dojo.toml (see ‘As a package’) takes
precedence over the repository root's when it exists, in a checkout too.
[art]
name = "〇〇流柔術" # the art, printed and on the crest's upper arc
[dojo]
name = "〇〇道場" # the dōjō, printed below it
crest_ring = "" # four characters on the crest's lower arc
[signer]
title = "師範" # the office that grants the award
name = "" # the signatory, in katakana
[motto]
text = "" # an optional school precept, in its own column
# (vertical sheets only)
[paper]
size = "a3" # or "a4"; --paper overrides it for a run
border = 22.0 # mm, the margin kept clear all round — see "Printing"
safe_inset = 38.0 # mm, where the text may begin
text_drop = 0.05 # fraction of page height — see "Where the columns start"
[[awards]]
slug = "dan"
heading = "段位免状" # names the document
formula = "右授与ス" # "the above is hereby awarded"
grade_column = true # the body also states a grade: 初段, 二段 …
[[awards]]
slug = "kyu"
heading = "級位認定証" # Kyū Grade Certificate
grade_column = "kyu" # a grade counted in kyū: 五級, 準五級 …
body = [ # the text below the grade, one string per line
"上記の者は本会所定の審査に合格したので",
"ここに認定します",
]
watermark = 0.08 # the crest's grey behind the text, 8 % of blackEach [[awards]] block becomes one --awards slug, so a school with a
different set of documents adds, removes or renames them here and nowhere
else. grade_column is the one subtle key: a dan certificate states a grade
in the body as well as in the heading, whereas a licence has already named
its award in the heading and would otherwise leave an empty column behind the
recipient's name. Setting it false closes the layout up. It also says what
--rank counts in: true (or "dan") in dan, "kyu" in kyū, so -r 5 is
五段 on the one and 五級 on the other. An explicit --rank on a licence
counts in dan.
The unit also decides the sheet. An award counting in kyū gets the kyū sheet, a simpler one for the grades below shodan (first dan): A4 portrait, read in horizontal lines, set in Noto Serif JP without the ink grading, with the crest as a light watermark behind the text and no precept.
Where a vertical sheet prints its formula, the kyū sheet prints body: one
string per printed line, one to three lines. Japanese has no spaces to
break a line at, so you choose the breaks. Without the key the sheet prints
the two lines above. formula is not needed on a kyū award and is ignored
there.
watermark is the crest's grey as a fraction of black, above 0 and up to 1.
The default, 0.08, is a light grey. At about 0.93 the crest is as dark as
the text, and above that darker. body and watermark are checked on every
award but used only on the kyū sheet.
crest, true unless set, says whether the sheet draws the crest: false
leaves it off a vertical sheet and leaves the watermark off the kyū sheet.
The precept prints on every vertical sheet when [motto] text has one.
The per-award motto key of 0.5.0 is gone. A configuration that still sets
it is refused with a message naming the file, the award and the key, so a
0.5.0 kyū block is refused until you delete its motto line. Delete its
crest = false as well if you want the watermark; its
formula = "右認定ス" can stay.
A colour key, which earlier versions read for a drawn frame, is still
accepted and ignored: the certificates print in black on plain white paper.
text_drop shifts every printed column's starting point down by that
fraction of the page height, giving the reading order more air above it
without resizing or re-tuning anything. 0.05 is the shipped taste; 0
reproduces the original layout, columns starting right under the crest.
It moves every column by the same amount, so a large value can push a tall
column past safe_inset. The one that matters is the date left blank:
its fourteen slots are the tallest column on the sheet, and at 0.05 it
still clears safe_inset by a few mm. Check with
menkyo -v proof -d '' before raising it further.
The kyū sheet has no columns, and text_drop does not move it: its lines
keep their own places.
The crest ring is set in a font by default, which is fine at 5 mm. If you have
your own crest and want its actual lettering, potrace each character to SVG,
drop them in a crest/ folder beside your dojo.toml, named 安.svg,
心.svg and so on, and set traced_glyphs = true under [dojo].
Compare the two before you commit to it. Display lettering is usually drawn
already distorted to fit its arc, and tracing it puts a second arc on top of
the first – which is why this is off by default. crest/ is git-ignored for
the same reason dojo.toml is.
The default crest (mon) is the yotsume-bishi, four lozenges in a diamond, a
traditional family crest (kamon); its proportions are
constants at the top of src/menkyo/build_diploma.py.
| flag | meaning |
|---|---|
-c, --config PATH |
which configuration to use (default: dojo.toml, else the shipped example) |
--example-config |
print the shipped example configuration and stop |
-a, --awards LIST |
which awards, or all (default: all) |
-v, --variants LIST |
which variants, or all (default: overlay) |
-f, --formats LIST |
pdf, png, svg (default: pdf) |
--no-ink |
solid black instead of graded sumi (brush ink) |
-n, --name KATAKANA |
print the recipient's name instead of leaving it blank |
-r, --rank GRADE |
print the grade: 2 or nidan → 二段, 1 → 初段 on a dan award; 5, gokyu or yellow → 五級 on a kyū award; or literal text |
-d, --date YYYY-MM-DD |
print the award date in kanji; today also works |
--honorific TEXT |
appended to a printed name (default from the config; '' to omit) |
--suffix TEXT |
tag for output filenames (defaults to the name) |
--paper SIZE |
the sheet, a3 or a4: landscape (default: [paper] size, else a3), and portrait for the kyū sheet (default: a4); given, it applies to every award |
--dpi N |
resolution for PNG output, 1–2400 (default: 300) |
--all |
shorthand for --awards all --variants all |
--clean |
empty build/ first |
--list |
list what would be written, then stop |
--json |
one JSON object per line on stdout, for programs (see above) |
--version |
print the version and stop |
menkyo -a dan -r 5 -n 'ヤン・ヤンセン' -d 2024-07-01
menkyo -a dan -v proof -f png # one proof sheet
menkyo --all -f pdf,png # every variant, PDF and PNG
| variant | what it is |
|---|---|
overlay |
the certificate, its text graded like sumi – the one to print |
overlay-flat |
solid black instead of graded sumi, pure vector |
proof |
safe areas and the zones the calligrapher fills |
Only overlay is built by default. All three are the same layout on plain
white paper. overlay and overlay-flat are black text and crest, with no
frame and no background; proof adds pale guides.
The kyū sheet is set in type and never graded, so its overlay is the
certificate as it prints, text in black over the pale watermark, and
overlay-flat is the same drawing, byte for byte; --no-ink changes
nothing on it. Its proof adds the margins, the zones for a blank name,
grade or date, and a circle for a seal right of the signatory's name.
Three slots are normally left for the calligrapher – the grade, the recipient and the date – and any of them can be printed instead.
--rank takes a number or romaji (Japanese in Latin letters) and writes the
kanji: 2 or nidan becomes 二段, and 1 becomes 初段 – first dan has its
own word, never 一段. Plain numerals are used rather than the formal 弐段; pass
the literal text if you want the formal forms (--rank 弐段). Anything that is
not a dan grade passes through untouched, so a licence can use the same flag:
--rank 師範. The exception is a kyū form (below): on a dan award it is
refused rather than printed in Latin letters. Either way, ū or uu reads as u
(jūdan → 十段).
The award decides the unit. On a kyū award (grade_column = "kyu") the same
flag counts in kyū (級), from 10 kyū, the first grade, up to 1 kyū, just below
shodan:
1to10, or the romajiikkyu…gokyu…jukkyu:5andgokyuare 五級;- the belt:
white六級,yellow五級,orange四級,green三級,blue二級,brown一級; - the children's steps between two belts, written with 準 (associate):
white-yelloworjun5準五級,yellow-orangeorjun4準四級,orange-greenorjun3準三級; - kanji as they should print:
--rank 準五級,--rank 三級.
Anything else is an error that lists these forms, a dan form such as nidan
included. On a vertical sheet a grade of more than two characters (準五級) is
set smaller to fit the space two take; on the kyū sheet there is room across
the page, and it keeps its size.
--date takes an ISO date, or today, and writes 西暦二〇二六年九月五日: the
year digit by digit as 西暦 (the Western calendar) requires, month and day as
spoken numbers (十二月, 三十一日). Leave it off and the column keeps an empty
grid to brush into – worth considering, since a sheet with the date already on
it is one you cannot hold back if a grading slips.
--name prints the recipient in katakana, in its own column between the
heading and the award, and moves the brushed rank slot left to make room; on
the kyū sheet, on its own line below the heading. The filename picks the name
up so one recipient's files never overwrite another's. Long names shrink to
fit.
How a Western name should be written in katakana is a judgement call. Dutch Geert could be ヘールト, ゲールト or フェールト – the hard G has no Japanese equivalent and every choice is a compromise. Agree it with the recipient before printing: it is the one word on the sheet they will look at hardest.
- Plain white paper, A3 or A4.
- Print
<award>-overlay-<size>.pdfat 100 % – no ‘fit to page’, no scaling. An A3 page is exactly 420 × 297 mm, an A4 page 297 × 210 mm; check with a ruler on the first sheet. borderandsafe_insetunder[paper]are margins on the A3 sheet:borderis kept clear all round (22 mm by default),safe_insetis where the text may begin (38 mm). Nothing prints them;-v proofdraws both, so you can check a layout against your printer's own unprintable edge.
A4 is the A3 certificate scaled by 1/√2 (about 0.71): the same layout,
crest and margins, all proportionally smaller, so border is 15.6 mm on A4
and the 12 mm columns are 8.5 mm. Choose it with --paper a4, or with
size = "a4" under [paper] as the dōjō's default.
The kyū sheet is A4 portrait, 210 × 297 mm, whatever [paper] size
says; --paper a3 makes it A3 portrait, 297 × 420 mm, the same layout scaled
up by √2. Its margins are those of the landscape A4: border and
safe_inset scaled by 1/√2.
Make a test print of the kyū sheet's watermark. The crest behind the
text is a light grey, 8 % of black by default (watermark = 0.08). A home
printer may print a grey that light too faintly to see, or in visible bands
or speckles. Print the overlay once at 100 % and look: if the crest is hard
to see, raise watermark a little (0.12); if it competes with the text,
lower it.
Print a -flat variant alongside your first real print. The graded text is
a greyscale raster; a home laser halftones grey, and some printers mishandle
its transparency. If the columns come out mottled, the -flat files are
the same layout in solid black, pure vector. Nothing else differs.
The PDFs are print files: the text is drawn as glyph outlines, so it cannot be searched or copied, and no font is embedded. The overlays are pure grey (DeviceGray), with no colour at all.
The printed text is set in Yuji Boku (佑字 木), a genuinely brush-written
kaisho (regular script), then graded by src/menkyo/ink.py so the ink thins
towards grey where the brush lifts – a real brush never lays down flat black.
The crest ring is set in Noto Serif JP SemiBold, a mincho (the printed
serif style), since a mon is a carved seal rather than brushwork. The kyū
sheet is set entirely in Noto Serif JP, in solid black, with no grading: a
plainer certificate, distinct from the brushed dan and licence sheets.
Both fonts ship in
src/menkyo/fonts/ and are read from there by file, never looked up by name,
so nothing needs installing and nothing on your machine changes or is used.
DECISIONS.md records why each line reads as it does, and how the ink was
arrived at.
src/menkyo/ the package: everything the wheel ships
build_diploma.py generator and CLI; typography constants at the top
render.py the SVG as PDF (glyph outlines) and PNG (pdfium)
png.py PNGs written and read a band of rows at a time
config.py the TOML configuration
paths.py where things live: checkout or installed
ink.py the sumi grading
example.toml placeholder configuration, the shipped default
fonts/ Yuji Boku and Noto Serif JP SemiBold + their OFL licences
tests/ the suite; tests/docker/ runs it on Linux
docs/ templates explaining each document to the person
who receives it, plus the image above
build/ generated; git-ignored
A certificate in a language its recipient cannot read is worth accompanying
with a page that says what it says. docs/ holds one such page per
document, written for exactly that reader.
They explain in full everything the generator prints on its own – the headings, the award and licence formulas, the date, 殿 (the honorific after the name), the reading order, the default crest. The four things that are configuration are described only as far as the form goes, each marked with a block beginning ‘If you are adapting this document’. Replace those with your own account: what your crest means, what your school's name means, who signs. Nobody else can write that part.
This produces credentials. It is a typesetting tool and it will cheerfully set any school's name in any grade – the placeholders exist so that it does not do so by default, but they are placeholders and not a lock.
Awarding rank in a Japanese martial art is somebody's prerogative, and usually not the prerogative of whoever has a printer. Configure it for the documents you are actually entitled to issue.
MIT – see LICENSE. Two things are not covered by it: the bundled fonts, which are under the SIL Open Font License, and crests and school names, which are nobody's to grant. See NOTICE.
