1. 背景
目前正在进行得一个小程序,客户提出了需求,要求将部分数据生成PDF提供下载和分享,由于是小程序项目,所以PDF得生成只能通过后端来进行,于是采用了常规的itext方案。生成得是那种比较偏文字类型得PDF。生成得PDF截图如下图所示
虽然也能通过部分手段实现报表的插入,但是在使用一段时间后,客户对PDF得美观度提出了需求,给出了设计稿,如下图所示
这时候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>
复制代码
- 服务器或者本地安装chrome以及对应得驱动
chrome下载地址
驱动下载地址
ps:注意版本得对应
- Java实现
/**
* 通用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. 总结
经过多方尝试,最终终于实现了我想要得效果,在寻找解决方案得过程中,遇到了无数得问题,最终把坑踩掉了,写篇文章记录一下。