最近在做一个功能,需要将当前的web界面转换成pdf进行下载。
目前网上流传的主流缺乏有三种:
- 用itextpdf进行转换
- 用jspdf进行转换
- 用wkhtmltopdf进行转换
综合三种技术,jspdf直接被pass掉,因为他是纯前端利用canvas截图来切割图片来生成的pdf,也就是pdf的内容全部为图片,这样会存在页面放大模糊等弊端。
那么再考虑itextpdf,经过查找资料和测试,这个工具对css的支持和页面的渲染效果不是太好,在浏览器正常打开的界面在经过itextpdf转换后样式不一样,所以这个也被pass。
那么最后只剩下wkhtmltopdf,其实刚开始也不太想用这个插件,因为并非java开发,需要编译为linux或者windows上的可执行程序进行调用。而且在实际测试这个插件的时候,本来打算直接将页面地址传给wkhtmltopdf进行转换,奈何我的前端是由react开发,webpack编译,在转换的时候总是会报错,刚开始怀疑是兼容性问题,所以下载了一个wiki内核的浏览器,经过测试同样打不开我们的系统界面,各种尝试无果后,有于时间原因,决定放弃解决系统兼容性问题。
下面想到了一种办法,直接将当前打开的界面的dom树进行clone,然后将dom树对象转为html文本传给后台,由后台将这个临时的html页面传给wkhtmltopdf生成pdf,当然这中间涉及到了clone后的dom树中的canvas图形消失,样式等问题。
这里采用了,直接将当前dom树中的head标签的所有内容和所要导出的内容的dom树一起clone并转换为字符传给后台,这样就保证了head中的css也可以传给后台,当前这也是初步的一个想法,因为head中的样式其实在我们编译前端的时候就已经可以确定好的,所以完全可以自己在后台直接使用编译好的样式,而不用每次从前台传,暂且先这样吧。
那么接下来解决canvas内容丢失问题,因为原来的echarts图形是js代码动态给canvas绘制的,clone来的canvas对象肯定只是个空的canvas没有内容,那么我想到的办法就是遍历当前内容区域所有canvas标签,并且转换为图片替换到clone来的canvas标签,这样整个界面canvas就变为了图片,也可以传递给后台了。
大体思路就到这里。下面是一部分的代码:
var getLayerNode = (node) => {
if(node.classList && node.classList.contains("layer")){
return node
}else{
return getLayerNode(node.parentElement)
}
}
const exporToPdf = ()=>{
var app = refContent.current
var list = app.getElementsByTagName("canvas")
var map={}
var promisArray=[]
var getCanvasImg = (id, canvasItem) => {
return new Promise(function (resolve, reject) {
var promise = html2canvas(canvasItem)
promise.then(canvas => {
var image = new Image();
image.src = canvas.toDataURL("image/png", 1);
map[id] = image
document.getElementById(id).removeAttribute("id")
resolve()
})
});
}
for (var i = 0; i < list.length; i++) {
var canvasItem = list[i]
if(canvasItem){
var id = uuid(6)
canvasItem.id = id
promisArray.push(getCanvasImg(id, getLayerNode(canvasItem.parentElement)))
}
}
var tmpApp = app.cloneNode(true);
tmpApp.style.display = "none"
Promise.all(promisArray).then(() => {
tmpApp.id = "appTmp"
document.body.appendChild(tmpApp)
for (var id in map) {
if (map[id]) {
var canvas = document.getElementById(id)
canvas.parentNode.replaceChild(map[id], canvas);
}
}
dispatch(Actions.exportReportToPdf(document.head.innerHTML, tmpApp.outerHTML, tmpApp.style.width, tmpApp.style.height, id, name))
document.body.removeChild(tmpApp)
})
}
@Slf4j
public class WkHtmlToPdfUtils {
public class HtmlToPdfInterceptor extends Thread {
private InputStream is;
public HtmlToPdfInterceptor(InputStream is){
this.is = is;
}
public void run(){
try{
InputStreamReader isr = new InputStreamReader(is, "utf-8");
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null) {
log.info(line.toString());
}
}catch (IOException e){
e.printStackTrace();
}
}
}
private String wkHtmlToPdfName = "wkhtmltopdf";
public WkHtmlToPdfUtils() {
this.wkHtmlToPdfName = isWindows() ? "wkhtmltopdf.exe" : "wkhtmltopdf";
}
private static boolean isLinux() {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
public String convertHtmlTextToPdf(ExportBean exportBean) {
String tmpDir = System.getProperty("java.io.tmpdir");
String fileName = UUID.randomUUID().toString();
exportBean.setUuid(fileName);
String resultPath = null;
try {
Document html = Jsoup.parse("<!DOCTYPE html><html></html>");
html.head().append(exportBean.getHead());
html.body().append(exportBean.getContent());
Elements scripts = html.getElementsByTag("script");
for(Element script : scripts) {
script.remove();
}
Element app = html.getElementById("appTmp");
String style = app.attr("style");
app.attr("style", style.replace("display: none;", ""));
resultPath = writeHtmlToFile(tmpDir,fileName+".html",html.toString());
} catch (Exception e) {
log.error("创建html文件失败",e);
return null;
}
if(resultPath != null) {
String targetPath = tmpDir+File.separator+fileName+".pdf";
if(convertHtmlToPdf(resultPath, targetPath, reportView)) {
reportView.setFilePath(targetPath);
log.info("生成PDF文件:{}",targetPath);
return targetPath;
}
}
return null;
}
public boolean convertHtmlToPdf(String srcPath, String destPath, ExportBean exportBean) {
File file = new File(destPath);
File parent = file.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
StringBuilder cmd = new StringBuilder();
if(System.getenv("WKHTMLTOPDF_HOME") == null) {
log.error("获取环境变量WKHTMLTOPDF_HOME失败");
return false;
}
String pxWidth = exportBean.getWidth().replaceAll("px", "");
String pxHeight = exportBean.getHeight().replaceAll("px", "");
long width = (long) Math.ceil((Long.parseLong(pxWidth)*25.4 / 96));
long height = (long) Math.ceil((Long.parseLong(pxHeight)*25.4 / 96));
String wkToPdfTool=System.getenv("WKHTMLTOPDF_HOME")+File.separator+"bin"+File.separator+wkHtmlToPdfName;
cmd.append(wkToPdfTool).append(" -B 0 -L 0 -R 0 -T 0 --disable-smart-shrinking").append(" --page-width ").append(width).append(" --page-height ")
.append(height).append(" ").append(srcPath).append(" ").append(destPath);
log.info(cmd.toString());
boolean result = true;
try {
Process proc = Runtime.getRuntime().exec(cmd.toString());
HtmlToPdfInterceptor error = new HtmlToPdfInterceptor(proc.getErrorStream());
HtmlToPdfInterceptor output = new HtmlToPdfInterceptor(proc.getInputStream());
error.start();
output.start();
proc.waitFor();
} catch (Exception e) {
result = false;
e.printStackTrace();
}
new File(srcPath).deleteOnExit();
return result;
}
private String writeHtmlToFile(String path, String name, String content) throws Exception {
File pathFile = new File(path);
if (!pathFile.exists()) {
pathFile.mkdirs();
}
File file = new File(path + File.separator + name);
file.createNewFile();
BufferedWriter out = new BufferedWriter(new FileWriter(file));
out.write(content);
out.flush();
out.close();
return file.getPath();
}
}