Skip to content

Instantly share code, notes, and snippets.

@vadimkantorov
Last active August 1, 2026 19:03
Show Gist options
  • Select an option

  • Save vadimkantorov/3a444b90c6880c195ecf13ca94a78a3c to your computer and use it in GitHub Desktop.

Select an option

Save vadimkantorov/3a444b90c6880c195ecf13ca94a78a3c to your computer and use it in GitHub Desktop.
Use Chrome headless to create a screenshot of LeafletJS map with a custom open marker popup
import argparse
import subprocess
import base64
parser = argparse.ArgumentParser()
parser.add_argument('--chrome-exe', default = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
parser.add_argument('--user-agent', default = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36')
parser.add_argument('--lang', default = 'en_EN')
parser.add_argument('--output-path', '-o', default = 'screenshot.png')
parser.add_argument('--html-path', default = 'test.html')
parser.add_argument('--height', type = int, default = 480)
parser.add_argument('--width', type = int, default = 640)
parser.add_argument('--center-latlon', default = '48.8566,2.3522')
parser.add_argument('--zoom', type = int, default = 13)
parser.add_argument('--marker-latlon', default = '48.8700159,2.375403')
parser.add_argument('--marker-address', default = '29 rue du Moulin Joly, 75011, Paris')
parser.add_argument('--marker-url', default = 'https://www.google.com/maps/place/Espace+Libertés+%2F+Reforum+Space+Paris/@48.8700159,2.375403,17z/data=!4m6!3m5!1s0x47e66d0022c5a879:0x2e87db69e9384941!8m2!3d48.8700124!4d2.3779779!16s%2Fg%2F11wh3tqz2m?entry=ttu&g_ep=EgoyMDI2MDcyNy4wIKXMDSoASAFQAw%3D%3D')
parser.add_argument('--marker-name', default = 'Espace Libertés | Reforum Space Paris')
parser.add_argument('--tile-url-pattern', default = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
parser.add_argument('--attribution', default = '© OpenStreetMap contributors')
parser.add_argument('--leaflet-version', default = '1.9.4')
args = parser.parse_args()
htmlstr = f'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@{args.leaflet_version}/dist/leaflet.css" />
</head>
<body>
<template id="template_popup">
<div class="map_popup">
<h2 id="name"></h2>
<h4><a id="place_address" target="_blank"></a></h4>
</div>
</template>
<div id="map" data-markerurl="{args.marker_url}" data-markeraddress="{args.marker_address}" data-markerlatlon="{args.marker_latlon}" data-markername="{args.marker_name}" data-zoom="{args.zoom}" data-centerlatlon="{args.center_latlon}" data-tileurlpattern="{args.tile_url_pattern}" data-attribution="{args.attribution}"></div>
<style>
#map {{ height: {args.height}px }}
.mapmarker {{ fill: hsl(230, 69%, 50%); fill-opacity: 1.0; }}
.map_popup, .leaflet-popup {{ width: 400px }}
</style>
<script type="module">
import * as L from "https://unpkg.com/leaflet@{args.leaflet_version}/dist/leaflet-src.esm.js";
const div = document.getElementById('map'), template = document.getElementById('template_popup');
const marker_latlon = div.dataset.markerlatlon.split(',').map(parseFloat), center_latlon = div.dataset.centerlatlon.split(',').map(parseFloat), zoom = parseInt(div.dataset.zoom);
const map = L.map(div).setView(center_latlon, zoom);
L.tileLayer(div.dataset.tileurlpattern, {{ attribution: div.dataset.attribution }}).addTo(map);
const marker = L.circleMarker(marker_latlon, {{ radius: 10, stroke: false, className: 'mapmarker' }}).addTo(map);
const elem = template.content.cloneNode(true);
elem.querySelector('#name').innerText = div.dataset.markername;
elem.querySelector('#place_address').innerText = div.dataset.markeraddress;
elem.querySelector('#place_address').href = div.dataset.markerurl;
const htmlstr = elem.firstElementChild.outerHTML;
marker.bindPopup(htmlstr);
marker.openPopup();
</script>
</body>
</html>
'''
if args.html_path:
with open(args.html_path, 'w') as f:
f.write(htmlstr)
if args.output_path:
datauri = 'data:text/html;charset=utf-8;base64,' + base64.b64encode(htmlstr.encode('utf-8')).decode()
subprocess.check_call([args.chrome_exe, '--headless', '--no-sandbox', '--disable-features=dbus', '--disable-gpu', f"--screenshot={args.output_path}", "--virtual-time-budget=25000", f'--lang={args.lang}', f"--user-agent='{args.user_agent}'", '--incognito', datauri])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment