环境准备
开发环境
java8
,SpringBoot 2.1.4
,字符集GBK
字体
宋体–simsun.ttf
pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>kernel</artifactId> <version>7.0.3</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>io</artifactId> <version>7.0.3</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>forms</artifactId> <version>7.0.3</version> </dependency> <!-- 解决中文字体问题 --> <dependency> <groupId>com.itextpdf</groupId> <artifactId>font-asian</artifactId> <version>7.0.3</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13</version> </dependency> <dependency> <groupId>com.itextpdf.tool</groupId> <artifactId>xmlworker</artifactId> <version>5.5.13</version> </dependency> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> <version>9.1.5</version> </dependency> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.1.5</version> </dependency>
模板生成PDF
将模板转换为html
从文件读取模板
/** * 初始化freemarker配置 * templateRoot:模板文件根目录 */ Configuration freemarkerCfg = initFreemarkerCfg(templateRoot); /** * 将模板转换为HTML字符串 */ String content = freeMarkerRender(data, freemarkerCfg, htmlTemplate, charSet);
private static Configuration initFreemarkerCfg(String templateRoot) { Configuration freemarkerCfg = new Configuration(); try { freemarkerCfg.setDirectoryForTemplateLoading(new File(templateRoot)); } catch (IOException e) { log.error("模板根路径获取失败!" + templateRoot, e); throw new RuntimeException("模板根路径获取失败!" + templateRoot,e); } return freemarkerCfg; }
/** * data 需要注入模板的数据 * freemarkerCfg freemarker配置 * htmlTmp 模板名称 * charSet 字符集 linux下使用UTF-8,windows下使用GBK,否则会出现中文乱码,模板文件的文件编码和声明编码同样需要保持一致 */ private static String freeMarkerRender(Map<String, Object> data, Configuration freemarkerCfg, String htmlTmp,String charSet) { try (Writer out = new StringWriter();) { Template template = freemarkerCfg.getTemplate(htmlTmp); template.setEncoding(charSet); template.process(data, out); out.flush(); return out.toString(); } catch (Exception e) { log.error("HTML加载数据失败!", e); throw new RuntimeException("HTML加载数据失败!", e); } }
从流读取模板
/** * data 需要注入模板的数据 * fileName 文件名称 * inputStream 模板文件流 * charSet 字符集 linux下使用UTF-8,windows下使用GBK,否则会出现中文乱码,模板文件的文件编码和声明编码同样需要保持一致 */ private static String freeMarkerRender(Map<String, Object> data, String fileName, InputStream inputStream, String charSet) { try (Writer out = new StringWriter(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream);) { Configuration configuration = new Configuration(); Template template = new Template(fileName, inputStreamReader, configuration); template.setEncoding(charSet); template.process(data, out); out.flush(); return out.toString(); } catch (Exception e) { log.debug("HTML加载数据失败!", e); throw new RuleException(ErrCodeFile.CO_HTML_TEMPLATE_CONVERT_ERROR); } }
生成PDF
/** * htmlContent 通过freemarker生成的html * fontPath 字体文件路径 * ByteArrayOutputStream pdf文件流 */ private static ByteArrayOutputStream htmlToPdf(String htmlContent, String fontPath) { try { ByteArrayOutputStream output = new ByteArrayOutputStream(); ITextRenderer render = new ITextRenderer(); ITextFontResolver fontResolver = render.getFontResolver(); fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); render.setDocumentFromString(htmlContent); render.getSharedContext().setBaseURL(BASE_URL); render.layout(); render.createPDF(output); return output; } catch (Exception e) { log.debug("html转换pdf失败!", e); throw new RuntimeException("html转换pdf失败!", e); } }
添加水印和页码
从文件获取水印
/** * outputStream 生成的pdf流 * waterMarkPath 水印文件路径 * fontPath 字体路径 * OutputStream pdf文件流 */ private static OutputStream addWaterImage(ByteArrayOutputStream outputStream, String waterMarkPath, String fontPath) { BaseFont baseFont = createFont(fontPath); try (InputStream input = new ByteArrayInputStream(outputStream.toByteArray());) { ByteArrayOutputStream output = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(input); PdfStamper stamp = new PdfStamper(reader, output); PdfContentByte contentByte = null; int n = reader.getNumberOfPages(); Image logo = null; if(StringUtils.isNotBlank(waterMarkPath)){ logo = Image.getInstance(waterMarkPath); } for (int i = 1; i <= n; i++) { contentByte = stamp.getUnderContent(i); Rectangle rectangle = reader.getPageSize(i); float width = rectangle.getWidth(); float height = rectangle.getHeight(); if(logo != null){ logo.setAbsolutePosition(width / 2 - logo.getWidth() / 2, height / 2); contentByte.addImage(logo); contentByte.saveState(); } String text = "第 " + i + " 页 /共 " + n + " 页"; contentByte.beginText(); contentByte.setFontAndSize(baseFont, 12); contentByte.showTextAligned(Element.ALIGN_CENTER, text, (width / 2) - 6, 15, 0); contentByte.endText(); } reader.close(); stamp.close(); return output; } catch (Exception e) { log.debug("添加水印和页码失败," + waterMarkPath, e); throw new RuntimeException("添加水印和页码失败," + waterMarkPath, e); } }
private static BaseFont createFont(String fontPath) { try { return BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); } catch (Exception e) { log.debug("字体读取失败," + fontPath, e); throw new RuntimeException("字体读取失败," + fontPath, e); } }
从流获取水印
/** * outputStream 生成的pdf流 * waterMarkPath 水印流 * fontPath 字体路径 * OutputStream pdf文件流 */ private static OutputStream addWaterImage(ByteArrayOutputStream outputStream, InputStream waterMarkPath, String fontPath) { BaseFont baseFont = createFont(fontPath); try (InputStream input = new ByteArrayInputStream(outputStream.toByteArray());) { ByteArrayOutputStream output = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(input); PdfStamper stamp = new PdfStamper(reader, output); PdfContentByte contentByte = null; int n = reader.getNumberOfPages(); Image logo = null; if(waterMarkPath != null){ byte[] waterMarkBytes = IOUtils.toByteArray(inputStream); logo = Image.getInstance(waterMarkBytes); } for (int i = 1; i <= n; i++) { contentByte = stamp.getUnderContent(i); Rectangle rectangle = reader.getPageSize(i); float width = rectangle.getWidth(); float height = rectangle.getHeight(); if(logo != null){ logo.setAbsolutePosition(width / 2 - logo.getWidth() / 2, height / 2); contentByte.addImage(logo); contentByte.saveState(); } String text = "第 " + i + " 页 /共 " + n + " 页"; contentByte.beginText(); contentByte.setFontAndSize(baseFont, 12); contentByte.showTextAligned(Element.ALIGN_CENTER, text, (width / 2) - 6, 15, 0); contentByte.endText(); } reader.close(); stamp.close(); return output; } catch (Exception e) { log.debug("添加水印和页码失败," + waterMarkPath, e); throw new RuntimeException("添加水印和页码失败," + waterMarkPath, e); } }
pdf加密码及权限设置
权限说明
权限 | 说明 |
---|---|
ALLOW_PRINTING | 文档允许打印 |
ALLOW_DEGRADED_PRINTING | 允许用户打印文档,但不提供allow_printing质量(128位加密) |
ALLOW_MODIFY_CONTENTS | 允许用户修改内容,例如 更改页面内容,或插入或删除页 |
ALLOW_ASSEMBLY | 允许用户插入、删除和旋转页面和添加书签。页面的内容不能更改,除非也授予allow_modify_contents权限,(128位加密) |
ALLOW_COPY | 允许用户复制或以其他方式从文档中提取文本和图形,包括使用辅助技术。例如屏幕阅读器或其他可访问设备 |
ALLOW_SCREENREADERS | 允许用户提取文本和图形以供易访问性设备使用,(128位加密) |
ALLOW_MODIFY_ANNOTATIONS | 允许用户添加或修改文本注释和交互式表单字段 |
ALLOW_FILL_IN | 允许用户填写表单字段,(128位加密) |
需要多个权限时,用|
拼接即可
无水印页码
在生成PDF时添加权限及密码
private static ByteArrayOutputStream htmlToPdf(String htmlContent, String fontPath, String password, String adminPassword) { try { ByteArrayOutputStream output = new ByteArrayOutputStream(); ITextRenderer render = new ITextRenderer(); ITextFontResolver fontResolver = render.getFontResolver(); setPDFEncryption(password, adminPassword, render); fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); render.setDocumentFromString(htmlContent); render.getSharedContext().setBaseURL(BASE_URL); render.layout(); render.createPDF(output); return output; } catch (Exception e) { log.debug("html转换pdf失败!", e); throw new RuleException(ErrCodeFile.CO_HTML_TO_PDF_FAILED); } }
/** 用户权限,根据需求自己设置*/ private static final int PERMIT = PdfWriter.ALLOW_PRINTING; /** pdf加密类型*/ private static final int ENCRYPTION_TYPE = PdfWriter.STANDARD_ENCRYPTION_128; private static void setPDFEncryption(String password, String adminPassword,ITextRenderer render) { PDFEncryption pdfEncryption = new PDFEncryption(); // 用户密码 pdfEncryption.setUserPassword(password.getBytes()); // 管理员密码 pdfEncryption.setOwnerPassword(adminPassword.getBytes()); // 用户权限 pdfEncryption.setAllowedPrivileges(PERMIT); // 加密类型 pdfEncryption.setEncryptionType(ENCRYPTION_TYPE); render.setPDFEncryption(pdfEncryption); }
有水印页码
在生成PDF时无需添加权限及密码,在添加水印及页码时添加
private static OutputStream addWaterImage(ByteArrayOutputStream outputStream, InputStream waterMarkPath, String fontPath, String password, String adminPassword) { BaseFont baseFont = createFont(fontPath); try (InputStream input = new ByteArrayInputStream(outputStream.toByteArray());) { ByteArrayOutputStream output = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(input); PdfStamper stamp = new PdfStamper(reader, output); // 用户密码,管理员密码,权限,加密类型 stamp.setEncryption(password.getBytes(), adminPassword.getBytes(), PERMIT, ENCRYPTION_TYPE); PdfContentByte contentByte = null; int n = reader.getNumberOfPages(); Image logo = null; if(waterMarkPath != null){ byte[] waterMarkBytes = IOUtils.toByteArray(inputStream); logo = Image.getInstance(waterMarkBytes); } for (int i = 1; i <= n; i++) { contentByte = stamp.getUnderContent(i); Rectangle rectangle = reader.getPageSize(i); float width = rectangle.getWidth(); float height = rectangle.getHeight(); if(logo != null){ logo.setAbsolutePosition(width / 2 - logo.getWidth() / 2, height / 2); contentByte.addImage(logo); contentByte.saveState(); } String text = "第 " + i + " 页 /共 " + n + " 页"; contentByte.beginText(); contentByte.setFontAndSize(baseFont, 12); contentByte.showTextAligned(Element.ALIGN_CENTER, text, (width / 2) - 6, 15, 0); contentByte.endText(); } reader.close(); stamp.close(); return output; } catch (Exception e) { log.debug("添加水印和页码失败," + waterMarkPath, e); throw new RuntimeException("添加水印和页码失败," + waterMarkPath, e); } }
模板记录
1、字符集乱码问题
<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>
(历史原因导致开发使用GBK字符集,UTF-8的情况暂时未知)charset应与Java代码中的传入的保持一致,同时ftl文件的字符集应与此保持一致,windows使用GBK,linux使用UTF-8,否则生成PDF乱码
2、img标签
<img src="${logoImage}" width="204"/>
img标签支持base64格式data:image/png;base64,
data:,文本数据 data:text/plain,文本数据 data:text/html,HTML代码 data:text/html;base64,base64编码的HTML代码 data:text/css,CSS代码 data:text/css;base64,base64编码的CSS代码 data:text/javascript,Javascript代码 data:text/javascript;base64,base64编码的Javascript代码 data:image/gif;base64,base64编码的gif图片数据 data:image/png;base64,base64编码的png图片数据 data:image/jpeg;base64,base64编码的jpeg图片数据 data:image/x-icon;base64,base64编码的icon图片数据
3、强制分页
<p style="margin: 0pt"> <div style="page-break-before: always; clear: both"/> </p>
4、 head记录
<head> <meta http-equiv="Content-Type" content="text/html; charset=GBK"/> <meta http-equiv="Content-Style-Type" content="text/css"/> <title>xxxx</title> <style type='text/css'></style> </head>