diff --git a/.gitignore b/.gitignore index e458e5a..e2c050f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env .mypy_cache .pytest_cache +.vscode *.egg-info *.pyc *.png diff --git a/README.md b/README.md index 199218e..bbd3ca7 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,24 @@ A python module to create static map images (PNG, SVG) with markers, geodesic li ## Installation +### SVG only version + ```shell pip install py-staticmaps ``` +### SVG + PNG version (via Cairo) + +```shell +pip install py-staticmaps[cairo] +``` + `py-staticmaps` uses `pycairo` for creating antialiased raster-graphics, so make sure `libcairo2` is installed on your system (on Ubuntu just install the `libcairo2-dev` package, i.e. `sudo apt install libcairo2-dev`). ## Examples +Note: PNG support (e.g. `context.render_cairo(...)`) is only available if the `pycairo` module is installed. ### Markers and Geodesic Lines @@ -195,4 +204,4 @@ Please take a look at the command line program which uses the `staticmaps` packa ## License -[MIT](LICENSE) © 2020 Florian Pigorsch +[MIT](LICENSE) © 2020-2012 Florian Pigorsch diff --git a/examples/custom_objects.py b/examples/custom_objects.py index c132eaa..39fa83b 100644 --- a/examples/custom_objects.py +++ b/examples/custom_objects.py @@ -3,7 +3,10 @@ # py-staticmaps # Copyright (c) 2021 Florian Pigorsch; see /LICENSE for licensing information -import cairo # type: ignore +try: + import cairo # type: ignore +except ImportError: + pass import s2sphere # type: ignore import staticmaps @@ -125,8 +128,9 @@ def render_cairo(self, renderer: staticmaps.CairoRenderer) -> None: context.set_tile_provider(staticmaps.tile_provider_CartoDarkNoLabels) # render png -image = context.render_cairo(800, 500) -image.write_to_png("custom_objects.png") +if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 500) + image.write_to_png("custom_objects.png") # render svg svg_image = context.render_svg(800, 500) diff --git a/examples/draw_gpx.py b/examples/draw_gpx.py index de3418b..f1af830 100644 --- a/examples/draw_gpx.py +++ b/examples/draw_gpx.py @@ -27,8 +27,9 @@ break # render png -image = context.render_cairo(800, 500) -image.write_to_png("running.png") +if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 500) + image.write_to_png("running.png") # render svg svg_image = context.render_svg(800, 500) diff --git a/examples/frankfurt_newyork.py b/examples/frankfurt_newyork.py index fc63952..d278747 100644 --- a/examples/frankfurt_newyork.py +++ b/examples/frankfurt_newyork.py @@ -16,8 +16,9 @@ context.add_object(staticmaps.Marker(newyork, color=staticmaps.RED, size=12)) # render png -image = context.render_cairo(800, 500) -image.write_to_png("frankfurt_newyork.png") +if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 500) + image.write_to_png("frankfurt_newyork.png") # render svg svg_image = context.render_svg(800, 500) diff --git a/examples/freiburg_area.py b/examples/freiburg_area.py index 52bd01a..9062107 100644 --- a/examples/freiburg_area.py +++ b/examples/freiburg_area.py @@ -445,8 +445,9 @@ ) # render png -image = context.render_cairo(800, 500) -image.write_to_png("freiburg_area.png") +if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 500) + image.write_to_png("freiburg_area.png") # render svg svg_image = context.render_svg(800, 500) diff --git a/examples/geodesic_circles.py b/examples/geodesic_circles.py index 58c505c..ffe3180 100644 --- a/examples/geodesic_circles.py +++ b/examples/geodesic_circles.py @@ -15,9 +15,11 @@ context.add_object(staticmaps.Circle(center2, 2000, fill_color=staticmaps.TRANSPARENT, color=staticmaps.GREEN, width=2)) context.add_object(staticmaps.Marker(center1, color=staticmaps.RED)) context.add_object(staticmaps.Marker(center2, color=staticmaps.GREEN)) + # render png -image = context.render_cairo(800, 600) -image.write_to_png("geodesic_circles.png") +if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 600) + image.write_to_png("geodesic_circles.png") # render svg svg_image = context.render_svg(800, 600) diff --git a/examples/tile_providers.py b/examples/tile_providers.py index 43e9fd9..8cd9fb8 100644 --- a/examples/tile_providers.py +++ b/examples/tile_providers.py @@ -20,8 +20,9 @@ context.set_tile_provider(provider) # render png - image = context.render_cairo(800, 500) - image.write_to_png(f"provider_{name}.png") + if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 500) + image.write_to_png(f"provider_{name}.png") # render svg svg_image = context.render_svg(800, 500) diff --git a/examples/us_capitals.py b/examples/us_capitals.py index d8a571e..b3e7d1e 100644 --- a/examples/us_capitals.py +++ b/examples/us_capitals.py @@ -20,8 +20,9 @@ context.add_object(staticmaps.Marker(capital, size=5)) # render png -image = context.render_cairo(800, 500) -image.write_to_png("us_capitals.png") +if staticmaps.cairo_is_supported(): + image = context.render_cairo(800, 500) + image.write_to_png("us_capitals.png") # render svg svg_image = context.render_svg(800, 500) diff --git a/requirements-cairo.txt b/requirements-cairo.txt new file mode 100644 index 0000000..78a9422 --- /dev/null +++ b/requirements-cairo.txt @@ -0,0 +1 @@ +pycairo diff --git a/requirements.txt b/requirements.txt index ea72258..f6b1454 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ appdirs geographiclib PILLOW -pycairo python-slugify requests s2sphere diff --git a/setup.py b/setup.py index 382ad1f..c044c82 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ def _read_reqs(rel_path: str) -> typing.List[str]: packages=[PACKAGE], install_requires=_read_reqs("requirements.txt"), extras_require={ + "cairo": _read_reqs("requirements-cairo.txt"), "dev": _read_reqs("requirements-dev.txt"), "examples": _read_reqs("requirements-examples.txt"), }, diff --git a/staticmaps/__init__.py b/staticmaps/__init__.py index 8106945..331aeeb 100644 --- a/staticmaps/__init__.py +++ b/staticmaps/__init__.py @@ -3,7 +3,7 @@ # flake8: noqa from .area import Area -from .cairo_renderer import CairoRenderer +from .cairo_renderer import CairoRenderer, cairo_is_supported from .circle import Circle from .color import ( parse_color, diff --git a/staticmaps/cairo_renderer.py b/staticmaps/cairo_renderer.py index 28d43f7..2ded32c 100644 --- a/staticmaps/cairo_renderer.py +++ b/staticmaps/cairo_renderer.py @@ -3,9 +3,14 @@ import io import math +import sys import typing -import cairo # type: ignore +try: + import cairo # type: ignore +except ImportError: + pass + from PIL import Image # type: ignore from .color import Color, BLACK, WHITE @@ -17,21 +22,33 @@ from .object import Object # pylint: disable=cyclic-import +def cairo_is_supported() -> bool: + return "cairo" in sys.modules + + +# Dummy types, so that type annotation works if cairo is missing. +cairo_Context = typing.Any +cairo_ImageSurface = typing.Any + + class CairoRenderer(Renderer): def __init__(self, transformer: Transformer) -> None: Renderer.__init__(self, transformer) + if not cairo_is_supported(): + raise RuntimeError("Cannot render to Cairo since the 'cairo' module could not be imported.") + self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *self._trans.image_size()) self._context = cairo.Context(self._surface) - def image_surface(self) -> cairo.ImageSurface: + def image_surface(self) -> cairo_ImageSurface: return self._surface - def context(self) -> cairo.Context: + def context(self) -> cairo_Context: return self._context @staticmethod - def create_image(image_data: bytes) -> cairo.ImageSurface: + def create_image(image_data: bytes) -> cairo_ImageSurface: image = Image.open(io.BytesIO(image_data)) if image.format == "PNG": return cairo.ImageSurface.create_from_png(io.BytesIO(image_data)) @@ -103,7 +120,7 @@ def render_attribution(self, attribution: typing.Optional[str]) -> None: def fetch_tile( self, download: typing.Callable[[int, int, int], typing.Optional[bytes]], x: int, y: int - ) -> typing.Optional[cairo.ImageSurface]: + ) -> typing.Optional[cairo_ImageSurface]: image_data = download(self._trans.zoom(), x, y) if image_data is None: return None diff --git a/staticmaps/context.py b/staticmaps/context.py index 505547e..3e614ca 100644 --- a/staticmaps/context.py +++ b/staticmaps/context.py @@ -3,15 +3,13 @@ import math import os -import sys import typing import appdirs # type: ignore -import cairo # type: ignore import s2sphere # type: ignore import svgwrite # type: ignore -from .cairo_renderer import CairoRenderer +from .cairo_renderer import CairoRenderer, cairo_is_supported from .color import Color from .meta import LIB_NAME from .object import Object, PixelBoundsT @@ -54,8 +52,8 @@ def set_tile_provider(self, provider: TileProvider) -> None: def add_object(self, obj: Object) -> None: self._objects.append(obj) - def render_cairo(self, width: int, height: int) -> cairo.ImageSurface: - if "cairo" not in sys.modules: + def render_cairo(self, width: int, height: int) -> typing.Any: + if not cairo_is_supported(): raise RuntimeError('You need to install the "cairo" module to enable "render_cairo".') center, zoom = self.determine_center_zoom(width, height)