terraink-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: terraink_py
3
+ Version: 0.1.0
4
+ Summary: Standalone Python CLI for generating styled OSM map posters as PNG and SVG.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: Pillow>=10.4.0
8
+
9
+ ## Python CLI
10
+
11
+ The repo now also ships a standalone Python renderer that skips the website entirely and writes map posters directly to `png` and `svg`.
12
+
13
+ Use `uv` in this repo:
14
+
15
+ ```bash
16
+ uv sync
17
+ ```
18
+
19
+ Generate a poster from a place name:
20
+
21
+ ```bash
22
+ uv run terraink \
23
+ "Ganjingzi District, China" \
24
+ --theme midnight_blue \
25
+ --layout print_a4_portrait \
26
+ --distance-m 4000 \
27
+ --format png svg \
28
+ --output outputs/ganjingzi
29
+ ```
30
+
31
+ `--location "..."` still works; the positional argument is just a shorter alias.
32
+
33
+ If you want the CLI installed globally after publishing:
34
+
35
+ ```bash
36
+ uv tool install terraink_py
37
+ terraink --help
38
+ ```
39
+
40
+ Build and publish to PyPI:
41
+
42
+ ```bash
43
+ uv build
44
+ uv publish
45
+ ```
46
+
47
+ Generate from coordinates in Python code:
48
+
49
+ ```python
50
+ from pathlib import Path
51
+
52
+ from terraink_py import PosterRequest, generate_poster
53
+
54
+ result = generate_poster(
55
+ PosterRequest(
56
+ output=Path("outputs/ganjingzi"),
57
+ formats=("png", "svg"),
58
+ lat=38.862405,
59
+ lon=121.513525,
60
+ title="Ganjingzi District",
61
+ subtitle="China",
62
+ theme="midnight_blue",
63
+ width_cm=21,
64
+ height_cm=29.7,
65
+ distance_m=4000,
66
+ include_buildings=True,
67
+ )
68
+ )
69
+
70
+ print(result.files)
71
+ ```
72
+
73
+ Notes:
74
+
75
+ - The Python renderer uses Nominatim + Overpass directly, so it is designed for city and regional posters rather than world-scale exports.
76
+ - `svg` output is true vector geometry, not a browser screenshot wrapped in SVG.
77
+ - Chinese place names now auto-fallback to common CJK system fonts on macOS/Linux; if your machine still lacks glyph coverage, pass `--font-file /path/to/font.ttf`.
78
+ - PNG export needs `Pillow`; `uv sync` will install it automatically.
79
+ - The PyPI package name is `terraink_py`, while the CLI command is `terraink`.
80
+
@@ -0,0 +1,72 @@
1
+ ## Python CLI
2
+
3
+ The repo now also ships a standalone Python renderer that skips the website entirely and writes map posters directly to `png` and `svg`.
4
+
5
+ Use `uv` in this repo:
6
+
7
+ ```bash
8
+ uv sync
9
+ ```
10
+
11
+ Generate a poster from a place name:
12
+
13
+ ```bash
14
+ uv run terraink \
15
+ "Ganjingzi District, China" \
16
+ --theme midnight_blue \
17
+ --layout print_a4_portrait \
18
+ --distance-m 4000 \
19
+ --format png svg \
20
+ --output outputs/ganjingzi
21
+ ```
22
+
23
+ `--location "..."` still works; the positional argument is just a shorter alias.
24
+
25
+ If you want the CLI installed globally after publishing:
26
+
27
+ ```bash
28
+ uv tool install terraink_py
29
+ terraink --help
30
+ ```
31
+
32
+ Build and publish to PyPI:
33
+
34
+ ```bash
35
+ uv build
36
+ uv publish
37
+ ```
38
+
39
+ Generate from coordinates in Python code:
40
+
41
+ ```python
42
+ from pathlib import Path
43
+
44
+ from terraink_py import PosterRequest, generate_poster
45
+
46
+ result = generate_poster(
47
+ PosterRequest(
48
+ output=Path("outputs/ganjingzi"),
49
+ formats=("png", "svg"),
50
+ lat=38.862405,
51
+ lon=121.513525,
52
+ title="Ganjingzi District",
53
+ subtitle="China",
54
+ theme="midnight_blue",
55
+ width_cm=21,
56
+ height_cm=29.7,
57
+ distance_m=4000,
58
+ include_buildings=True,
59
+ )
60
+ )
61
+
62
+ print(result.files)
63
+ ```
64
+
65
+ Notes:
66
+
67
+ - The Python renderer uses Nominatim + Overpass directly, so it is designed for city and regional posters rather than world-scale exports.
68
+ - `svg` output is true vector geometry, not a browser screenshot wrapped in SVG.
69
+ - Chinese place names now auto-fallback to common CJK system fonts on macOS/Linux; if your machine still lacks glyph coverage, pass `--font-file /path/to/font.ttf`.
70
+ - PNG export needs `Pillow`; `uv sync` will install it automatically.
71
+ - The PyPI package name is `terraink_py`, while the CLI command is `terraink`.
72
+
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "terraink_py"
7
+ version = "0.1.0"
8
+ description = "Standalone Python CLI for generating styled OSM map posters as PNG and SVG."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "Pillow>=10.4.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ terraink = "terraink_py.cli:main"
17
+
18
+ [tool.setuptools]
19
+ include-package-data = true
20
+
21
+ [tool.setuptools.package-data]
22
+ terraink_py = ["data/*.json"]
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+ include = ["terraink_py*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ from .api import PosterGenerator, generate_poster
2
+ from .data import get_layout, get_theme, load_layouts, load_themes
3
+ from .models import PosterRequest, PosterResult
4
+
5
+ __all__ = [
6
+ "PosterGenerator",
7
+ "PosterRequest",
8
+ "PosterResult",
9
+ "generate_poster",
10
+ "get_layout",
11
+ "get_theme",
12
+ "load_layouts",
13
+ "load_themes",
14
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .data import get_layout, get_theme
6
+ from .geo import CM_PER_INCH, MercatorProjector, compute_poster_and_fetch_bounds, resolve_canvas_size
7
+ from .http import CachedHttpClient
8
+ from .models import Coordinate, PosterRequest, PosterResult
9
+ from .osm import fetch_osm_layers, resolve_location
10
+ from .render import build_scene, render_png, render_svg
11
+
12
+
13
+ class PosterGenerator:
14
+ def generate(self, request: PosterRequest) -> PosterResult:
15
+ prepared = prepare_request(request)
16
+ prepared.validate()
17
+
18
+ client = CachedHttpClient(
19
+ cache_dir=prepared.cache_dir,
20
+ user_agent=prepared.user_agent,
21
+ timeout_seconds=prepared.timeout_seconds,
22
+ )
23
+ location = resolve_location(prepared, client)
24
+ center = Coordinate(lat=location.lat, lon=location.lon)
25
+ size = resolve_canvas_size(
26
+ prepared.width_cm / CM_PER_INCH,
27
+ prepared.height_cm / CM_PER_INCH,
28
+ dpi=prepared.dpi,
29
+ max_pixels=prepared.max_pixels,
30
+ max_side=prepared.max_side,
31
+ )
32
+ bounds = compute_poster_and_fetch_bounds(
33
+ center=center,
34
+ distance_meters=prepared.distance_m,
35
+ aspect_ratio=prepared.width_cm / prepared.height_cm,
36
+ )
37
+ layers = fetch_osm_layers(bounds.fetch_bounds, prepared, client)
38
+ projector = MercatorProjector.from_bounds(bounds.poster_bounds, size.width, size.height)
39
+ theme = get_theme(prepared.theme)
40
+ scene = build_scene(
41
+ size=size,
42
+ center=center,
43
+ title=(prepared.title or location.city or location.label).strip(),
44
+ subtitle=(prepared.subtitle or location.country).strip(),
45
+ theme=theme,
46
+ layers=layers,
47
+ projector=projector,
48
+ request=prepared,
49
+ )
50
+ output_paths = resolve_output_paths(prepared.output, prepared.formats)
51
+ files: list[Path] = []
52
+ for fmt, path in output_paths.items():
53
+ if fmt == "png":
54
+ render_png(scene, path)
55
+ elif fmt == "svg":
56
+ path.parent.mkdir(parents=True, exist_ok=True)
57
+ path.write_text(render_svg(scene), encoding="utf-8")
58
+ files.append(path)
59
+ return PosterResult(
60
+ files=tuple(files),
61
+ location=location,
62
+ theme=theme,
63
+ size=size,
64
+ bounds=bounds,
65
+ )
66
+
67
+
68
+ def prepare_request(request: PosterRequest) -> PosterRequest:
69
+ if request.layout:
70
+ layout = get_layout(request.layout)
71
+ request.width_cm = layout.width_cm
72
+ request.height_cm = layout.height_cm
73
+ return request
74
+
75
+
76
+ def resolve_output_paths(output: Path, formats: tuple[str, ...]) -> dict[str, Path]:
77
+ output = Path(output)
78
+ if len(formats) == 1 and output.suffix.lower() == f".{formats[0]}":
79
+ return {formats[0]: output}
80
+
81
+ base = output.with_suffix("") if output.suffix else output
82
+ return {fmt: base.with_suffix(f".{fmt}") for fmt in formats}
83
+
84
+
85
+ def generate_poster(request: PosterRequest) -> PosterResult:
86
+ return PosterGenerator().generate(request)
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .api import generate_poster
7
+ from .data import load_layouts, load_themes
8
+ from .models import DEFAULT_OVERPASS_URL, PosterRequest
9
+
10
+
11
+ def build_parser() -> argparse.ArgumentParser:
12
+ themes = ", ".join(load_themes().keys())
13
+ layouts = ", ".join(load_layouts().keys())
14
+ parser = argparse.ArgumentParser(
15
+ prog="terraink",
16
+ description="Generate styled OpenStreetMap posters as PNG and SVG.",
17
+ )
18
+ parser.add_argument(
19
+ "query",
20
+ nargs="?",
21
+ help="Optional positional location query, equivalent to --location.",
22
+ )
23
+ parser.add_argument("--output", required=True, help="Output file path or base path.")
24
+ parser.add_argument("--format", nargs="+", default=["png"], choices=["png", "svg"])
25
+ parser.add_argument("--location", help="Location query for Nominatim geocoding.")
26
+ parser.add_argument("--lat", type=float, help="Latitude.")
27
+ parser.add_argument("--lon", type=float, help="Longitude.")
28
+ parser.add_argument("--title", help="Override poster title.")
29
+ parser.add_argument("--subtitle", help="Override poster subtitle.")
30
+ parser.add_argument("--theme", default="midnight_blue", help=f"Theme id. Available: {themes}")
31
+ parser.add_argument("--layout", help=f"Layout id. Available: {layouts}")
32
+ parser.add_argument("--width-cm", type=float, default=21.0, help="Poster width in centimeters.")
33
+ parser.add_argument("--height-cm", type=float, default=29.7, help="Poster height in centimeters.")
34
+ parser.add_argument("--distance-m", type=float, default=4000.0, help="Half-width map distance in meters.")
35
+ parser.add_argument("--dpi", type=int, default=300, help="Raster export DPI for PNG sizing.")
36
+ parser.add_argument("--font-file", type=Path, help="Optional TTF/OTF file used for PNG text rendering.")
37
+ parser.add_argument("--font-family", help="Font family name written into SVG text nodes.")
38
+ parser.add_argument("--cache-dir", type=Path, default=Path(".terraink-cache"))
39
+ parser.add_argument("--overpass-url", default=DEFAULT_OVERPASS_URL)
40
+ parser.add_argument("--nominatim-url", default="https://nominatim.openstreetmap.org")
41
+ parser.add_argument("--user-agent", default="terraink/0.1")
42
+ parser.add_argument("--timeout", type=int, default=90)
43
+ parser.add_argument("--hide-text", action="store_true", help="Do not draw poster title/subtitle/coords.")
44
+ parser.add_argument("--hide-credits", action="store_true", help="Do not draw footer credits.")
45
+ parser.add_argument("--include-buildings", action=argparse.BooleanOptionalAction, default=False)
46
+ parser.add_argument("--include-water", action=argparse.BooleanOptionalAction, default=True)
47
+ parser.add_argument("--include-parks", action=argparse.BooleanOptionalAction, default=True)
48
+ parser.add_argument("--include-aeroway", action=argparse.BooleanOptionalAction, default=True)
49
+ parser.add_argument("--include-rail", action=argparse.BooleanOptionalAction, default=True)
50
+ parser.add_argument("--include-roads", action=argparse.BooleanOptionalAction, default=True)
51
+ parser.add_argument("--include-road-path", action=argparse.BooleanOptionalAction, default=True)
52
+ parser.add_argument("--include-road-minor-low", action=argparse.BooleanOptionalAction, default=True)
53
+ parser.add_argument("--include-road-outline", action=argparse.BooleanOptionalAction, default=True)
54
+ return parser
55
+
56
+
57
+ def main(argv: list[str] | None = None) -> int:
58
+ parser = build_parser()
59
+ args = parser.parse_args(argv)
60
+ request = PosterRequest(
61
+ output=Path(args.output),
62
+ formats=tuple(args.format),
63
+ location=args.location or args.query,
64
+ lat=args.lat,
65
+ lon=args.lon,
66
+ title=args.title,
67
+ subtitle=args.subtitle,
68
+ width_cm=args.width_cm,
69
+ height_cm=args.height_cm,
70
+ distance_m=args.distance_m,
71
+ dpi=args.dpi,
72
+ theme=args.theme,
73
+ layout=args.layout,
74
+ font_file=args.font_file,
75
+ font_family=args.font_family,
76
+ show_poster_text=not args.hide_text,
77
+ include_credits=not args.hide_credits,
78
+ include_buildings=args.include_buildings,
79
+ include_water=args.include_water,
80
+ include_parks=args.include_parks,
81
+ include_aeroway=args.include_aeroway,
82
+ include_rail=args.include_rail,
83
+ include_roads=args.include_roads,
84
+ include_road_path=args.include_road_path,
85
+ include_road_minor_low=args.include_road_minor_low,
86
+ include_road_outline=args.include_road_outline,
87
+ cache_dir=args.cache_dir,
88
+ overpass_url=args.overpass_url,
89
+ nominatim_url=args.nominatim_url,
90
+ user_agent=args.user_agent,
91
+ timeout_seconds=args.timeout,
92
+ )
93
+ result = generate_poster(request)
94
+ for path in result.files:
95
+ print(path)
96
+ return 0