Skip to main content

Military symbols (MIL-STD-2525 / STANAG APP-6) in Python — port of milsymbol.js

Project description

milsymbol-py

Tests

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:

  1. 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).

  2. 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.

  3. 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:

  1. 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.

  2. 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.

  3. 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 cairosvg for 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:

  1. Clone the new version and build it:
    git clone https://github.com/spatialillusions/milsymbol.git
    cd milsymbol && npm install && npm run build
    
  2. Re-run the extraction tools to regenerate data files:
    node tools/extract_data.mjs ./milsymbol ./milsymbol-py/milsymbol/data
    
  3. Re-generate the corpus reference from JS:
    node tools/gen_corpus_ref.mjs ./milsymbol ./milsymbol-py/milsymbol/data
    
  4. Run the corpus test to see what changed:
    pytest tests/test_corpus.py -m slow -v
    
  5. Update playground/milsymbol.js with 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-py and workflow publish.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

milsymbol-0.1.0.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

milsymbol-0.1.0-py3-none-any.whl (1.9 MB view details)

Uploaded Python 3

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

Hashes for milsymbol-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8a048fea0f4ad5cd591e925ea9197c6b8c20ea037603b7657a2b1d112ad5c7d5
MD5 ebb8cf69fce3e251adfe3d7ae8b4c505
BLAKE2b-256 2b38491d42cdb8e253e30f8e06a513f60dacb1b026d868f7ce82c53bb1a87228

See more details on using hashes here.

Provenance

The following attestation bundles were made for milsymbol-0.1.0.tar.gz:

Publisher: publish.yml on klundeen/milsymbol-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for milsymbol-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 425c7fef8d5e444f28af809fff606ae7717d63e8ee402cb93fcac140bf64b832
MD5 ebb86a3c799671c8c7867f65e72156c4
BLAKE2b-256 62080c4c41aeb1c41833706271d6ff7d3820221b0cd03ef596965665ca171919

See more details on using hashes here.

Provenance

The following attestation bundles were made for milsymbol-0.1.0-py3-none-any.whl:

Publisher: publish.yml on klundeen/milsymbol-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page