node puppeteer 批量生成图片 和 pdf

在数智赛事中某场赛事需要导出全部的球员报告pdf和球星卡图片,需要一次性生成全部数据导出。

在生成img图片的时候遇到顶部白线问题使用截取1px解决

clip: {
            x: boundingBox.x,
            y: boundingBox.y + 1, // 去掉顶部1px
            width: boundingBox.width,
            height: boundingBox.height - 1 // 减去1px
          }

 

 

 

完整代码如下

const express = require("express");
const axios = require("axios");
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const FormData = require("form-data");

const imgList = require('./data/img.js');
const pdfList = require('./data/pdf.js');

module.exports = async (app) => {
  const router = express.Router({
    mergeParams: true, //合并url参数 导入父级参数到子级配置
  });

  const { default: Queue } = await import("queue");

  // 创建一个任务队列
  const screenshotQueue = new Queue({
    concurrency: 1, // 同时处理一个任务
    autostart: false, // 自动开始处理任务
  });

  // 定义生成截图的函数
  const generateScreenshot = async (url, screenshotPath, retries = 3) => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
  
    // 设置页面视口尺寸,增加分辨率
    await page.setViewport({ width: 744, height: 1045, deviceScaleFactor: 2 });
  
    try {
      await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 }); // 增加导航超时时间到60秒
  
      // 等待 class="front-block" 元素加载完毕
      await page.waitForSelector(".front-block", { timeout: 60000 }); // 增加等待超时时间到60秒
  
      // 等待所有图片加载完毕
      await page.evaluate(async () => {
        const images = Array.from(document.images);
        await Promise.all(
          images.map((img) => {
            if (img.complete) return;
            return new Promise((resolve, reject) => {
              img.onload = resolve;
              img.onerror = reject;
            });
          })
        );
      });
  
      // 获取 class="front-block" 元素
      const frontBlock = await page.$(".front-block");
  
      if (frontBlock) {
        // 获取元素的尺寸和位置
        const boundingBox = await frontBlock.boundingBox();
  
        // 截取该元素的截图,并使用 clip 选项去掉顶部白线
        await page.screenshot({
          path: screenshotPath,
          type: 'jpeg',
          quality: 100,
          clip: {
            x: boundingBox.x,
            y: boundingBox.y + 1, // 去掉顶部1px
            width: boundingBox.width,
            height: boundingBox.height - 1 // 减去1px
          }
        });
  
        // 关闭浏览器
        await browser.close();
  
        // 返回截图路径
        return screenshotPath;
      } else {
        await browser.close();
        throw new Error('Element with class "front-block" not found');
      }
    } catch (error) {
      await browser.close();
      if (retries > 0) {
        console.log(`Retrying... (${3 - retries + 1})`);
        return generateScreenshot(url, screenshotPath, retries - 1);
      } else {
        throw error;
      }
    }
  };
  

  app.post("/pdf/api/img", async (req, res) => {
    try {
      const mainList = imgList;

      for (const item of mainList) {
        const { id, competitionId, playerId, type, page, playerName, shirtNum,competionName } = item;
        const url = `http://localhost:8080/#/starCardServe?token=176541f05cddc799da155fa8e3f06a2d&id=${id}&skinId=32&currentProfilePicture=undefined&competitionId=${competitionId}&playerId=${playerId}&type=${type}&&page=back`;

        console.log('请求的页面', url);
        
        // 创建 球星卡背面 文件夹路径
        const screenshotDir = path.join(__dirname, '球星卡背面');
        if (!fs.existsSync(screenshotDir)) {
          fs.mkdirSync(screenshotDir, { recursive: true });
        }

        const screenshotPath = path.join(screenshotDir, `${competionName}-${playerName}-${shirtNum}-背面.png`);

        // 添加任务到队列
        screenshotQueue.push(async (cb) => {
          try {
            const res = await generateScreenshot(url, screenshotPath);
            console.log("🚀 生成图片成功:", res);
            cb(null, screenshotPath);
          } catch (err) {
            console.log("🚀 生成图片错误:", err.message);
            cb(err);
          }
        });
      }

      // 检查队列是否已经在运行
      if (!screenshotQueue.running) {
        screenshotQueue.start((err, result) => {
          if (err) {
            console.error(`队列处理错误: ${err.message}`);
          } else {
            console.log(`队列处理完成: ${result}`);
          }
        });
      }

      // 立即返回响应
      res.status(200).send({
        code: "200",
        message: "任务已添加到队列",
        ok: true,
      });
    } catch (err) {
      res.status(500).send({
        code: 500,
        message: err.message,
        ok: false,
      });
    }
  });

  // 创建一个任务队列
  const pdfQueue = new Queue({
    concurrency: 1, // 同时处理一个任务
    autostart: false, // 自动开始处理任务
  });

  // 定义生成PDF的函数
  const generatePDF = async (url, pdfPath, retries = 3) => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    try {
      await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 }); // 增加导航超时时间到60秒

      // 等待页面特定内容加载完毕
      await page.waitForSelector(".player-report", { timeout: 60000 }); // 增加等待超时时间到60秒

      // 设置页面尺寸
      await page.setViewport({ width: 750, height: 1624 });

      // 渲染成PDF
      await page.pdf({
        path: pdfPath,
        width: '750px',
        height: '1624px',
        printBackground: true,
        margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
        displayHeaderFooter: false,
      });

      await browser.close();

      return pdfPath;
    } catch (error) {
      await browser.close();
      if (retries > 0) {
        console.log(`Retrying... (${3 - retries + 1})`);
        return generatePDF(url, pdfPath, retries - 1);
      } else {
        throw error;
      }
    }
  };

  app.post("/pdf/api/pdf", async (req, res) => {
    try {
      const mainList = pdfList;

      for (const item of mainList) {
        const { playerId, registrationPlayerId, scheduleId, matchId, matchZoneId, playerName, shirtNum, competitionId, competionName, mId } = item;

        const url = `http://localhost:8080/#/joyPlayerReport?playerId=${playerId}&registrationPlayerId=${registrationPlayerId}&scheduleId=${scheduleId}&page=all&matchId=${matchId}&matchZoneId=${matchZoneId}&userId=undefined&mId=${mId}`;
        console.log('请求的页面', url);

        // 创建 competition 文件夹路径
        const competitionDir = path.join(__dirname, 'exportPdfs', competionName);
        if (!fs.existsSync(competitionDir)) {
          fs.mkdirSync(competitionDir, { recursive: true });
        }

        const pdfPath = path.join(competitionDir, `${competionName}-${shirtNum}-${playerName}.pdf`);

        // 添加任务到队列
        pdfQueue.push(async (cb) => {
          try {
            const pdfres = await generatePDF(url, pdfPath);
            console.log("🚀 ~ pdfQueue.push ~ pdfres:", pdfres);
            cb(null, pdfPath);
          } catch (err) {
            console.log("🚀 ~ pdfQueue.push ~ err:");
            cb(err);
          }
        });
      }

      // 检查队列是否已经在运行
      if (!pdfQueue.running) {
        pdfQueue.start((err, result) => {
          if (err) {
            res.status(500).send({
              code: 500,
              message: err.message,
              ok: false,
            });
          } else {
            res.send({
              code: 200,
              entity: result, // 这里返回的是PDF的本地路径
              message: "PDF生成成功",
              ok: true,
            });
          }
        });
      } else {
        res.status(200).send({
          code: 200,
          message: "任务已添加到队列",
          ok: true,
        });
      }
    } catch (err) {
      res.status(500).send({
        code: 500,
        message: err.message,
        ok: false,
      });
    }
  });

  // 错误处理函数
  app.use(async (err, req, res, next) => {
    res.status(err.statusCode || 500).send({
      code: 500,
      message: err.message,
      ok: false,
    });
  });
};
登入分享下感受吧~