在一些业务场景中,需要生成pdf文件或者jpg图片,有时候还需要带上水印。我们可以事先用freemarker定义好html模板,然后把模板转换成pdf或jpg文件。
同时freemarker模板还支持变量的定义,在使用时可以填充具体的业务数据。
1、Maven导包
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version></parent><dependencies><!-- freemarker --><dependency><groupId>org.springframework</groupId><artifactId>spring-context-support</artifactId></dependency><dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId></dependency><!-- pdf核心包 --><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.12</version></dependency><!-- 适配中文字体 --><dependency><groupId>com.itextpdf</groupId><artifactId>itext-asian</artifactId><version>5.2.0</version></dependency><!-- html转pdf --><dependency><groupId>com.itextpdf.tool</groupId><artifactId>xmlworker</artifactId><version>5.5.12</version></dependency><!-- pdf转图片 --><dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.5</version></dependency></dependencies>
2、接口定义
2.1、请求
@Datapublic class GeneratePdfReq {/*** 生成pdf文件的绝对路径*/@NotBlank(message = "生成pdf文件的绝对路径不能为空")@Pattern(regexp = "^.*(\\.pdf|\\.jpg)$", message = "生成的文件必须以.pdf或.jpg结尾")private String absolutePath;/*** 使用html模板的绝对路径*/@NotBlank(message = "使用的模板路径不能为空")private String templateName;/*** 渲染模板的业务数据*/private Object dataModel;/*** 水印信息*/private WaterMarkInfo waterMarkInfo;/*** pdf文件的宽,默认A4*/private float width = 595;/*** pdf文件的高,默认A4*/private float height = 842;}
2.2、水印
@Datapublic class WaterMarkInfo {/*** 如果为null设置水印时会报错*/private String waterMark = "";/*** 水印透明度,值越小透明度越高*/private float opacity = 0.2F;/*** 水印字体,如果乱码设置为本地宋体字体:fonts/simsun.ttc,1*/private String fontName = "STSong-Light";/*** 水印编码格式,如果乱码设置为:BaseFont.IDENTITY_H*/private String encoding = "UniGB-UCS2-H";/*** 字体大小*/private float fontSize = 24;/*** 横坐标在页面宽度的百分比,左下角为原点*/private float x = 50;/*** 纵坐标在页面高度的百分比,左下角为原点*/private float y = 40;/*** 水印旋转角度*/private float rotation = 45;}
2.3、响应
@Datapublic class GeneratePdfResp {/*** 生成pdf的绝对路径*/private String absolutePath;}
3、应用代码
3.1、渲染freemarker模板获取html网页
@Service("freeMarkerService")@Slf4jpublic class FreeMarkerServiceImpl implements FreeMarkerService {@Autowiredprivate FreeMarkerConfigurer freeMarkerConfigurer;/*** 渲染html后获取整个页面内容** @param templatePath 模板路径* @param dataModel 业务数据,一般以map形式传入* @return*/@Overridepublic String getHtml(String templatePath, Object dataModel) {log.info("开始将模板{}渲染为html,业务数据{}", templatePath, JSONUtil.toJsonPrettyStr(dataModel));Configuration cfg = freeMarkerConfigurer.getConfiguration();cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // freemaker异常时仍旧抛出,统一异常处理cfg.setClassicCompatible(true);// 不需要对null值预处理,否则需要在模板取值时判断是否存在,不然报错StringWriter stringWriter = new StringWriter();try {// 设置模板所在目录,绝对路径方式,不打进jar包// cfg.setDirectoryForTemplateLoading(new File(templatePath).getParentFile());// Template temp = cfg.getTemplate(new File(templatePath).getName());// 相对路径设置模板所在目录,模板打进jar包,默认就是resources目录下的/templates目录。cfg.setClassForTemplateLoading(this.getClass(), "/templates");Template temp = cfg.getTemplate(templatePath);temp.process(dataModel, stringWriter);} catch (Exception e) {log.error(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL.getDesc(), e);throw new PdfBizException(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL);}return stringWriter.toString();}}
3.2、将html网页转pdf,并添加水印
@Service("pdfService")@Slf4jpublic class PdfServiceImpl implements PdfService {public static final String FONT_PATH = "fonts/simsun.ttc,1";@Autowiredprivate WaterMarkerService waterMarkerService;/*** html页面内容转pdf,并给每页附上水印** @param html html页面内容* @param width pdf的宽* @param height pdf的高* @param waterMarkInfo 水印信息* @return*/@Overridepublic byte[] html2Pdf(String html, float width, float height, WaterMarkInfo waterMarkInfo) {log.info("=================开始将html转换为pdf=================");ByteArrayOutputStream out = new ByteArrayOutputStream();this.html2Pdf(html, width, height, out);byte[] bytes = out.toByteArray();// 设置水印if (waterMarkInfo != null) {bytes = waterMarkerService.addWaterMarker(bytes, waterMarkInfo);}return bytes;}/*** html转pdf** @param html html页面内容* @param width pdf的宽* @param height pdf的高* @param out 输出流,pdf文件用此流输出,需要pdf文档关闭后流中才会有数据*/@Override@SneakyThrowspublic void html2Pdf(String html, float width, float height, OutputStream out) {@Cleanup Document document = new Document(new RectangleReadOnly(width, height)); // 默认A4纵向// 这里需要关闭document才能让生成的pdf字节数据刷到输出流中PdfWriter writer = PdfWriter.getInstance(document, out); // 关闭可能导致生成的pdf显示异常(Chrome)document.open();// 设置字体,这里统一用simsun.ttc即宋体XMLWorkerFontProvider asianFontProvider = new XMLWorkerFontProvider() {@Overridepublic Font getFont(String fontname, String encoding, boolean embedded, float size, int style, BaseColor color, boolean cached) {Font font;try {font = new Font(BaseFont.createFont(FONT_PATH, BaseFont.IDENTITY_H, BaseFont.EMBEDDED));} catch (Exception e) {log.error(PdfErrorCode.SET_PDF_FONT_FAIL.getDesc(), e);throw new PdfBizException(PdfErrorCode.SET_PDF_FONT_FAIL);}font.setStyle(style);font.setColor(color);if (size > 0) {font.setSize(size);}return font;}};// 生成pdftry {XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"), asianFontProvider);// 如果系统已经装有simsun.ttc字体,则不需要单独设置字体也不需要itext-asian jar包// XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"));} catch (RuntimeWorkerException e) {log.error(PdfErrorCode.HTML_CONVERT2PDF_FAIL.getDesc(), e);throw new PdfBizException(PdfErrorCode.HTML_CONVERT2PDF_FAIL);}}}
添加水印实现类
@Service("waterMarkerService")@Slf4jpublic class WaterMarkerServiceImpl implements WaterMarkerService {/*** 给pdf文件每页添加水印** @param source pdf文件的字节数组形式* @param waterMarkInfo 水印信息* @return*/@Overridepublic byte[] addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo) {log.info("开始设置水印数据{}", JSONUtil.toJsonPrettyStr(waterMarkInfo));ByteArrayOutputStream out = new ByteArrayOutputStream();this.addWaterMarker(source, waterMarkInfo, out);return out.toByteArray();}/*** 给pdf文件每页添加水印** @param source pdf文件的字节数组形式* @param waterMarkInfo 水印信息* @param out 输出流,pdf文件用此流输出,需要pdf文档关闭后流中才会有数据*/@Override@SneakyThrowspublic void addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo, OutputStream out) {@Cleanup PdfReader reader = new PdfReader(source);// 这里需要关闭PdfStamper才能让生成的pdf字节数据刷到输出流中@Cleanup PdfStamper pdfStamper = new PdfStamper(reader, out);BaseFont font = BaseFont.createFont(waterMarkInfo.getFontName(), waterMarkInfo.getEncoding(), BaseFont.EMBEDDED);PdfGState gs = new PdfGState();gs.setFillOpacity(waterMarkInfo.getOpacity());// 给每页pdf生成水印for (int i = 1; i <= reader.getNumberOfPages(); i++) {PdfContentByte waterMarker = pdfStamper.getUnderContent(i);waterMarker.beginText();// 设置水印透明度waterMarker.setGState(gs);// 设置水印字体和大小waterMarker.setFontAndSize(font, waterMarkInfo.getFontSize());// 设置水印位置、内容、旋转角度float X = reader.getPageSize(i).getWidth() * waterMarkInfo.getX() / 100;float Y = reader.getPageSize(i).getHeight() * waterMarkInfo.getY() / 100;waterMarker.showTextAligned(Element.ALIGN_CENTER, waterMarkInfo.getWaterMark(), X, Y, waterMarkInfo.getRotation());// 设置水印颜色waterMarker.setColorFill(BaseColor.GRAY);waterMarker.endText();}}}
3.3、整合实现
@Slf4j@Service("generatePdfService")public class GeneratePdfServiceImpl implements RestService {@Autowiredprivate FreeMarkerService freeMarkerService;@Autowiredprivate PdfService pdfService;@Override@SneakyThrowspublic GeneratePdfResp service(GeneratePdfReq generatePdfReq) {log.info("开始生成pdf文件,请求报文:{}", JSONUtil.toJsonPrettyStr(generatePdfReq));/*1.根据freemarker模板填充业务数据获取完整的html字符串*/String html = freeMarkerService.getHtml(generatePdfReq.getTemplateName(), generatePdfReq.getDataModel());/*2.生成pdf文件(内存)*/byte[] bytes = pdfService.html2Pdf(html, generatePdfReq.getWidth(), generatePdfReq.getHeight(), generatePdfReq.getWaterMarkInfo());/*3.本地保存pdf文件*/File targetFile = new File(generatePdfReq.getAbsolutePath());// 上级目录不存在则创建if (!targetFile.getParentFile().exists()) {targetFile.getParentFile().mkdirs();}// 根据不同文件名后缀生成对应文件if (generatePdfReq.getAbsolutePath().endsWith("pdf")) {FileUtils.writeByteArrayToFile(targetFile, bytes);} else {@Cleanup PDDocument document = PDDocument.load(bytes);PDFRenderer renderer = new PDFRenderer(document);BufferedImage bufferedImage = renderer.renderImageWithDPI(0, 150);// 只打第一页,dpi越大图片越高清也越耗时ByteArrayOutputStream baos = new ByteArrayOutputStream();ImageIO.write(bufferedImage, "jpg", baos);FileUtils.writeByteArrayToFile(targetFile, baos.toByteArray());}log.info("文件本地保存完成,文件路径:[{}]", targetFile.getAbsolutePath());/*4.组织返回*/GeneratePdfResp generatePdfResp = new GeneratePdfResp();generatePdfResp.setAbsolutePath(targetFile.getAbsolutePath());return generatePdfResp;}}
3.4、controller
@Slf4j@RestControllerpublic class PdfController {@Autowiredprivate RestService generatePdfService;@PostMapping(value = "/html2Pdf")public GeneratePdfResp html2Pdf(@RequestBody @Validated GeneratePdfReq req) {GeneratePdfResp resp = generatePdfService.service(req);return resp;}}
4、应用
4.1、freemarker模板(html模板)
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><meta http-equiv="Content-Style-Type" content="text/css"/><style>body {font-family: SimSun}</style><title>html模板</title></head><body><div><p style="margin:0pt; orphans:0; text-align:center; widows:0"><span style="font-family:SimSun; font-size:16pt">html模板</span><br/></p><p>姓名:${name}</p><p>证件号码:${cardNo}</p><p>日期:${date}</p></div></body></html>


