A Python reader and writer for the
Styled Map Package (.smp)
format: a ZIP archive holding everything needed to render a MapLibre map offline
— a style.json document, vector and/or raster tiles, glyphs, and sprites.
It is a port of the reader and writer from the JavaScript reference implementation. It uses only the Python standard library. It implements SMP format version 1.0.
This is a reader and writer only; there is no downloader, HTTP server, or viewer.
It does not migrate or validate styles. Pass a valid MapLibre v8 style to
Writer; v7 migration and the MapLibre style validator are out of scope and
remain the caller's responsibility. Invalid styles and unknown properties on the
style are preserved.
This package is not published to PyPI. Install it from a source checkout:
pip install .from styled_map_package import Reader
with Reader("map.smp") as reader:
reader.get_version() # "1.0"
style = reader.get_style("http://host/maps/a") # smp:// URIs rewritten to a base URL
style = reader.get_style() # or keep the raw smp://maps.v1/ URIs
res = reader.get_resource("s/0/2/1/1.mvt.gz")
res.data, res.content_type, res.content_encoding, res.content_length, res.resource_type
reader.read("s/0/2/1/1.mvt.gz") # raw entry bytes
reader.has("s/0/2/1/1.mvt.gz")
reader.namelist()Reader(file, *, max_entries=500_000, max_resource_size=20*1024*1024) accepts a
path, bytes, or a binary file object. It rejects unsafe entry names (..,
absolute paths, drive letters) when opening, normalizes names to Unicode NFC,
and enforces the entry-count and resource-size limits to guard against malicious
archives.
from styled_map_package import Writer
style = {
"version": 8,
"name": "My map",
"sources": {"roads": {"type": "vector", "url": "https://example.com/tiles.json"}},
"layers": [
{"id": "bg", "type": "background"},
{"id": "roads", "type": "line", "source": "roads", "source-layer": "roads"},
],
}
writer = Writer(style)
writer.add_tile(tile_bytes, z=0, x=0, y=0, source_id="roads", format="mvt")
writer.add_glyphs(glyph_pbf_gz, font="Open Sans Regular", range="0-255")
writer.add_sprite(json=sprite_index, png=sprite_png)
writer.save("map.smp")add_tile expects XYZ tile coordinates (convert TMS first with tms_to_xyz_y)
and accepts bytes, a file path, or a binary file object; the format is detected
from the tile's magic bytes when omitted.
From the sources and tiles you add, the writer rewrites resource URLs to the
smp://maps.v1/ scheme, reduces text-font stacks to a single available font,
computes the smp:bounds, smp:maxzoom, smp:sourceFolders and
smp:bufferTiles metadata, drops sources and layers that have no data, and
orders and compresses entries as the specification recommends. A source declared
in the style becomes a tile source the first time a tile is added for it.
save() raises a subclass of styled_map_package.errors.SMPError when the
archive is incomplete or inconsistent, such as MissingSourcesError,
MissingFontsError, MissingSpriteError, SourceNotFoundError,
TileFormatMismatchError, or DuplicateEntryError.
Writer(style, dedupe=True) stores byte-identical tiles once, with extra
central-directory entries aliasing the shared data (spec §3.6). It helps
archives with many repeated tiles, such as empty ocean, but produces archives
that some general-purpose ZIP tools (macOS Finder, Info-ZIP, Go's archive/zip)
reject, so it is off by default. This package's reader and the JavaScript
reference reader both read deduplicated archives.
write_smp_archive assembles an archive from explicit entries without touching
a style, handling entry ordering, per-extension compression, ZIP64, and optional
deduplication. Use it when you build style.json yourself, as the
QGIS plugin does:
from styled_map_package import write_smp_archive, ArchiveEntry
write_smp_archive(output_path, [
ArchiveEntry(name="style.json", path=style_json_path),
ArchiveEntry(name="VERSION", data=b"1.0"),
*[ArchiveEntry(name=f"s/0/{z}/{x}/{y}.png", path=p) for z, x, y, p in tiles],
])python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytestThe cross-language interop tests drive the JavaScript reference implementation
(styled-map-package-api
on npm) with Node.js, and read real .smp fixtures it produces (committed in
the JS source repo, not on npm). scripts/test-interop.sh installs the npm
package, downloads the fixtures, and runs the interop tests:
./scripts/test-interop.shThe styled-map-package-api version is pinned once in package.json; the
fixtures are fetched from the upstream release tag matching the installed
version (resolved via npm ls), so they cannot drift from the package under
test. The Node-driven tests skip when Node.js or the npm package is
unavailable; the fixture-reading tests skip until
scripts/fetch-js-fixtures.sh has downloaded them to tests/fixtures/js
(override the download location with SMP_JS_FIXTURES, or the source ref with
SMP_JS_REF).
MIT — see LICENSE.