Java端网页PDF生成方案(Chrome无头浏览器)

2,831 阅读4分钟

1. 背景

目前正在进行得一个小程序,客户提出了需求,要求将部分数据生成PDF提供下载和分享,由于是小程序项目,所以PDF得生成只能通过后端来进行,于是采用了常规的itext方案。生成得是那种比较偏文字类型得PDF。生成得PDF截图如下图所示

image.png

虽然也能通过部分手段实现报表的插入,但是在使用一段时间后,客户对PDF得美观度提出了需求,给出了设计稿,如下图所示

image.png

这时候itext就无法解决这一问题了,因为很多内容需要定制化,并且插入了很多图片和echarts图表。因此需要寻求其他得解决方案。

2. 解决方案

核心思路,通过H5生成想要实现得页面,截取该页面,后端通过切割该图片由itext贴入PDF中,最终生成对应得PDF。

1. 小程序内嵌H5页面,通过canvas绘制生成图片传递到后台,并生成PDF。

小程序通过web-view内嵌H5页面,通过访问对应得页面绘制图片,并传到后台生成PDF,过程并不顺利,因为安卓和ios浏览器内核得不同,导致绘制得页面出现偏移以及部分细节丢失等各种问题,最后生成得PDF有大概率无法使用,也有部分时候能够成功生成,因此放弃了这个方案。

2. 纯JAVA端生成。

通过ChormeHeadless + selenium实现在服务器访问网页并截图。

  • Java端需要引入一下几个jar包
<!--截图-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.0.0-alpha-6</version>
</dependency>
/**
 * 通用chromeDriver获取方法
 *
 * @param argument 获取浏览器宽高
 */
public WebDriver getDriver(String argument) {
    //驱动地址(linux用)
     System.setProperty("webdriver.chrome.driver",
           "/xxx/chromedriver");
    ChromeOptions chromeOptions = new ChromeOptions();
    // chrome安装路径(linux用)
    chromeOptions.setBinary("/usr/bin/google-chrome");
    chromeOptions.addArguments("--no-sandbox");
    chromeOptions.addArguments("start-maximized");
    chromeOptions.addArguments("disable-infobars");
    chromeOptions.addArguments("--disable-dev-shm-usage");
    chromeOptions.addArguments("--test-type");
    chromeOptions.addArguments("--disable-extensions");
    chromeOptions.addArguments("--headless");
    chromeOptions.setExperimentalOption("useAutomationExtension", false);
    chromeOptions.addArguments("--disable-dev-shm-usage");
    if(!argument.equals("")) chromeOptions.addArguments(argument);
    WebDriver webDriver = new ChromeDriver(chromeOptions);
    return webDriver;
}

/**
 * 无头浏览器获取页面生成的宽高
 * 完成后执行截图
 */
public static pdfImgGenerate(String imgName, String type, String resultId) {
    String url = MdisConfig.getPdfUrl() + "?id=" + resultId + "&type=" + type;
    WebDriver driver = new PdfUtils().getDriver("");
    driver.get(url);
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //获取JS执行器,可以执行js代码来控制页面
    JavascriptExecutor driver_js = ((JavascriptExecutor) driver);
    Long height = (Long) driver_js.executeScript("return document.body.scrollHeight");
    Long width = (Long) driver_js.executeScript("return document.body.scrollWidth");
    System.out.println("height" + height);
    Map<String, Long> map = new HashMap<>();
    map.put("height", height);
    map.put("width", width);
    driver.quit();
    new PdfUtils().frontEndCut(imgName, url, map);
}

/**
 * 无头浏览器截图
 */
public frontEndCut(String imgName, String url, Map<String, Long> map) {
    String argument = "--window-size=" + map.get("width") + "," + map.get("height");
    WebDriver driver = new PdfUtils().getDriver(argument);
    driver.get(url);
    // 页面等待渲染时长,如果你的页面需要动态渲染数据的话一定要留出页面渲染的时间,单位默认是秒
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 获取到截图的文件
    File screenshotFile = ((TakesScreenshot) driver)
            .getScreenshotAs(OutputType.FILE);
    if((screenshotFile != null) && screenshotFile.exists()) {
        //截取到的图片存到本地
        FileOutputStream out = null;
        FileInputStream in = null;
        try {
            in = new FileInputStream(screenshotFile);
            // 本地路径
            out = new FileOutputStream(MdisConfig.getUploadPath() + "/images/" + imgName + ".png");
            byte[] b = new byte[1024];
            while(true) {
                int temp = in.read(b, 0, b.length);
                // 如果temp = -1的时候,说明读取完毕
                if(temp == -1) {
                    break;
                }
                out.write(b, 0, temp);
            }
        } catch (Exception e) {
            //TODO异常处理
        }
    }
    driver.quit();
}

由于比较赶时间,代码写的比较粗糙,总体得实现思路是,调用服务器得浏览器访问要被截图得页面,获取整个页面对应得高度和宽度,然后通过获得得宽高再对该区域进行一次截图保存到本地。 获取到该图片后,即可对图片根据A4纸大小进行切割并通过itext贴入PDF,切割和生成PDF比较简单,代码就不贴了。由于该页面是VUE编写而非静态页面,因此截图及获取高度时需要等待一段时间,等待页面绘制完全。

3. 可能遇到得问题

1. 本地运行报错

解决方法:

  • 查看chrome版本是否与驱动对应
  • 是否赋予驱动对应得运行权限
  • 查看selenium版本是否与chrome版本得年代一致,修改selenium版本

2. 部署到服务器运行报错

解决方法:

  • 是否赋予驱动对应得运行权限
  • 查看本地代码得chrome-headless参数是否正确,特别注意'--no-sandbox'这个参数

3. 其他错误(更换selenium版本后,jar包中得版本并没有被替换)

解决方法:

  • 移除maven中引入得swagger包,可能存在冲突

4. 总结

经过多方尝试,最终终于实现了我想要得效果,在寻找解决方案得过程中,遇到了无数得问题,最终把坑踩掉了,写篇文章记录一下。