方案有很多:要么依赖于内部组件开发,要么依赖于外部转换工具。
内部组件:
Itext 用于生成PDF文档的一个java类库
flyingSaucer flying saucer是基于itext的,其最大的优势,是对css2.1的支持,页面渲染效果很好。但是也正是不支持css3,所以从最开始的用它,到之后的弃用它,哈哈哈
外部工具:
wkHtmlToPdf,此工具是执行程序,需要安装,并且代码嵌入cmd。
kkFileView 此工具是在线浏览工具,也可以下载。
今天记录一下itext的使用过程。
@PostMapping(value = "/pdfDownload", produces = "application/pdf")
@ResponseBody
public Result download(@RequestBody Map<String, Object> map, HttpServletResponse response, HttpServletRequest request) {
try {
ProductPdfVo productPdfVo = BeanUtil.fillBeanWithMap(map, new ProductPdfVo(), new CopyOptions());
log.info("/pdfDownload入参:{}", JSONUtil.toJsonStr(map));
Map<String, Object> data = BeanUtil.beanToMap(productPdfVo);
//crm审批
if ("0".equals(productPdfVo.getFlag())) {
List<ProductQuotation> productQuotation = productPdfVo.getProductQuotation();
if (CollUtil.isNotEmpty(productQuotation)) {
productQuotation.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
});
}
List<ServiceQuotation> serviceQuotation = productPdfVo.getServiceQuotation();
if (CollUtil.isNotEmpty(serviceQuotation)) {
serviceQuotation.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
});
}
List<ConsultQuotation> consultQuotation = productPdfVo.getConsultQuotation();
if (CollUtil.isNotEmpty(consultQuotation)) {
consultQuotation.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
});
}
List<Maintenance> maintenance = productPdfVo.getMaintenance();
if (CollUtil.isNotEmpty(maintenance)) {
maintenance.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
});
}
List<ExternalQuotation> externalQuotation = productPdfVo.getExternalQuotation();
if (CollUtil.isNotEmpty(externalQuotation)) {
externalQuotation.stream().forEach(x -> {
if (StrUtil.isNotBlank(x.getRate()) && !x.getRate().contains("%")) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
});
}
PdfUtil.download(templateEngine, request, "index62", data, response, "ws.pdf",productPdfVo.getFlag());
} else {
//无审批
List<ProductQuotation> productQuotation = productPdfVo.getProductQuotation();
if (CollUtil.isNotEmpty(productQuotation)) {
productQuotation.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate())) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
if (StrUtil.isNotBlank(x.getTaxPrice())) {
x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
}
});
}
List<ServiceQuotation> serviceQuotation = productPdfVo.getServiceQuotation();
if (CollUtil.isNotEmpty(serviceQuotation)) {
serviceQuotation.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate())) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
if (StrUtil.isNotBlank(x.getTaxPrice())) {
x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
}
});
}
List<ConsultQuotation> consultQuotation = productPdfVo.getConsultQuotation();
if (CollUtil.isNotEmpty(consultQuotation)) {
consultQuotation.forEach(x -> {
if (StrUtil.isNotBlank(x.getRate())) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
if (StrUtil.isNotBlank(x.getTaxPrice())) {
x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
}
});
}
List<ExternalQuotation> externalQuotation = productPdfVo.getExternalQuotation();
if (CollUtil.isNotEmpty(externalQuotation)) {
externalQuotation.stream().forEach(x -> {
if (StrUtil.isNotBlank(x.getRate())) {
x.setRateInt(Double.parseDouble(x.getRate()));
}
if (StrUtil.isNotBlank(x.getTaxPrice())) {
x.setTaxPrice(String.valueOf(Math.round(Double.parseDouble(x.getTaxPrice()))));
}
});
}
PdfUtil.download(templateEngine, request, "index61", data, response, "ws.pdf",productPdfVo.getFlag());
}
} catch (Exception e) {
log.error("pdfDownload异常:{}", e);
response.setStatus(500);
return Result.error(e.getMessage());
}
return Result.success();
// html2PdfUtil.html2Pdf("");
// String html = createHtml("index", "index", data);
// convertHtmlToImage(html,"ws.png");
}
package com.cloudwise.cactus.util;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.attach.impl.layout.HtmlPageBreak;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.DocumentProperties;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.IBlockElement;
import com.itextpdf.layout.element.IElement;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Element;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.xml.xmp.XmpWriter;
import lombok.extern.slf4j.Slf4j;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.WebContext;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
//import org.w3c.dom.Document;
/**
* pdf处理工具类
*
* @author gourd.hu
* @version 1.0
*/
@Slf4j
public class PdfUtil {
/**
* 以文件流形式下载到浏览器
*
* @param templateEngine 配置
* @param templateName 模板名称
* @param listVars 模板参数集
* @param response HttpServletResponse
* @param fileName 下载文件名称
*/
public static void download(TemplateEngine templateEngine, HttpServletRequest request, String templateName, Map<String, Object> listVars, HttpServletResponse response, String fileName,String flag) {
//需要添加的水印文字
String waterMarkName = "信息XXX";
String waterMarkName1 = "信息yyyy";
//水印字体透明度
float opacity = 0.3f;
//水印字体大小
int fontsize = 14;
//水印倾斜角度(0-360)
int angle = 30;
//数值越大每页竖向水印越少
int heightDensity = 20;
//数值越大每页横向水印越少
int widthDensity = 5;
try (ServletOutputStream out = response.getOutputStream()) {
// 设置编码、文件ContentType类型、文件头、下载文件名
response.setCharacterEncoding(XmpWriter.UTF8);
// response.setContentType("application/pdf");
// response.setHeader("Content-Disposition", "attachment;fileName=" +
// new String(fileName.getBytes("gb2312"), "ISO8859-1"));
// generateAll(templateEngine, request, response, templateName, out, listVars);
FileOutputStream fileOutputStream = new FileOutputStream(new File("ws.pdf"));
final Context ctx = new Context();
ctx.setVariables(listVars);
String htmlContent = templateEngine.process(templateName, ctx);
// PdfWriter pdfWriter = new PdfWriter(out);
PdfWriter pdfWriter = new PdfWriter(fileOutputStream);
PdfDocument pdfDoc = new PdfDocument(pdfWriter,new DocumentProperties());
pdfDoc.getDocumentInfo().setTitle(fileName);
// pdfDoc.setDefaultPageSize(PageSize.A4);// 自定义pageSize
// Document document = new Document(pdfDoc);
// PdfFont font = PdfFontFactory.createFont("static/fonts/simhei.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// document.setFont(font).setFontSize(10);
ConverterProperties props = new ConverterProperties();
FontProvider fp = new FontProvider();
fp.addFont("static/fonts/simhei.ttf"); // 添加字体文件
props.setFontProvider(fp);
// HtmlConverter.convertToPdf(htmlContent, pdfDoc, props);
List<IElement> elements = HtmlConverter.convertToElements(htmlContent, props);
Document document = new Document(pdfDoc, PageSize.A4, false);
document.setMargins(0, 0, 0, 0);
for (IElement element : elements) {
// 分页符
if (element instanceof HtmlPageBreak) {
document.add((HtmlPageBreak) element);
//普通块级元素
} else {
document.add((IBlockElement) element);
}
}
document.close();
pdfDoc.close();
fileOutputStream.flush();
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// byteArrayOutputStream.writeTo(out);
// byte[] buffer =byteArrayOutputStream.toByteArray();
// InputStream sbs = new ByteArrayInputStream(buffer);
if("0".equals(flag)){
addWaterMark("ws.pdf",out, waterMarkName, opacity, fontsize, angle, heightDensity, widthDensity,false);
}else{
addWaterMark("ws.pdf",out, waterMarkName1, opacity, fontsize, angle, heightDensity, widthDensity,false);
}
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}finally {
File file=new File("ws.pdf");
if (file.exists()){
file.delete();
}
}
}
/**
* pdf下载到特定位置
*
* @param templateEngine 配置
* @param templateName 模板名称
* @param listVars 模板参数集
* @param filePath 下载文件路径
*/
public static void save(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, String filePath) {
try (OutputStream out = new FileOutputStream(filePath);) {
// generateAll(templateEngine, templateName, out, listVars);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/**
* pdf预览
*
* @param templateEngine 配置
* @param templateName 模板名称
* @param listVars 模板参数集
* @param response HttpServletResponse
*/
public static void preview(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response) {
try (ServletOutputStream out = response.getOutputStream()) {
// generateAll(templateEngine, templateName, out, listVars);
out.flush();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/**
* 按模板和参数生成html字符串,再转换为flying-saucer识别的Document
*
* @param templateName 模板名称
* @param variables 模板参数
* @return Document
*/
private static Document generateDoc(TemplateEngine templateEngine, HttpServletRequest request, HttpServletResponse response, String templateName, Map<String, Object> variables) {
// 声明一个上下文对象,里面放入要存到模板里面的数据
// final Context context = new Context();
// context.setVariables(variables);
// StringWriter stringWriter = new StringWriter();
// try (BufferedWriter writer = new BufferedWriter(stringWriter)) {
// templateEngine.process(templateName, context, writer);
// writer.flush();
// DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
// return builder.parse(new ByteArrayInputStream(stringWriter.toString().getBytes()));
// } catch (Exception e) {
ResponseEnum.TEMPLATE_PARSE_ERROR.assertFail(e);
// }
return null;
}
/**
* 核心: 根据Thymeleaf 模板生成pdf文档
*
* @param templateEngine 配置
* @param templateName 模板名称
* @param out 输出流
* @param listVars 模板参数
* @throws Exception 模板无法找到、模板语法错误、IO异常
*/
private static void generateAll(TemplateEngine templateEngine, HttpServletRequest request, HttpServletResponse response, String templateName, OutputStream out, Map<String, Object> listVars) throws Exception {
// 断言参数不为空
//ResponseEnum.TEMPLATE_DATA_NULL.assertNotEmpty(listVars);
ITextRenderer renderer = new ITextRenderer();
//设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
ITextFontResolver fontResolver = renderer.getFontResolver();
//避免中文为空设置系统字体
fontResolver.addFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
// 如果linux下有问题,可使用以下方式解决。
// 有一个项目是用docker部署的,一直报错找不到simsun.ttf文件,但需要将simsun.ttf上传到/usr/share/fonts
//fontResolver.addFont(CommonUtil.isLinux() ? "/usr/share/fonts/simsun.ttf" : "static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
//根据参数集个数循环调用模板,追加到同一个pdf文档中
//(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
// for (int i = 0; i < listVars.size(); i++) {
// Document docAppend = generateDoc(templateEngine, request, response, templateName, listVars.get(i));
// renderer.setDocument(docAppend, null);
WebContext ctx = new WebContext(request, response,request.getServletContext(),request.getLocale(), listVars);
ctx.setVariables(listVars);
String content = templateEngine.process(templateName, ctx);
renderer.setDocumentFromString(content);
//展现和输出pdf
renderer.layout();
// if (i == 0) {
renderer.createPDF(out);
// } else {
//写下一个pdf页面
// renderer.writeNextDocument();
// }
// }
renderer.finishPDF(); //完成pdf写入
}
/**
* pdf添加水印
* @param inputFile 需要添加水印的文件
* @param outputFile 添加完水印的文件存放路径
* @param waterMarkName 需要添加的水印文字
* @param opacity 水印字体透明度
* @param fontsize 水印字体大小
* @param angle 水印倾斜角度(0-360)
* @param heightDensity 数值越大每页竖向水印越少
* @param widthDensity 数值越大每页横向水印越少
* @param cover 是否覆盖
* @return
*/
public static boolean addWaterMark(String inputFile, OutputStream out, String waterMarkName,
float opacity, int fontsize, int angle, int heightDensity, int widthDensity,boolean cover) {
// if (!cover){
// File file=new File(outputFile);
// if (file.exists()){
// return true;
// }
// }
File file=new File(inputFile);
if (!file.exists()){
return false;
}
PdfReader reader = null;
PdfStamper stamper = null;
try {
int interval = -5;
reader = new PdfReader(inputFile);
stamper = new PdfStamper(reader,out);
BaseFont base = BaseFont.createFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
Rectangle pageRect = null;
PdfGState gs = new PdfGState();
//这里是透明度设置
gs.setFillOpacity(opacity);
//这里是条纹不透明度
gs.setStrokeOpacity(0.2f);
int total = reader.getNumberOfPages() + 1;
System.out.println("Pdf页数:" + reader.getNumberOfPages());
JLabel label = new JLabel();
FontMetrics metrics;
int textH = 0;
int textW = 0;
label.setText(waterMarkName);
metrics = label.getFontMetrics(label.getFont());
//字符串的高, 只和字体有关
textH = metrics.getHeight();
//字符串的宽
textW = metrics.stringWidth(label.getText());
PdfContentByte under;
//循环PDF,每页添加水印
for (int i = 1; i < total; i++) {
pageRect = reader.getPageSizeWithRotation(i);
Rectangle pageRect1 = reader.getPageSize(i);
// under = stamper.getOverContent(i); //在内容上方添加水印
under = stamper.getUnderContent(i); //在内容下方添加水印
stamper.setRotateContents(false);
stamper.setFormFlattening(true);
PdfShading axial = PdfShading.simpleAxial(stamper.getWriter(),
pageRect1.getLeft(pageRect1.getWidth()/10), pageRect1.getBottom(),
pageRect1.getRight(pageRect1.getWidth()/10), pageRect1.getBottom(),
new BaseColor(245, 245, 245), new BaseColor(245, 245, 245), true, true);
under.paintShading(axial);
under.saveState();
under.setGState(gs);
under.beginText();
under.setFontAndSize(base, 10);
//under.setColorFill(BaseColor.PINK); //添加文字颜色 不能动态改变 放弃使用
// under.setFontAndSize(base, fontsize); //这里是水印字体大小
for (int height = textH; height < pageRect.getHeight() * 2; height = height + textH * heightDensity) {
for (int width = textW; width < pageRect.getWidth() * 1.5 + textW; width = width + textW * widthDensity) {
// rotation:倾斜角度
under.showTextAligned(Element.ALIGN_LEFT, waterMarkName, width - textW, height - textH, angle);
}
}
//添加水印文字
under.endText();
}
System.out.println("添加水印成功!");
return true;
} catch (IOException e) {
System.out.println("添加水印失败!错误信息为: " + e);
e.printStackTrace();
return false;
} catch (Exception e) {
System.out.println("添加水印失败!错误信息为: " + e);
e.printStackTrace();
return false;
} finally {
//关闭流
if (stamper != null) {
try {
stamper.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (reader != null) {
reader.close();
}
}
}
}
工具类中,还写了加水印的,水印需要在pdf生成之后,再加水印,感觉有点费事。直接把outputStream转为InputStream的话,会报错
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// byteArrayOutputStream.writeTo(out);
// byte[] buffer =byteArrayOutputStream.toByteArray();
// InputStream sbs = new ByteArrayInputStream(buffer);
所以,就老老实实的生成pdf再删除吧。背景色是在加水印的代码中体现出来的。
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
工具类中,我还写了flySaucer的代码,所以,也得加依赖:
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
simhei.ttf汉化字体包,百度下载即可。
spring:
thymeleaf:
cache: false
prefix: classpath:/first/
mode: HTML
suffix: .html
encoding: utf-8
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<style>
h1{
color: brown;
}
.quotation {
width: 100%;
}
.quotation, .quotation th, .quotation td {
border: 1px solid brown;
border-collapse: collapse;
}
.quotation th {
background-color: brown;
color: white;
}
</style>
</head>
<body style="font-family: SimSun">
<h1>Quotation</h1>
<table>
<tr>
<th>Customer</th>
</tr>
<tr>
<td th:text="${#dates.format(quotationDate, 'yyyy-MM-dd')}"></td>
</tr>
<tr>
<td th:text="${#dates.format(offerValidity, 'yyyy-MM-dd')}"></td>
</tr>
<tr>
<td th:text="${customerName}"></td>
</tr>
</table>
<br />
<table class="quotation">
<tr>
<th>序号</th>
<th>产品名称</th>
<th>规格及明细</th>
<th>单价</th>
<th>单位</th>
<th>数量</th>
<th>税率</th>
</tr>
<tr th:each="item, iterStat: ${productQuotation}">
<td th:text="${iterStat.index + 1}"></td>
<td th:text="${item.productName}"></td>
<td th:text="${item.details}"></td>
<td th:text="${item.unitPrice}"></td>
<td th:text="${item.unit}"></td>
<td th:text="${item.nums}"></td>
<td th:text="${item.rate}"></td>
</tr>
</table>
</body>
</html>
body那加了字体包名,是flySaucer必需的,但不是Itext必需的。
另外要注意,汉化包以及静态文件,需要在pom中配置:
<resources>
<resource>
<directory>src/main/resources/</directory>
</resource>
</resources>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
<nonFilteredFileExtension>xls</nonFilteredFileExtension>
<nonFilteredFileExtension>docx</nonFilteredFileExtension>
<nonFilteredFileExtension>html</nonFilteredFileExtension>
<nonFilteredFileExtension>css</nonFilteredFileExtension>
<nonFilteredFileExtension>png</nonFilteredFileExtension>
<!-- <nonFilteredFileExtension>ttf</nonFilteredFileExtension>-->
<!-- <nonFilteredFileExtension>ttc</nonFilteredFileExtension>-->
</nonFilteredFileExtensions>
</configuration>
</plugin>
build标签的resources标签 与 maven-resources-plugin是异曲同工之妙。