Skip to content

Sprite Generation


Overview

Sprites are combined image sheets containing all map icons. Mapbox GL loads a single sprite sheet to render POI icons, road shields, and other symbols efficiently. The sprite generator is a Go library with Python bindings via ctypes.


Pipeline

graph LR
    A[SVG icons + manifest.json] --> B[Layout]
    B --> C[Render]
    C --> D["sprite.png / sprite@2x.png"]
    C --> E["sprite.json / sprite@2x.json"]

1. Icon library

The input is an .iconLibrary directory:

woosmap.iconLibrary/
├── manifest.json       # Which icons to include and how
├── icons/              # SVG source icons (rendered as SDF)
└── raster/             # PNG icons (placed as-is)

2. Layout (bin-packing)

Icons are packed into a sprite sheet using a bin-packing algorithm. For stretchable icons (like road shields), the layout step also computes 9-patch metadata (content, stretchX, stretchY) so the icon can stretch to fit text labels with icon-text-fit: "both".

3. Render

For each icon in the packed layout:

  1. Draw casing at alpha ≈ 0.25 (the background shape — circle, rect, or filled)
  2. Rasterize SVG at alpha = 1.0 (the foreground icon)
  3. Raster (PNG) icons are placed directly without SDF processing

This alpha separation is what enables the dual-color trick.

4. Output

File Description
sprite.png 1x sprite sheet
sprite@2x.png 2x (Retina) sprite sheet
sprite.json 1x sprite metadata (positions, sizes, SDF flag)
sprite@2x.json 2x sprite metadata

manifest.json

The manifest lists all icons to include with their rendering options:

[
  {"name": "restaurant", "raster": false},
  {"name": "road", "raster": false, "stretchable": true, "cornerInset": 3.0, "padding": 0, "casing": "filled"},
  {"name": "logo", "raster": true}
]
Field Description
name Icon filename (without extension)
raster true for PNG icons, false for SVG/SDF
stretchable Enables 9-patch stretching
cornerInset Corner radius for stretchable content area and Filled casing
padding Padding around the icon content
casing Override casing type: "circle", "square", "filled"

Casing types

Each SDF icon is rendered with a background casing shape drawn at low alpha (≈ 0.25). At runtime Mapbox GL maps icon-halo-color to the casing and icon-color to the foreground — see SDF & Dual-Color Trick.

Kind Manifest value Description
None No casing
Rect "square" Rounded rectangle, hardcoded 4px corner radius
Circle "circle" Circle (default for POI icons)
Filled "filled" Rounded rectangle using cornerInset from manifest as corner radius

Casing resolution order

The renderer picks the casing for each icon in this order:

  1. Manifest override — if the icon entry has a "casing" field, use it
  2. Hardcoded map — a few icons (e.g. bus) have built-in casing types in the Go code
  3. DefaultCircle if nothing else matches

Python API

Building the manifest

"""Building the icon manifest for sprite generation."""

from map_style.generator.sprite import build_required_icons

required = build_required_icons(
    icon_names={"restaurant", "road"},
    raster_icons=set(),
    stretchable_icons={"road": 3.0},
    casing_overrides={"road": "filled"},
)

Generating sprites

"""Creating an icon library and generating sprites."""

from pathlib import Path

from map_style.generator.sprite import SpriteGenerator, build_required_icons

gen = SpriteGenerator()
required = build_required_icons(icon_names={"restaurant", "road"})

# Create icon library from source icons
gen.create_icon_library(
    icon_library_path=Path("woosmap.iconLibrary"),
    required_icons=required,
    icons_source_dir=Path("icons/"),
    raster_source_dir=Path("raster/"),
)

# Generate sprite sheet
gen.generate_sprite(
    icon_library_path=Path("woosmap.iconLibrary"),
    output_dir=Path("output/"),
)

Slicing an existing sprite

"""Slicing an existing sprite into individual icons."""

from pathlib import Path

from map_style.generator.sprite import SpriteGenerator

gen = SpriteGenerator()
gen.slice_sprite(
    sprite_path=Path("sprite@2x.png"),
    output_dir=Path("sliced/"),
)

Native library

The Python bindings use ctypes to call the Go shared library. The library is searched in order:

  1. PYSPRITE_LIB environment variable
  2. sprite/dist/libpysprite.{so,dylib,dll}
  3. generator/lib/libpysprite.{so,dylib,dll}

Build it locally:

make lib

Next