Recently I needed to display a map with some polygons, markers and popups on it. I wanted to have both an interactive map but also be able to create a hardcopy from my linux's terminal via a script in a headless server.

I found a solution which satisfies both requirements. Have the interactive map be from the HTML produced by LeafletJS and then spawn a headless browser (thank you WWW::Mechanize::Chrome & Corion!!) and take a screenshot of the map, via a script from any headless server.

To me, at least, the screenshot part sounds like a very roundabout way of getting a hardcopy of a map (a PDF or PNG) but after researching this for a few weeks, I realised that this is how it is done in R (and in the serpent world, it is called htmlwebshot).

Building on WWW::Mechanize::Chrome's screenshot functionality and on a conversation I had with LanX some years ago on how to inject JS into a mechanized browser and manipulate the DOM with it I have now published WWW::Mechanize::Chrome::DOMops which finds/deletes elements of the current DOM of your mechanized browser and then on top of that, WWW::Mechanize::Chrome::Webshot which takes a screenshot of your browser contents onto a local file. In short, it renders a URL or a local file onto a, possibly headless, mechanized browser (with all encompassing CSS and JS included), allows some settle time, optionally removing some DOM elements which clutter the view and takes a screenshot of what's currently rendered. I think this is as good as it gets for html2pdf. I mean the browser is the final arbiter on how html renders right? (well, sort of).

https://leafletjs.com/ is really very good at displaying map tiles, images/satellite or vectors, from various sources (e.g. OpenStreetMap for navigation vector maps or ArcGIS/ESRI for satellite images, both free). It is also very easy to draw polygons, markers, etc. on top of the map using map coordinates. And then allows you to move/pan/zoom interactively. Really cool software. Alas in Javascript.

And so, in the below script I combine both to get both a self-contained HTML (it requires lefleat.js external dependency) of an interactive map as well as a printout in the form of PDF/PNG.

Caveat: the output PDF contains only part of the view (increasing the dpi perhaps?) whereas the PNG contains everything the browser window contains. So, a PNG is output instead.

PS: I had in mind to create a Just another Perl Hacker all over Africa but the polyline is quite some bytes long and I will spare you the bandwidth. Just imagine Just another Perl Hacker sprayed all over the globe.

PS2: Geo::Leaflet does a good job at exposing the basic functionality of LeafletJS via Perl. But there are lots more features and options for your map. I propose that once you have a basic HTML map, to template it and keep adding more features there. You do not need to keep using Geo::Leaflet. Check the script's output HTML file.

# by bliako for perlmonks 21/08/2025 use Geo::Leaflet; use FindBin; use File::Spec; use WWW::Mechanize::Chrome::Webshot; my $lat = 11.1; my $lon = 22.2; my $outbase = 'hack'; my $map = Geo::Leaflet->new( id => "myMap", center => [$lat, $lon], zoom => 3, ); # the tiles are coming from: $map->tileLayer( # navigation vector tiles #url => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', # satellite images url => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_ +Imagery/MapServer/tile/{z}/{y}/{x}', options => { maxZoom => 19, maxNativeZoom => 23, #attribution => 'openstreetmap', attribution => 'esri', }, ); $map->polyline( coordinates => [ [9.0, 20.0], [11.3, 23.1], [12.1, 19.4], ], options=>{color=>'red', weight=>'5'} ); my $map_html = $map->html; my $outdir = $FindBin::Bin; my $map_html_outfile = File::Spec->catfile($outdir, $outbase.'_map.htm +l'); my $FH; open($FH, '>', $map_html_outfile) or die "failed to open file for writing html '$map_html_outfile', +$!"; print $FH $map->html; close $FH; my $map_pdf_outfile = File::Spec->catfile($outdir, $outbase.'_map.png' +); my $shooter = WWW::Mechanize::Chrome::Webshot->new({ 'settle-time' => 10, 'resolution' => '2000x2000', }); my $local_uri = URI->new('file://'.$map_html_outfile); $shooter->shoot({ 'output-filename' => $map_pdf_outfile, 'url' => $local_uri->as_string, 'remove-DOM-elements' => [ {'element-xpathselector' => '//div[id="leaflet-control-container +"]'}, ], 'exif' => {'created' => 'by the shooter'}, }); print "$0 : done, map saved to PDF '$map_pdf_outfile'\n";

bw, bliako