Military symbols (MIL-STD-2525 / STANAG APP-6) in Python — port of milsymbol.js
Project description
milsymbol-py
A reference implementation and test harness for porting milsymbol (JavaScript) to Python — with a usable frozen renderer as a side effect.
Generates SVG military symbols per MIL-STD-2525 (B/C/D/E) and STANAG APP-6 (B/D/E).
from milsymbol import Symbol
sym = Symbol("10031000001211000000", size=80) # friendly infantry
svg = sym.as_svg() # → SVG string
sym = Symbol("SHG-UCI----", size=80) # hostile infantry
svg = sym.as_svg()
The porting problem
Milsymbol is 33,000 lines of JavaScript. A naïve port — translate the JS to Python line by line — is what most people attempt, and it tends to fail. The codebase looks deceptively simple: one main class, a handful of methods, clean SVG output. But ~27,000 of those lines are data — hand-tuned SVG path coordinates for hundreds of military symbol icons, each positioned to fit precisely inside affiliation- specific frame shapes (rectangle for friendly, diamond for hostile, quatrefoil for unknown, etc.). The remaining ~5,000 lines of logic compose those icon parts based on a parsed SIDC code and render them to SVG.
An LLM (or a mid-level engineer) can translate the 5,000 lines of
composition logic reasonably well. The problem is the 27,000 lines of
icon geometry. Every path coordinate matters. A misplaced decimal in a
single d="M 85,140 30,0 c 0,-20 -30,-20 -30,0 z" produces a
visually broken symbol, and you can't verify correctness without
domain expertise in the military standards.
Our approach: extraction, not translation
Instead of translating code, we treat the JS library as an oracle:
-
Extract at build time. A Node.js tool runs the original milsymbol library and creates every valid symbol across all affiliations. For each one, it captures the fully-composed draw instruction tree (the intermediate representation milsymbol uses before SVG serialization) and the bounding box. This produces ~109,000 symbol variants serialized as gzipped JSON (~2 MB).
-
Look up at runtime. When
Symbol(sidc)is called in Python, it does a dictionary lookup on the SIDC to get the pre-composed draw instructions and bounding box. No parsing, no composition logic, no icon geometry — just a key lookup. -
Render. A compact Python renderer (~250 lines) walks the draw instruction tree and emits SVG markup, matching the JS output character-for-character.
The result: pixel-identical SVG output verified by exact string comparison against the JS reference, with a Python codebase of ~400 lines instead of 33,000.
What Phase 1 actually is
To be direct: this is not yet a port of milsymbol. It's three things:
-
A test oracle. 109K pre-computed reference symbols that any future port — Python, Rust, Go, whatever — can verify against, character by character. This is the hard part that didn't exist before.
-
A frozen renderer. It works today for every symbol in MIL-STD-2525E / APP-6 as of milsymbol 3.0.3. If you just need military symbols in Python and don't need extensibility, it's a perfectly functional library. But it's frozen at extraction time.
-
Scaffolding for the real port. The project structure, FastAPI comparison server, visual playground, and smoke tests. When someone ports the actual composition logic (Phase 2), all the verification infrastructure is already in place.
The extracted data approach cannot handle:
- Runtime extensions — milsymbol's
addSymbolPart()/addIconParts()API lets users register custom symbology. The extracted data only covers built-in symbols. - Novel SIDC combinations — modifier combinations not covered during extraction produce no output.
This is a deliberate tradeoff, not an oversight. Phase 2 — porting the ~5,000 lines of composition logic — is dramatically easier because Phase 1 exists. You port one function, run the full comparison, and know immediately what broke.
Analogy
Think of it as the difference between porting a compiler and shipping pre-compiled object files. We ran the JS "compiler" on every valid input at build time, shipped the results, and wrote a Python "linker" to assemble them into SVG. The linker is trivial; the compiler port comes later — and when it does, we already have the full test corpus.
A note on process
This project was built entirely through 5-hour conversation between Kevin Lundeen (a computer science professor at Seattle University) and Claude Opus 4.6. Kevin directed the architecture, asked the right questions, and pressure-tested the approach — but never read the milsymbol JS source code or the generated Python code directly. Claude analyzed the 33,000-line codebase, devised the extraction strategy, wrote the tooling, ported the renderer, and built the test harness. The project is a demonstration of what's possible when a senior engineer uses an LLM as a tool: the human provides judgment and direction, the machine provides the throughput and detail work. The comment that inspired this project observed that LLMs fail at tasks like porting JS libraries to Python. We didn't port the library — we found a way around the problem entirely.
Install
pip install milsymbol # core library (no dependencies)
pip install milsymbol[server] # adds FastAPI comparison server
Or from source:
git clone https://github.com/klundeen/milsymbol-py.git
cd milsymbol-py
pip install -e ".[dev]"
pytest
API
Symbol(sidc, **kwargs)
| Argument | Default | Description |
|---|---|---|
sidc |
(required) | SIDC string — number (20-digit) or letter format |
size |
100 |
Symbol size (scaling factor; 100 = base size) |
stroke_width |
4 |
Frame stroke width |
outline_width |
0 |
Outline width around the symbol |
Methods
| Method | Returns | Description |
|---|---|---|
as_svg() |
str |
Complete SVG markup |
is_valid() |
bool |
Whether the SIDC resolved to a known symbol |
get_anchor() |
dict |
{"x": float, "y": float} — placement anchor |
get_size() |
dict |
{"width": float, "height": float} — rendered size |
get_metadata() |
dict |
Parsed SIDC fields |
Affiliations
Number SIDCs encode affiliation at position 3:
| Code | Affiliation | Frame |
|---|---|---|
3 |
Friend | Rectangle (blue) |
6 |
Hostile | Diamond (red) |
4 |
Neutral | Square (green) |
1 |
Unknown | Quatrefoil (yellow) |
Letter SIDCs encode affiliation at position 1 (F/H/N/U/etc.).
Playground
A browser-based comparison tool shows JS and Python output side by side. The render areas use a parchment background so the symbols' black text labels are visible.
# Start the Python backend
pip install milsymbol[server]
uvicorn server:app --port 8000
# Serve the playground
cd playground && python -m http.server 3000
# Open http://localhost:3000/playground.html
Coverage
| Category | Count |
|---|---|
| Number SIDCs (all affiliations) | 37,828 |
| Letter SIDCs (all affiliations + echelons) | 71,388 |
| Symbol sets | 19 |
| Tests (fast) | 157 |
| Tests (full corpus, vs JS reference) | 403 |
| Corpus match rate | 109,216 / 109,216 (100%) |
| Python source lines | ~800 |
| Data (gzipped) | 2 MB |
Can I use this in production?
Yes. The library produces pixel-identical SVG to the JS library for all 109,216 symbols in the MIL-STD-2525 / STANAG APP-6 set, verified by exact string comparison against JS-generated reference SVGs. Every affiliation, every symbol set, every echelon.
Caveats:
- Frozen to milsymbol 3.0.3's built-in symbol set — no runtime extensions or custom icon registration.
- Text fields and modifiers are computed in Python. The core symbols are extracted from JS; the composition logic around them is ported.
- SVG output only (pipe through
cairosvgfor PNG if needed).
Wheel size: 1.8 MB, zero runtime dependencies.
Upstream version
Pinned to milsymbol 3.0.4
(b05f2d7).
The playground checks for newer releases on load via the GitHub API
and displays a warning if the upstream has moved ahead.
The version pin is recorded in pyproject.toml under [tool.milsymbol].
Staying in sync
When the upstream JS library updates:
- Clone the new version and build it:
git clone https://github.com/spatialillusions/milsymbol.git cd milsymbol && npm install && npm run build
- Re-run the extraction tools to regenerate data files:
node tools/extract_data.mjs ./milsymbol ./milsymbol-py/milsymbol/data
- Re-generate the corpus reference from JS:
node tools/gen_corpus_ref.mjs ./milsymbol ./milsymbol-py/milsymbol/data
- Run the corpus test to see what changed:
pytest tests/test_corpus.py -m slow -v
- Update
playground/milsymbol.jswith the new build.
A future CI job could automate steps 1–4 on a schedule, opening a PR when the upstream version changes.
Development
git clone https://github.com/klundeen/milsymbol-py.git
cd milsymbol-py
pip install -e ".[dev]"
pytest # 157 fast tests
pytest -m slow # + 403 corpus tests
ruff check milsymbol/ tests/ server.py # lint
ruff format --check milsymbol/ tests/ server.py # format check
mypy milsymbol/ server.py --ignore-missing-imports # type check
CI/CD
Every push to main and every PR runs:
- ruff lint + format check
- mypy type checking
- pytest across Python 3.10, 3.11, 3.12, 3.13
Creating a GitHub Release triggers automatic publishing to PyPI via trusted publishing.
Publishing to PyPI
TODO (Kevin): Configure trusted publishing on pypi.org: Project → Settings → Publishing → add GitHub publisher with
klundeen/milsymbol-pyand workflowpublish.yml.
Then: create a GitHub Release tagged v0.1.0 and the package
publishes automatically.
Known upstream quirks
These behaviors are inherited from the JS library and reproduced faithfully by the Python port:
- Quantity / echelon collision. The quantity text field is positioned relative to the frame bounding box, not the full symbol including echelon dots. At larger sizes, quantity text can overlap the echelon modifier.
Roadmap
- Extract test oracle (109K reference symbols)
- Frozen renderer with exact SVG match
- Visual comparison playground
- Port text field placement (quantity, type, designation, etc.)
- Port echelon / mobility / HQ / TF / feint-dummy modifiers
- Comprehensive test fixture (109K symbols vs JS reference)
- Port composition logic (Phase 2 — real port)
- Automated upstream sync (CI job to detect new milsymbol releases)
- Extension API
- PNG rasterization (via cairosvg or similar)
License
MIT — same as the original milsymbol library.
Credits
- milsymbol by Måns Beckman — the original JS library.
- Symbol geometry and icon data derived from MIL-STD-2525 and STANAG APP-6.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file milsymbol-0.1.0.tar.gz.
File metadata
- Download URL: milsymbol-0.1.0.tar.gz
- Upload date:
- Size: 1.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a048fea0f4ad5cd591e925ea9197c6b8c20ea037603b7657a2b1d112ad5c7d5
|
|
| MD5 |
ebb8cf69fce3e251adfe3d7ae8b4c505
|
|
| BLAKE2b-256 |
2b38491d42cdb8e253e30f8e06a513f60dacb1b026d868f7ce82c53bb1a87228
|
Provenance
The following attestation bundles were made for milsymbol-0.1.0.tar.gz:
Publisher:
publish.yml on klundeen/milsymbol-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
milsymbol-0.1.0.tar.gz -
Subject digest:
8a048fea0f4ad5cd591e925ea9197c6b8c20ea037603b7657a2b1d112ad5c7d5 - Sigstore transparency entry: 1152529423
- Sigstore integration time:
-
Permalink:
klundeen/milsymbol-py@6f6c6adbf8976516dc2c06c066fc203b2f4600b1 -
Branch / Tag:
- Owner: https://github.com/klundeen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6f6c6adbf8976516dc2c06c066fc203b2f4600b1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file milsymbol-0.1.0-py3-none-any.whl.
File metadata
- Download URL: milsymbol-0.1.0-py3-none-any.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
425c7fef8d5e444f28af809fff606ae7717d63e8ee402cb93fcac140bf64b832
|
|
| MD5 |
ebb86a3c799671c8c7867f65e72156c4
|
|
| BLAKE2b-256 |
62080c4c41aeb1c41833706271d6ff7d3820221b0cd03ef596965665ca171919
|
Provenance
The following attestation bundles were made for milsymbol-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on klundeen/milsymbol-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
milsymbol-0.1.0-py3-none-any.whl -
Subject digest:
425c7fef8d5e444f28af809fff606ae7717d63e8ee402cb93fcac140bf64b832 - Sigstore transparency entry: 1152529621
- Sigstore integration time:
-
Permalink:
klundeen/milsymbol-py@6f6c6adbf8976516dc2c06c066fc203b2f4600b1 -
Branch / Tag:
- Owner: https://github.com/klundeen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6f6c6adbf8976516dc2c06c066fc203b2f4600b1 -
Trigger Event:
release
-
Statement type: