最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

express - Generating PDF with Puppeteer: View Not Fully Captured and Leaflet Tiles Not Fully Loaded - Stack Overflow

programmeradmin1浏览0评论

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
Add a comment  | 

1 Answer 1

Reset to default 1

to 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;
      }
    }
  `;

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论