laxit/sigil

Turn an integer into one recognizable Cistercian-style glyph: SVG, ASCII or DXF.

Maintainers

Package info

github.com/laxit-patel/php-sigil

pkg:composer/laxit/sigil

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.1 2026-08-27 10:28 UTC

This package is auto-updated.

Last update: 2026-08-27 11:30:38 UTC


README

Umbral Sigilstone

๐Ÿ’œ Umbral Sigilstone โ€” PHP

laxit/sigil โ€” turns an integer into one recognizable Cistercian-style glyph, rendered as SVG, ASCII or DXF. Four digits by default, more on request.

The Umbral Sigilstone presses a number into a single struck mark: one stem and a handful of strokes, identical everywhere it is struck โ€” in pixels, in ink, or in cut metal.

Packagist Version PHP Version CI Downloads License

7323  โ†’  ๐Ÿ’œ  โ†’  svg ยท ascii ยท dxf

7323 built from 7000 + 300 + 20 + 3, one quadrant per digit

This is the PHP implementation, one language directory of the Sigil repo; the spec it implements is SPEC.md.

composer require laxit/sigil

Requires PHP 8.1+. No runtime dependencies. The package ships model.json, the declarative definition it loads, so it works with nothing else installed.

Use it

One Encoder resolves a number into a list of line segments. Each renderer draws that same list in one format โ€” so the three sections below differ only in which renderer you construct.

use Laxit\Sigil\Encoder;

$encoder = new Encoder();

ASCII โ€” terminals, logs, commit messages

Draws on its own small integer grid (4 cells per quadrant by default), with Bresenham stepping for the diagonals.

use Laxit\Sigil\Renderer\AsciiRenderer;

echo (new AsciiRenderer($encoder))->render(7323);
    |
    |\
    | \
    |  \
|---|   \
|   |\
|   | \
|   |  \
|   |   \

Read it by quadrant: top-right is ones, top-left tens, bottom-right hundreds, bottom-left thousands. Widen it with new AsciiRenderer($encoder, cells: 6).

SVG โ€” the web

One <line> per segment plus the stem, drawn slightly heavier. The viewBox is derived from the coordinates, so it stays correct at any geometry.

use Laxit\Sigil\Renderer\SvgRenderer;

echo (new SvgRenderer($encoder))->render(7323);
<svg xmlns="http://www.w3.org/2000/svg" viewBox="16 6 168 228" role="img" aria-label="Sigil glyph for 7323">
  <g stroke="#111111" stroke-width="6" stroke-linecap="round" fill="none">
  <line x1="100" y1="20" x2="100" y2="220" stroke-width="8"/>
  <line x1="100" y1="20" x2="170" y2="120"/><!-- ones.diagDown -->
  <line x1="100" y1="120" x2="30" y2="120"/><!-- tens.bottom -->
  <line x1="100" y1="120" x2="170" y2="220"/><!-- hundreds.diagDown -->
  <line x1="100" y1="120" x2="30" y2="120"/><!-- thousands.top -->
  <line x1="30" y1="120" x2="30" y2="220"/><!-- thousands.outer -->
  </g>
</svg>

Pass stroke: 'currentColor' to inherit the surrounding text colour, so the glyph themes itself:

new SvgRenderer($encoder, stroke: 'currentColor', strokeWidth: 8);

DXF โ€” laser cutters and CNC

Minimal AutoCAD R12, LINE entities only. Y is negated: CAD Y grows upward and the encoder's grows downward, so a straight copy would come off the bed upside down.

use Laxit\Sigil\Renderer\DxfRenderer;

file_put_contents('7323.dxf', (new DxfRenderer($encoder))->render(7323));
0
SECTION
2
ENTITIES
0
LINE
8
0
10
100.0
20
-20.0
...

One caveat that only matters on a real machine: quadrants meeting at the vertical midpoint share an edge, so tens.bottom and thousands.top are the same line and both are emitted. Invisible on screen; a doubled pass on a laser. examples/04-laser-cutting.php detects and strips them.

Numbers past 9999

Four places is the historical Cistercian system, not a limit of the geometry. A place is a side of the stem and a row down it, so a taller stem holds more digits โ€” as one mark, not several glyphs side by side.

9999 at two rows, through 1234567890 at five rows, each taller than the last

$wide = new Encoder(rows: 3);          // 6 places, 0-999999
$wide->digitsOf(123456);               // ones 6, tens 5, ... hundredThousands 1

Encoder::fitting(12345678);            // picks the 4 rows it needs
Encoder::maxFor(4);                    // 99999999

The default stays rows: 2 on purpose: it is the historical system, it is what the golden vectors pin, and a fixed height keeps a set of glyphs visually uniform โ€” an avatar grid where each glyph is a different height is worse than one that throws.

The range is derived from the number of places, never declared, so it cannot disagree with them. That matters: on a four-place glyph 12345 would render identically to 2345, silently losing its leading digit. Hence:

(new Encoder())->render(12345);        // InvalidArgumentException, not a wrong glyph

The ceiling is 18 places (9 rows). Past that 10^n - 1 exceeds an exact integer, so the encoder refuses rather than rounding.

CLI

php bin/demo.php 7323 ascii   # also: svg, dxf, all

Examples

examples/ has four runnable, self-contained programs โ€” input, output and implementation for each:

01-basics.php The whole API โ€” number in, segment list out, three renderers over the same list
02-avatar-fingerprint.php Identicon-style avatars: a stable mark per user, no image files, no uploads
03-wire-format.php Resolve on the server, draw in the browser with no Cistercian logic client-side
04-laser-cutting.php DXF for a laser/CNC, and the doubled-cut gotcha you must handle
composer install
php examples/01-basics.php 7323

Start with examples/README.md, which shows each one's output inline.

Six identicon avatars derived from email addresses

Renderers

Renderer Output
SvgRenderer One <line> per segment plus the stem, drawn slightly heavier. viewBox is derived from the coordinates, so it stays correct for any geometry.
AsciiRenderer Its own small integer grid (4 cells per quadrant by default), Bresenham stepping for the diagonals.
DxfRenderer Minimal R12 LINE entities. Y is negated โ€” CAD Y grows upward, the encoder's Y grows downward, so a straight copy comes off a laser upside down.

Geometry

Defaults come from geometryDefaults in model.json โ€” stemHeight=200, quadrantWidth=70, stemX=100, stemTopY=20, the values the golden fixtures were generated with. Override them per instance:

$encoder = new Encoder(stemHeight: 100, quadrantWidth: 35, stemX: 50, stemTopY: 10);

Encoder::stem() returns a positional [x1, y1, x2, y2] array, matching the fixture format.

Architecture

model.json    the definition -- segments, digit map, quadrants, geometry   (at the repo root)
SegmentModel  typed wrapper over model.json's segments + digitMap
Quadrant      typed wrapper over model.json's places + quadrants
Encoder       the generic resolver: model.json -> (x1,y1,x2,y2) segment list
Renderer/*    segment list -> one specific output format

None of these classes contain the digit map. They read model.json โ€” a vendored copy of the canonical definition in the spec repo, which diffs it against every implementation's copy on every build so it cannot drift. Encoder::locateModel() resolves it: SIGIL_MODEL, then beside the package root, then one level up.

Renderers call Encoder::segmentsFor() and Encoder::stem() and nothing else โ€” they never touch SegmentModel or Quadrant. That constraint is what makes new output formats and new language ports cheap, so it is enforced by a test (RendererTest::testRenderersDoNotReachIntoTheNumberLogic), not by review.

Tests

composer install
composer test

CI runs the suite on PHP 8.1 through 8.4, checks the fixtures are not stale, and executes every example.

The suite that matters is tests/VectorsTest.php: it replays ../fixtures/vectors.json and asserts this implementation reproduces every vector exactly. That is the whole definition of "spec-compliant", and every language implementation in this repo has the equivalent file running against the same JSON.

If the fixtures live somewhere else, point at them:

SIGIL_FIXTURES=/path/to/vectors.json composer test

Changing the glyph

model.json is the one file that defines what a glyph looks like โ€” for this package and every other language implementation. Change it in the spec repo first, then copy it here. The fixtures must be regenerated in the same change:

php bin/vectors.php            # rewrite ../fixtures/vectors.json
php bin/vectors.php --check    # CI: exit 1 if the committed fixtures are stale

Commit the regenerated fixtures alongside the change. This is a breaking change for every language implementation at once, not just for PHP.

License

MIT โ€” see LICENSE.