Thanks for considering it. This file tells you what kinds of contribution fit, and what the project will not accept — knowing the second saves you more time than the first.
These are the most useful things you can send, and neither requires understanding the internals.
If you own an e-ink tablet or reader that is not in ctrlgrid/data/devices.yaml,
add it:
- id: my-device
name: "Manufacturer Model"
pixels: { x: 1620, y: 2160 } # ALWAYS portrait
physical: { x: 179.7mm, y: 239.6mm } # computed from pixels ÷ density
density: 229dpi
color: grayscale
margin: 0mm
quirks: []
source: "https://… or 'owner-verified — how you checked'"
verified: 2026-07source and verified are mandatory. Device specifications are exactly the
kind of data that spreads quietly and becomes quietly wrong. A profile without a
source will not be merged.
Owner-verified beats a manufacturer page. If you generated a PDF at those
dimensions and it fit your device exactly, say so — that is stronger evidence
than a spec sheet, and secondary sources are demonstrably unreliable here (one
widely repeated figure for the reMarkable Paper Pro is simply wrong; see
docs/research.md).
Sanity-check before you submit. Pixels ÷ density should give the physical size, and the diagonal should match the advertised one. If they disagree, one of the numbers is wrong — say which you trust and why.
Presets live in ctrlgrid/data/presets/*.yaml and are ordinary definition files.
There is no separate mechanism for them, which is deliberate: a preset is a
worked example, and the presets are also the documentation.
That means a preset should be readable, not merely correct. Name it for what it
produces (millimeter-a4, calligraphy-a4), comment the non-obvious values, and
prefer the plain form over the clever one.
Read the specification first. It is docs/pflichtenheft-vorlagengenerator.md — in
German, and deliberately not part of the shipped package. It records not just
what the tool does but why, and most design questions you will have are
answered there with reasoning.
If you disagree with a decision, open an issue naming the section. Several decisions look arbitrary and are not.
These are architectural, not stylistic. A pull request that breaks one will be asked to change, however good the rest is.
reportlabappears only inctrlgrid/writers/pdf.py, andtests/test_architecture.pyenforces it. No generator, no model, no CLI code imports it. The second writer is dead before it exists otherwise.- Positions are integer micrometres. Never accumulate floats to compute a coordinate. The tool's one promise is dimensional accuracy.
- Generators yield marks, they do not build lists. A 200-page dot grid is hundreds of thousands of objects.
- Fail loudly, never silently mangle. Validate before writing the first page; abort completely or build completely. An error message that does not let the user act is a bug.
- Everything user-visible is English — keys, code, messages, preset names. No translation layer.
- Reproducible output. Same input, same bytes. No
hash(), no wall-clock time, no locale-dependent formatting in the document.
ctrlgrid is a handle with blades. The handle owns page geometry, header,
footer, frame, stamp, page loop and output; a blade only produces marks. Three
interfaces are contracts and are specified with signatures in § 3.6 of the
specification:
- definition file → validated model
- generator → marks
- marks → writer (bidirectional: the writer also answers text-metric queries)
Everything else is an implementation detail and may change.
A new blade is a registry entry in ctrlgrid/generators/__init__.py plus one
module; generators/lines.py is the worked example. It gets the page loop,
header, footer, framing and output for free — and whether that stays true is the
test of whether the architecture holds.
Beyond generate, seam 2 asks a blade three questions: is_page_invariant
(§ 10.1), describe for the run report (§ 5.3), and periodic_axes, which is
how the handle snaps and places the surplus without ever reaching into the blade
(§ 8.3, § 8.5). A blade with no periodic families returns nothing from the last
one, and that is what makes snap an error there rather than a no-op.
Before proposing one, check § 2 of the specification. Some things are excluded on purpose: music engraving, free paths and curves, a general drawing language, a plugin system, a GUI.
The mark vocabulary is six primitives (§ 6). It is kept small precisely so that
a foreign writer is an evening's work. A writer that cannot do everything is
fine — report what you support via capabilities() and the pre-flight check
will refuse definitions that need more.
uv run pytestThe important test is not a unit test: tests/test_dimensional.py generates a
PDF, reads it back, and checks the MediaBox and mark coordinates against
expected values. If you touch geometry, that is the one that matters.
Golden-file comparisons check parsed geometry, never bytes — otherwise a
reportlab update breaks the suite.
- One concern per pull request.
- Say what you verified, and how. "Tests pass" is less useful than "printed it at 100 % and measured 5.0 mm".
- If you changed behaviour the specification describes, update the specification in the same pull request. A spec that drifts from the code is worse than none.
By contributing you agree that your contribution is licensed under the MIT
licence, as in LICENSE.