Skip to content

digidem/styled-map-package-python

Repository files navigation

styled-map-package

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.

Scope

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.

Install

This package is not published to PyPI. Install it from a source checkout:

pip install .

Reading

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.

Writing

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.

Deduplication

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.

Low-level archive API

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],
])

Development

python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest

The 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.sh

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

License

MIT — see LICENSE.

About

Python reader and writer for the Styled Map Package (`.smp`) format

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors