I have an Angular application that contains only a Leaflet map. I've built an Express.js server using Puppeteer to capture the map and generate a PDF.
I pass the zoom level and map layer as URL parameters. However, I’m facing two major issues:
- The PDF does not capture the entire view of the map—it only includes a portion of it.
- Some map tiles are not fully loaded in the PDF. Some appear gray, partially transparent, or missing.
To debug this, I set Puppeteer’s headless option to false so I could see what it captures. In the opened browser window, the full map is visible as expected. However, the generated PDF does not match what I see in that window.
this is my express js code :
app.get('/print', async (req, res) => {
try {
const { zoom, layer } = req.query;
console.log(`Generating PDF with Zoom: ${zoom}, Layer: ${layer}`);
const browser = await puppeteer.launch({
headless: false,
args: ['--start-maximized'], // Start the browser in maximized mode
});
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080,deviceScaleFactor: 2});
const mapUrl = `http://localhost:4200/?zoom=${zoom}&layer=${encodeURIComponent(layer)}`;
await page.emulate('screen'); //<--
await page.goto(mapUrl, { waitUntil: 'networkidle0' });
// Wait for the map element (#map) to be available
await page.waitForSelector('#map');
// Change layer and zoom level dynamically in the page context
await page.evaluate((zoom, layer) => {
if (window.leafletMap) {
// Set the zoom level
window.leafletMap.setZoom(parseInt(zoom));
// Remove all layers
window.leafletMap.eachLayer((l) => window.leafletMap.removeLayer(l));
// Add the new layer
const newLayer = L.tileLayer(layer);
newLayer.addTo(window.leafletMap);
// Wait for the new layer to be loaded before proceeding
return new Promise((resolve) => {
newLayer.on('load', resolve); // Wait until the layer is loaded
});
}
}, zoom, layer);
// Ensure the map is fully rendered before capturing the PDF
await page.waitForFunction(() => document.querySelectorAll('.leaflet-tile-loaded').length > 0, {
timeout: 18000, // extended timeout to ensure the tiles are fully loaded
});
// Wait for the map to stabilize by checking if tiles are loaded
await page.waitForSelector('.leaflet-tile-loaded', { timeout: 10000 });
// Ensure the tiles are loaded properly by waiting for the 'load' event of all tiles
await page.evaluate(() => {
return new Promise((resolve) => {
let tilesLoaded = 0;
const totalTiles = document.querySelectorAll('.leaflet-tile').length;
const interval = setInterval(() => {
tilesLoaded = document.querySelectorAll('.leaflet-tile-loaded').length;
if (tilesLoaded === totalTiles) {
clearInterval(interval);
resolve();
}
}, 100);
});
});
// Generate the PDF with higher scale for better resolution
const pdfPath = path.join(__dirname, 'map.pdf');
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true,
scale: 1.5,
landscape: true,
});
// await browser.close();
// Send the PDF as a download response
res.download(pdfPath, 'map.pdf', (err) => {
if (err) {
console.error('Error sending PDF:', err);
res.status(500).send('Error generating PDF');
} else {
fs.unlinkSync(pdfPath);
}
});
} catch (error) {
console.error('Error generating PDF:', error);
res.status(500).send('Error generating PDF');
}
});
This is what my Angular app looks like: : And this is the PDF generated by the Express.js server:
My questions:
- How can I ensure that Puppeteer captures the entire visible map, not just a portion of it?
- How can I make sure all Leaflet tiles are fully loaded in the PDF before capturing?
This is a proof of concept (PoC) for my work, and I will eventually integrate it into a larger project. Any help would be greatly appreciated!
Thank you in advance!
I have an Angular application that contains only a Leaflet map. I've built an Express.js server using Puppeteer to capture the map and generate a PDF.
I pass the zoom level and map layer as URL parameters. However, I’m facing two major issues:
- The PDF does not capture the entire view of the map—it only includes a portion of it.
- Some map tiles are not fully loaded in the PDF. Some appear gray, partially transparent, or missing.
To debug this, I set Puppeteer’s headless option to false so I could see what it captures. In the opened browser window, the full map is visible as expected. However, the generated PDF does not match what I see in that window.
this is my express js code :
app.get('/print', async (req, res) => {
try {
const { zoom, layer } = req.query;
console.log(`Generating PDF with Zoom: ${zoom}, Layer: ${layer}`);
const browser = await puppeteer.launch({
headless: false,
args: ['--start-maximized'], // Start the browser in maximized mode
});
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080,deviceScaleFactor: 2});
const mapUrl = `http://localhost:4200/?zoom=${zoom}&layer=${encodeURIComponent(layer)}`;
await page.emulate('screen'); //<--
await page.goto(mapUrl, { waitUntil: 'networkidle0' });
// Wait for the map element (#map) to be available
await page.waitForSelector('#map');
// Change layer and zoom level dynamically in the page context
await page.evaluate((zoom, layer) => {
if (window.leafletMap) {
// Set the zoom level
window.leafletMap.setZoom(parseInt(zoom));
// Remove all layers
window.leafletMap.eachLayer((l) => window.leafletMap.removeLayer(l));
// Add the new layer
const newLayer = L.tileLayer(layer);
newLayer.addTo(window.leafletMap);
// Wait for the new layer to be loaded before proceeding
return new Promise((resolve) => {
newLayer.on('load', resolve); // Wait until the layer is loaded
});
}
}, zoom, layer);
// Ensure the map is fully rendered before capturing the PDF
await page.waitForFunction(() => document.querySelectorAll('.leaflet-tile-loaded').length > 0, {
timeout: 18000, // extended timeout to ensure the tiles are fully loaded
});
// Wait for the map to stabilize by checking if tiles are loaded
await page.waitForSelector('.leaflet-tile-loaded', { timeout: 10000 });
// Ensure the tiles are loaded properly by waiting for the 'load' event of all tiles
await page.evaluate(() => {
return new Promise((resolve) => {
let tilesLoaded = 0;
const totalTiles = document.querySelectorAll('.leaflet-tile').length;
const interval = setInterval(() => {
tilesLoaded = document.querySelectorAll('.leaflet-tile-loaded').length;
if (tilesLoaded === totalTiles) {
clearInterval(interval);
resolve();
}
}, 100);
});
});
// Generate the PDF with higher scale for better resolution
const pdfPath = path.join(__dirname, 'map.pdf');
await page.pdf({
path: pdfPath,
format: 'A4',
printBackground: true,
scale: 1.5,
landscape: true,
});
// await browser.close();
// Send the PDF as a download response
res.download(pdfPath, 'map.pdf', (err) => {
if (err) {
console.error('Error sending PDF:', err);
res.status(500).send('Error generating PDF');
} else {
fs.unlinkSync(pdfPath);
}
});
} catch (error) {
console.error('Error generating PDF:', error);
res.status(500).send('Error generating PDF');
}
});
This is what my Angular app looks like: : And this is the PDF generated by the Express.js server:
My questions:
- How can I ensure that Puppeteer captures the entire visible map, not just a portion of it?
- How can I make sure all Leaflet tiles are fully loaded in the PDF before capturing?
This is a proof of concept (PoC) for my work, and I will eventually integrate it into a larger project. Any help would be greatly appreciated!
Thank you in advance!
Share Improve this question asked Mar 6 at 2:37 Jihed Ben ZarbJihed Ben Zarb 575 bronze badges 1- Let us continue this discussion in chat. – Jihed Ben Zarb Commented Mar 7 at 0:18
1 Answer
Reset to default 1to solve this thats what i did :
- check if all tiles are loaded by comparing the total number of tiles (.leaflet-tile) with the loaded tiles (.leaflet-tile-loaded). Wait for all tiles to be fully loaded before proceeding.
await page.evaluate(() => {
return new Promise((resolve) => {
const checkTilesLoaded = () => {
const totalTiles = document.querySelectorAll('.leaflet-tile').length;
const loadedTiles = document.querySelectorAll('.leaflet-tile-loaded').length;
console.log(`Tiles loaded: ${loadedTiles}/${totalTiles}`);
if (loadedTiles === totalTiles) {
console.log('All tiles are fully loaded!');
resolve();
} else {
setTimeout(checkTilesLoaded, 500); // Retry every 500ms
}
};
checkTilesLoaded();
});
});`
- apply custom styles for the print layout using @media print to adjust the page size for the PDF therefore the PDF is like the web application now :
await page.evaluate(() => {
const style = document.createElement('style');
style.innerHTML = `
@media print {
body {
width: 1900px;
height: 1200px;
}
}
`;