Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions tests/test_tms_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
collect_zoom_stats,
compute_bounds,
detect_scheme,
detect_tile_format,
generate_tilemapresource_xml,
iter_tiles,
)
Expand Down Expand Up @@ -161,6 +162,21 @@ def test_returns_both_bounds(self, tiny_tms_dir: Path):
assert tms_bounds != xyz_bounds


class TestDetectTileFormat:
def test_png(self):
assert detect_tile_format(b"\x89PNG\r\n\x1a\n" + b"\x00" * 8) == "png"

def test_jpeg(self):
assert detect_tile_format(b"\xff\xd8\xff\xe0" + b"\x00" * 8) == "jpg"

def test_webp(self):
# RIFF container with WEBP fourcc at offset 8
assert detect_tile_format(b"RIFF\x00\x00\x00\x00WEBPVP8 ") == "webp"

def test_unknown_falls_back_to_png(self):
assert detect_tile_format(b"not an image") == "png"


class TestGenerateTimemapresourceXml:
def test_contains_expected_elements(self):
xml = generate_tilemapresource_xml(
Expand Down
29 changes: 25 additions & 4 deletions tilepack/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

import click

from tilepack.tms_utils import collect_zoom_stats, compute_bounds, detect_scheme, iter_tiles
from tilepack.tms_utils import (
collect_zoom_stats,
compute_bounds,
detect_scheme,
detect_tile_format,
iter_tiles,
)


def run_convert(input_root: str, output_file: str, scheme: str | None = None) -> None:
Expand Down Expand Up @@ -50,6 +56,14 @@ def _convert_mbtiles(root: Path, out: Path, *, stats: dict, scheme: str) -> None
bounds = compute_bounds(stats, scheme=scheme)
flip_y = scheme == "xyz"

def _first_tile_format(root: Path) -> str:
"""Detect the tile format from the first tile in the folder."""
for _z, _x, _y, tile_path in iter_tiles(root):
return detect_tile_format(tile_path.read_bytes())
return "png"

tile_format = _first_tile_format(root)

# Create MBTiles database
if out.exists():
out.unlink()
Expand All @@ -75,7 +89,7 @@ def _convert_mbtiles(root: Path, out: Path, *, stats: dict, scheme: str) -> None

meta = {
"name": root.name,
"format": "png",
"format": tile_format,
"bounds": bounds_str,
"center": f"{center_lon:.6f},{center_lat:.6f},{center_zoom}",
"minzoom": str(minzoom),
Expand Down Expand Up @@ -145,6 +159,13 @@ def tile_entries():
click.echo("Reading and sorting tiles...")
entries = sorted(tile_entries(), key=lambda e: e[0])

tile_format = detect_tile_format(entries[0][1]) if entries else "png"
tile_type = {
"png": TileType.PNG,
"jpg": TileType.JPEG,
"webp": TileType.WEBP,
}[tile_format]

# Write PMTiles using the low-level writer
from pmtiles.writer import Writer as PMTilesWriter

Expand All @@ -159,7 +180,7 @@ def tile_entries():

writer.finalize(
header={
"tile_type": TileType.PNG,
"tile_type": tile_type,
"min_zoom": minzoom,
"max_zoom": maxzoom,
"min_lon_e7": int(bounds[0] * 1e7),
Expand All @@ -170,7 +191,7 @@ def tile_entries():
},
metadata={
"name": root.name,
"format": "png",
"format": tile_format,
"bounds": f"{bounds[0]:.6f},{bounds[1]:.6f},{bounds[2]:.6f},{bounds[3]:.6f}",
"minzoom": str(minzoom),
"maxzoom": str(maxzoom),
Expand Down
13 changes: 13 additions & 0 deletions tilepack/tms_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,16 @@ def generate_tilemapresource_xml(
</TileSets>
</TileMap>
"""


def detect_tile_format(tile_data: bytes) -> str:
"""Sniff the image format from a tile's magic bytes.

Returns an MBTiles/TileJSON ``format`` string ("png", "jpg", "webp").
Falls back to "png" for unrecognized data.
"""
if tile_data[:4] == b"RIFF" and tile_data[8:12] == b"WEBP":
return "webp"
if tile_data[:2] == b"\xff\xd8":
return "jpg"
return "png"