# PyMGL: Maplibre GL Native Static Renderer for Python This package provides an interface to `mapblibre-gl-native` to render Mapbox GL styles to PNG images. WARNING: this package is under active development and the API may change without notice. ## Goals This package is intended to provide a lightweight interface to `maplibre-gl-native` for rendering Mapbox GL to PNG image data using Python. This is particularly useful for server-side rendering of maps for use in reports. This package provides only the Python API for interacting with `maplibre-gl-native`; it does not provide higher-level functionality such as a web server or a CLI. For a stand-alone service implmenting rendering functionality, see [mbgl-renderer](https://github.com/consbio/mbgl-renderer) (implemented in NodeJS). ## Install ### Supported operating systems #### MacOS 10.15+ (x86_64 only) Wheels are available on PyPI: ```bash pip install pymgl ``` To verify that it installed correctly, run the included test suite: ```bash python -m pip install pytest Pillow numpy pixelmatch python-dotenv python -m pytest --pyargs pymgl -v ``` #### Ubuntu 22.04 & 20.04 Due to the complexity of building manylinux wheels that include OpenGL and successfully compile `maplibre-gl-native`, wheels are only available for Ubuntu 22.04 and 20.04. Wheels are available on the release page in Github. Download and install from there. Unfortunately, Python wheel names are very restrictive, so we have added `.ubuntu-22.04` and `.ubuntu-20.04` suffixes to the wheel names, which have to be stripped off before you can install them. Something like this for Ubuntu 22.04: ```bash wget https://github.com/brendan-ward/pymgl/releases/download//pymgl----linux_x86_64.whl.ubuntu-22.04 # rename file to remove .ubuntu-22.04 suffix find . -type f -name "*.whl.ubuntu-22.04" -print0 -exec bash -c 'mv "${0}" "${0//.ubuntu-22.04/}"' {} \; python3 -m pip install --find-links . pymgl ``` You also need to install the following runtime dependencies: **Ubuntu 22.04:** ```bash apt-get install libicu70 \ libcurl4 \ libjpeg-turbo8 \ libpng16-16 \ libprotobuf23 \ libuv1 \ libx11-6 \ libegl1 \ libopengl0 \ xvfb ``` **Ubuntu 20.04:** ```bash apt-get install libicu66 \ libcurl4 \ libjpeg-turbo8 \ libpng16-16 \ libprotobuf17 \ libuv1 \ libx11-6 \ libegl1 \ libopengl0 \ xvfb ``` You must have Xvfb running in order to successfully use `pymgl`. You can setup and run Xvfb manually, or wrap calls to python in `Xvfb-run`. To verify that it installed correctly, run the included test suite: ```bash python -m pip install pytest Pillow numpy pixelmatch python-dotenv xvfb-run -a --server-args="-screen 0 1024x768x24 -ac +render -noreset" \ python -m pytest --pyargs pymgl -v ``` #### Windows Windows is not and will not be supported. ## Usage To create a map object, you must always provide a Mapbox GL style JSON string or URL to a well-known style hosted by Mapbox or Maptiler: ```Python from pymgl import Map style = """{ "version": 8, "sources": { "basemap": { "type": "raster", "tiles": ["https://services.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}"], "tileSize": 256 } }, "layers": [ { "id": "basemap", "source": "basemap", "type": "raster" } ] }""" map = Map(style, , , , , , , , ) ``` See the [styles](#styles) section for more information about map styles. Other than style, all other parameters are optional with default values. NOTE: `style` and `ratio` cannot be changed once the instance is constructed. You can use a well-known style instead of providing a style JSON string, but you must also provide a token and identify the correct provider: ```Python map = Map("mapbox://styles/mapbox/streets-v11", token=, provider="mapbox") ``` Valid providers are `mapbox`, `maptiler`, and `maplibre`. ### Map properties You can set additional properties on the map instance after it is created: ```Python map.setCenter(longitude, latitude) map.setZoom(zoom) map.setSize(width, height) map.setBearing(bearing) # map bearing in degrees map.setPitch(pitch) # map pitch in degrees map.setFilter(layerId, filterJSON or None) map.setPaintProperty(layerId, property, value) map.setVisibility(layerId, True / False) ``` You can retrieve these values using attributes, if needed: ```Python map.size # (width, height) map.center # (longitude, latitude) map.zoom map.bearing map.pitch ``` You can also retrive information about the map's style or a specific layer: ```Python map.listLayers() # [, ...] map.listSources() # [, ...] map.getFilter() # returns JSON value or None map.getPaintProperty(, ) # returns JSON value or None map.getLayerJSON() # returns JSON describing layer ``` NOTE: paint properties may be decoded to their internal representation. For example, a CSS color string `#FF0000` will be returned as `["rgba", 255, 0, 0, 1]`. IMPORTANT: if you are using a remotely-hosted style, you need to force the map to load - which loads all underying assets - before listing the style's layers, sources, or other properties. ```Python map = Map("mapbox://styles/mapbox/streets-v11", token=, provider="mapbox") map.listLayers() # [] map.load() map.listLayers() # [, ...] ``` Alternatively, you can download the style yourself and provide that as input to the Map, and it will show all layers without requiring a render first. However, not all assets will be loaded until the first render. ```Python from urllib.request import urlopen url = f"https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token={MAPBOX_TOKEN}" with urlopen(url) as r: style = r.read() map = Map(style.decode("UTF-8") token=, provider="mapbox") map.listLayers() # [, ...] ``` You can auto-fit the map to bounds instead of using center longitude / lantitude and zoom: ```Python map.setBounds(xmin, ymin, xmax, ymax, ) ``` You can register an image for use with your style by providing an ID, raw image bytes, width, height, pixel ratio, and indicate if it should be interpreted as SDF: ``` map.addImage("id", img_bytes, width, height, , ) ``` See the [SDF image docs](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/) for more information about using SDF images. ### Rendering You can render the map to PNG bytes: ```Python img_bytes = map.renderPNG() ``` This returns `bytes` containing the RGBA PNG data. You can render the map to a raw buffer as a numpy array (`uint8` dtype): ```Python array = map.renderBuffer() ``` The array is a sequence of RGBA values for each pixel in the image. This may be useful if you are going to immediately read the image data into another package such as `Pillow` or `pyvips` to combine with other image operations. ### Map instances WARNING: you must manually delete the map instance if you assign a new map instance to that variable, or this package will segfault (not yet sure why). This problem does not occur if separate instances are assigned to separate variables. ```Python map = Map(