需求背景
目前项目中有通过 上传word模板,结合用户填写数据,导出渲染后的PDF数据,提升数据安全性。现记录一下使用过程及遇到的问题
项目方案
- Adobe Acrobat Pro 打开刚刚制作的pdf文件模板表单,后台读取模板,并替换值,导出即可。
- 通过将模板文件替换成 HTML, 并将模板文件放入数据库中,后台从数据库读取模板,并替换值,导出即可。
项目技术
方案一 采用 Adobe Acrobat Pro 软件(注意,需要考虑版权),及后端使用 itextpdf 读取模板和导出模板。
方案二 采用 freemarker 及 flying-saucer-pdf 读取html,并渲染成pdf。
方案对比
方案一: 操作简单,容错性较高。建议 复杂性的可以采用方案一。
方案二: 因为涉及模板转成html,再由html转成pdf,因此可能会存在格式失真,等问题,操作比较复杂,效果比较好。 建议简单点的 可以采用该方案。
制作模板步骤
方案一
1. 下载Adobe Acrobat pro。
2. 准备好 PDF 模板文件。
3. 使用 Adobe Acrobat pro 打开PDF模板文件,选择 表单 -》 添加或者编辑域
方案二
1. 准备模板文件word 或者 模板pdf,使用word打开,另存为 Html
2. 将html 作为中转,使用 freemarker 渲染模板文件。
实现方式
方案一
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.4.3</version>
</dependency>
public static void main(String[] args) {
// 模板路径
String templatePath = "C:" + File.separator + "Users" + File.separator + "Administrator" + File.separator + "Desktop" + File.separator + "模板1.pdf";
// 生成的新文件路径
String savePath = "C:" + File.separator + "Users" + File.separator + "Administrator" + File.separator + "Desktop" + File.separator + "test2.pdf";
Map<String, Object> o = new HashMap<>();
Map<String, String> dataMap = new HashMap<>();
Map<String, String> imageMap = new HashMap<>();
dataMap.put("salaryYear","2020");
dataMap.put("salaryMonth","11");
dataMap.put("salaryDay","27");
o.put("dataMap", dataMap);
o.put("imageMap", imageMap);
PdfReader reader;
FileOutputStream out;
ByteArrayOutputStream bos;
PdfStamper stamper;
try {
com.itextpdf.text.pdf.BaseFont bf = com.itextpdf.text.pdf.BaseFont.createFont("C:" + File.separator + "Windows" + File.separator + "Fonts" + File.separator + "simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
Font FontChinese = new Font(bf, 5, Font.NORMAL);
out = new FileOutputStream(savePath);// 输出流
reader = new PdfReader(templatePath);// 读取pdf模板
bos = new ByteArrayOutputStream();
stamper = new PdfStamper(reader, bos);
AcroFields form = stamper.getAcroFields();
//文字类的内容处理
Map<String, String> datemap = (Map<String, String>) o.get("dataMap");
form.addSubstitutionFont(bf);
float fontSize = 10.5f;
for (String key : datemap.keySet()) {
String value = datemap.get(key);
form.setFieldProperty(key, "textfont", bf, null);
form.setFieldProperty(key, "textsize", fontSize, null);
form.setField(key, value);
}
//图片类的内容处理
Map<String, String> imgmap = (Map<String, String>) o.get("imageMap");
for (String key : imgmap.keySet()) {
String value = imgmap.get(key);
String imgpath = value;
int pageNo = form.getFieldPositions(key).get(0).page;
Rectangle signRect = form.getFieldPositions(key).get(0).position;
float x = signRect.getLeft();
float y = signRect.getBottom();
//根据路径读取图片
Image image = Image.getInstance(imgpath);
//获取图片页面
PdfContentByte under = stamper.getOverContent(pageNo);
//图片大小自适应
image.scaleToFit(signRect.getWidth(), signRect.getHeight());
//添加图片
image.setAbsolutePosition(x, y);
under.addImage(image);
}
stamper.setFormFlattening(true);// 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
stamper.close();
Document doc = new Document();
Font font = new Font(bf, 32);
PdfCopy copy = new PdfCopy(doc, out);
doc.open();
PdfImportedPage importPage = copy.getImportedPage(new PdfReader(bos.toByteArray()), 1);
copy.addPage(importPage);
doc.close();
} catch (IOException e) {
System.out.println(e);
} catch (DocumentException e) {
System.out.println(e);
}
}
方案二
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.0.9</version>
</dependency>
<!-- parse DOM -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
@SneakyThrows
@Test
public void exportHtmlToPdf() {
Map<String, Object> businessData = new HashMap<>();
String formatContent = formatContent(“html”, businessData);
//pdf生成
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
ITextRenderer iTextRenderer = new ITextRenderer();
iTextRenderer.setDocumentFromString(formatContent);
//设置字体 其他字体需要添加字体库
ITextFontResolver fontResolver = iTextRenderer.getFontResolver();
fontResolver.addFont("C:" + File.separator + "Windows" + File.separator + "Fonts" + File.separator + "simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont(this.getClass().getClassLoader().getResource("fonts/wingdings2.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
fontResolver.addFont(this.getClass().getClassLoader().getResource("fonts/simsunb.ttf").getPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
iTextRenderer.setDocument(builder.parse(new ByteArrayInputStream(formatContent.getBytes())), null);
iTextRenderer.layout();
//生成PDF
ByteArrayOutputStream baos = new ByteArrayOutputStream();
iTextRenderer.createPDF(baos);
String savePath = "C:" + File.separator + "Users" + File.separator + "Administrator" + File.separator + "Desktop" + File.separator + "test.pdf";
FileOutputStream fos = new FileOutputStream(savePath);
fos.write(baos.toByteArray());
baos.close();
fos.flush();
fos.close();
}
private String formatContent(String content, Map<String, Object> businessData) {
String formatContent = "";
try {
Configuration configuration = freeMarkerConfigurationFactory.createConfiguration();
StringTemplateLoader stringLoader = new StringTemplateLoader();
stringLoader.putTemplate("sendMessageTemplate", content);
configuration.setTemplateLoader(stringLoader);
freemarker.template.Template temp = null;
temp = configuration.getTemplate("sendMessageTemplate", "utf-8");
StringWriter stringWriter = new StringWriter(2048);
temp.process(businessData, stringWriter);
formatContent = stringWriter.toString();
stringWriter.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
// 自动修复 html 格式
// return Jsoup.parse(formatContent).html();
return formatContent;
}
常见问题
方案一: 常见问题
- 多行文本能不能自动换行
- 单选框如何勾选
3 . 生成后文件变得很大,模板 100KB,下载后,变成 7M多
问题原因
将字体打包进了原文件中
解决方案
引入字体库
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
com.itextpdf.text.pdf.BaseFont bf = com.itextpdf.text.pdf.BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
4 . 如何获取单元格默认字体大小
com.itextpdf.text.pdf.BaseFont bf = com.itextpdf.text.pdf.BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
reader = new PdfReader(url);// 读取pdf模板
bos = new ByteArrayOutputStream();
stamper = new PdfStamper(reader, bos);
stamper.setFullCompression();
AcroFields form = stamper.getAcroFields();
//文字类的内容处理
form.addSubstitutionFont(bf);
float fontSize = 10.5f;
for (Map.Entry<String, Object> entry : businessData.entrySet()) {
form.setFieldProperty(entry.getKey(), "textfont", bf, null);
if(form.getFields().get(entry.getKey()) != null
&& form.getFields().get(entry.getKey()).getValue(0).get(new PdfName("DA")) != null){
String numbers = getNumbers(form.getFields().get(entry.getKey()).getValue(0).get(new PdfName("DA")) + "");
if(!"0".equals(numbers)){
fontSize = CommonUtils.evalFloat(numbers,0.0F);
} else {
fontSize = 10.5f;
}
}
form.setFieldProperty(entry.getKey(), "textsize", fontSize, null);
form.setField(entry.getKey(), entry.getValue() != null ? entry.getValue().toString() : "");
}
// 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
stamper.setFormFlattening(true);
stamper.close();
public String getNumbers(String content) {
Matcher matcher = Pattern.compile("\\d+").matcher(content);
while (matcher.find()) {
return matcher.group(0);
}
return "";
}
方案二: 常见问题
1. 元素类型 "meta" 必须由匹配的结束标记 "</meta>" 终止。
问题原因
flying saucer对xml格式要求很严格,因此必须是完整的格式。(尝试使用过 Jsoup.parse(formatContent).html() 修复格式,但是发现 这个问题还是不能修复,目前只能手动改)
问题描述
org.xhtmlrenderer.util.XRRuntimeException: Can't load the XML resource (using TRaX transformer). org.xml.sax.SAXParseException; lineNumber: 55; columnNumber: 4; 元素类型 "meta" 必须由匹配的结束标记 "</meta>" 终止。
at org.xhtmlrenderer.resource.XMLResource$XMLResourceBuilder.createXMLResource(XMLResource.java:192)
at org.xhtmlrenderer.resource.XMLResource.load(XMLResource.java:75)
at org.xhtmlrenderer.pdf.ITextRenderer.setDocumentFromString(ITextRenderer.java:165)
at org.xhtmlrenderer.pdf.ITextRenderer.setDocumentFromString(ITextRenderer.java:160)
修改前
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" >
<meta name="Generator" content="Microsoft Word 15 (filtered)" >
</head>
修改后
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="Generator" content="Microsoft Word 15 (filtered)" />
</head>
2 . 不能识别
org.xhtmlrenderer.util.XRRuntimeException: Can't load the XML resource (using TRaX transformer). org.xml.sax.SAXParseException: The entity "nbsp" was referenced, but not declared.
org.xhtmlrenderer.resource.XMLResource$XMLResourceBuilder.createXMLResource(XMLResource.java:191)
org.xhtmlrenderer.resource.XMLResource.load(XMLResource.java:71)
解决方案:
将 替换为以下
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
- 支持中文
//设置字体 其他字体需要添加字体库
ITextFontResolver fontResolver = iTextRenderer.getFontResolver();
fontResolver.addFont("C:"+ File.separator +"Windows"+ File.separator +"Fonts"+ File.separator +"simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
注意!!!
页面中字体不能使用中文,需要使用英文名称,而且是大小写敏感的!例如宋体的英文名称是 SimSun(注意不是simsun!,首字母都是大写的)
错误写法:font-family:宋体 或者 font-family:simsun
正确写法:font-family:SimSun 或者 font-family:SimHei
如果生成的pdf中文不显示或者乱码,请确认如下信息:
确保页面中所有内容都指定了字体,最好能指定 body {font-family:…},以防止漏网之鱼。
确保上述所有字体均通过addFont加入,字体名称错误或者字体不存在会抛出异常,很方便,但是没导入的字体不会有任何提示。
确保字体名称正确,不使用中文,大小写正确。
确保html标签都正确,简单的方法是所有内容都去掉,随便写几个中文看看能否正常生成,如果可以,在认真检查html标签,否则再次检查上述几条。
还有就是中文换行的问题了,带有中文而且文字较多存在换行情况时,需要给table加入样式:
table-layout:fixed,然后表格中的td使用%还指定td的宽度。
- 生成的模板 右边被截断,显示不全
html 页面不能不要大于A4格式大小 可以通过 css 中 @page{**} 设置模板大小