diff --git a/tests/test_tms_utils.py b/tests/test_tms_utils.py index ec6f749..2d1f05b 100644 --- a/tests/test_tms_utils.py +++ b/tests/test_tms_utils.py @@ -9,6 +9,7 @@ collect_zoom_stats, compute_bounds, detect_scheme, + detect_tile_format, generate_tilemapresource_xml, iter_tiles, ) @@ -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( diff --git a/tilepack/convert.py b/tilepack/convert.py index 6510acc..a4415e3 100644 --- a/tilepack/convert.py +++ b/tilepack/convert.py @@ -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: @@ -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() @@ -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), @@ -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 @@ -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), @@ -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), diff --git a/tilepack/tms_utils.py b/tilepack/tms_utils.py index f430918..d6cd487 100644 --- a/tilepack/tms_utils.py +++ b/tilepack/tms_utils.py @@ -216,3 +216,16 @@ def generate_tilemapresource_xml( """ + + +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"