在一些业务场景中,需要生成pdf文件或者jpg图片,有时候还需要带上水印。我们可以事先用freemarker定义好html模板,然后把模板转换成pdf或jpg文件。
同时freemarker模板还支持变量的定义,在使用时可以填充具体的业务数据。

1、Maven导包

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>2.1.4.RELEASE</version>
  5. </parent>
  6. <dependencies>
  7. <!-- freemarker -->
  8. <dependency>
  9. <groupId>org.springframework</groupId>
  10. <artifactId>spring-context-support</artifactId>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.freemarker</groupId>
  14. <artifactId>freemarker</artifactId>
  15. </dependency>
  16. <!-- pdf核心包 -->
  17. <dependency>
  18. <groupId>com.itextpdf</groupId>
  19. <artifactId>itextpdf</artifactId>
  20. <version>5.5.12</version>
  21. </dependency>
  22. <!-- 适配中文字体 -->
  23. <dependency>
  24. <groupId>com.itextpdf</groupId>
  25. <artifactId>itext-asian</artifactId>
  26. <version>5.2.0</version>
  27. </dependency>
  28. <!-- htmlpdf -->
  29. <dependency>
  30. <groupId>com.itextpdf.tool</groupId>
  31. <artifactId>xmlworker</artifactId>
  32. <version>5.5.12</version>
  33. </dependency>
  34. <!-- pdf转图片 -->
  35. <dependency>
  36. <groupId>org.apache.pdfbox</groupId>
  37. <artifactId>pdfbox</artifactId>
  38. <version>2.0.5</version>
  39. </dependency>
  40. </dependencies>

2、接口定义

2.1、请求

  1. @Data
  2. public class GeneratePdfReq {
  3. /**
  4. * 生成pdf文件的绝对路径
  5. */
  6. @NotBlank(message = "生成pdf文件的绝对路径不能为空")
  7. @Pattern(regexp = "^.*(\\.pdf|\\.jpg)$", message = "生成的文件必须以.pdf或.jpg结尾")
  8. private String absolutePath;
  9. /**
  10. * 使用html模板的绝对路径
  11. */
  12. @NotBlank(message = "使用的模板路径不能为空")
  13. private String templateName;
  14. /**
  15. * 渲染模板的业务数据
  16. */
  17. private Object dataModel;
  18. /**
  19. * 水印信息
  20. */
  21. private WaterMarkInfo waterMarkInfo;
  22. /**
  23. * pdf文件的宽,默认A4
  24. */
  25. private float width = 595;
  26. /**
  27. * pdf文件的高,默认A4
  28. */
  29. private float height = 842;
  30. }

2.2、水印

  1. @Data
  2. public class WaterMarkInfo {
  3. /**
  4. * 如果为null设置水印时会报错
  5. */
  6. private String waterMark = "";
  7. /**
  8. * 水印透明度,值越小透明度越高
  9. */
  10. private float opacity = 0.2F;
  11. /**
  12. * 水印字体,如果乱码设置为本地宋体字体:fonts/simsun.ttc,1
  13. */
  14. private String fontName = "STSong-Light";
  15. /**
  16. * 水印编码格式,如果乱码设置为:BaseFont.IDENTITY_H
  17. */
  18. private String encoding = "UniGB-UCS2-H";
  19. /**
  20. * 字体大小
  21. */
  22. private float fontSize = 24;
  23. /**
  24. * 横坐标在页面宽度的百分比,左下角为原点
  25. */
  26. private float x = 50;
  27. /**
  28. * 纵坐标在页面高度的百分比,左下角为原点
  29. */
  30. private float y = 40;
  31. /**
  32. * 水印旋转角度
  33. */
  34. private float rotation = 45;
  35. }

2.3、响应

  1. @Data
  2. public class GeneratePdfResp {
  3. /**
  4. * 生成pdf的绝对路径
  5. */
  6. private String absolutePath;
  7. }

3、应用代码

3.1、渲染freemarker模板获取html网页

  1. @Service("freeMarkerService")
  2. @Slf4j
  3. public class FreeMarkerServiceImpl implements FreeMarkerService {
  4. @Autowired
  5. private FreeMarkerConfigurer freeMarkerConfigurer;
  6. /**
  7. * 渲染html后获取整个页面内容
  8. *
  9. * @param templatePath 模板路径
  10. * @param dataModel 业务数据,一般以map形式传入
  11. * @return
  12. */
  13. @Override
  14. public String getHtml(String templatePath, Object dataModel) {
  15. log.info("开始将模板{}渲染为html,业务数据{}", templatePath, JSONUtil.toJsonPrettyStr(dataModel));
  16. Configuration cfg = freeMarkerConfigurer.getConfiguration();
  17. cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); // freemaker异常时仍旧抛出,统一异常处理
  18. cfg.setClassicCompatible(true);// 不需要对null值预处理,否则需要在模板取值时判断是否存在,不然报错
  19. StringWriter stringWriter = new StringWriter();
  20. try {
  21. // 设置模板所在目录,绝对路径方式,不打进jar
  22. // cfg.setDirectoryForTemplateLoading(new File(templatePath).getParentFile());
  23. // Template temp = cfg.getTemplate(new File(templatePath).getName());
  24. // 相对路径设置模板所在目录,模板打进jar包,默认就是resources目录下的/templates目录。
  25. cfg.setClassForTemplateLoading(this.getClass(), "/templates");
  26. Template temp = cfg.getTemplate(templatePath);
  27. temp.process(dataModel, stringWriter);
  28. } catch (Exception e) {
  29. log.error(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL.getDesc(), e);
  30. throw new PdfBizException(PdfErrorCode.PDF_TEMPLATE_RENDER_FAIL);
  31. }
  32. return stringWriter.toString();
  33. }
  34. }

3.2、将html网页转pdf,并添加水印

  1. @Service("pdfService")
  2. @Slf4j
  3. public class PdfServiceImpl implements PdfService {
  4. public static final String FONT_PATH = "fonts/simsun.ttc,1";
  5. @Autowired
  6. private WaterMarkerService waterMarkerService;
  7. /**
  8. * html页面内容转pdf,并给每页附上水印
  9. *
  10. * @param html html页面内容
  11. * @param width pdf的宽
  12. * @param height pdf的高
  13. * @param waterMarkInfo 水印信息
  14. * @return
  15. */
  16. @Override
  17. public byte[] html2Pdf(String html, float width, float height, WaterMarkInfo waterMarkInfo) {
  18. log.info("=================开始将html转换为pdf=================");
  19. ByteArrayOutputStream out = new ByteArrayOutputStream();
  20. this.html2Pdf(html, width, height, out);
  21. byte[] bytes = out.toByteArray();
  22. // 设置水印
  23. if (waterMarkInfo != null) {
  24. bytes = waterMarkerService.addWaterMarker(bytes, waterMarkInfo);
  25. }
  26. return bytes;
  27. }
  28. /**
  29. * htmlpdf
  30. *
  31. * @param html html页面内容
  32. * @param width pdf的宽
  33. * @param height pdf的高
  34. * @param out 输出流,pdf文件用此流输出,需要pdf文档关闭后流中才会有数据
  35. */
  36. @Override
  37. @SneakyThrows
  38. public void html2Pdf(String html, float width, float height, OutputStream out) {
  39. @Cleanup Document document = new Document(new RectangleReadOnly(width, height)); // 默认A4纵向
  40. // 这里需要关闭document才能让生成的pdf字节数据刷到输出流中
  41. PdfWriter writer = PdfWriter.getInstance(document, out); // 关闭可能导致生成的pdf显示异常(Chrome
  42. document.open();
  43. // 设置字体,这里统一用simsun.ttc即宋体
  44. XMLWorkerFontProvider asianFontProvider = new XMLWorkerFontProvider() {
  45. @Override
  46. public Font getFont(String fontname, String encoding, boolean embedded, float size, int style, BaseColor color, boolean cached) {
  47. Font font;
  48. try {
  49. font = new Font(BaseFont.createFont(FONT_PATH, BaseFont.IDENTITY_H, BaseFont.EMBEDDED));
  50. } catch (Exception e) {
  51. log.error(PdfErrorCode.SET_PDF_FONT_FAIL.getDesc(), e);
  52. throw new PdfBizException(PdfErrorCode.SET_PDF_FONT_FAIL);
  53. }
  54. font.setStyle(style);
  55. font.setColor(color);
  56. if (size > 0) {
  57. font.setSize(size);
  58. }
  59. return font;
  60. }
  61. };
  62. // 生成pdf
  63. try {
  64. XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"), asianFontProvider);
  65. // 如果系统已经装有simsun.ttc字体,则不需要单独设置字体也不需要itext-asian jar
  66. // XMLWorkerHelper.getInstance().parseXHtml(writer, document, new ByteArrayInputStream(html.getBytes("UTF-8")), null, Charset.forName("UTF-8"));
  67. } catch (RuntimeWorkerException e) {
  68. log.error(PdfErrorCode.HTML_CONVERT2PDF_FAIL.getDesc(), e);
  69. throw new PdfBizException(PdfErrorCode.HTML_CONVERT2PDF_FAIL);
  70. }
  71. }
  72. }

添加水印实现类

  1. @Service("waterMarkerService")
  2. @Slf4j
  3. public class WaterMarkerServiceImpl implements WaterMarkerService {
  4. /**
  5. * pdf文件每页添加水印
  6. *
  7. * @param source pdf文件的字节数组形式
  8. * @param waterMarkInfo 水印信息
  9. * @return
  10. */
  11. @Override
  12. public byte[] addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo) {
  13. log.info("开始设置水印数据{}", JSONUtil.toJsonPrettyStr(waterMarkInfo));
  14. ByteArrayOutputStream out = new ByteArrayOutputStream();
  15. this.addWaterMarker(source, waterMarkInfo, out);
  16. return out.toByteArray();
  17. }
  18. /**
  19. * pdf文件每页添加水印
  20. *
  21. * @param source pdf文件的字节数组形式
  22. * @param waterMarkInfo 水印信息
  23. * @param out 输出流,pdf文件用此流输出,需要pdf文档关闭后流中才会有数据
  24. */
  25. @Override
  26. @SneakyThrows
  27. public void addWaterMarker(byte[] source, WaterMarkInfo waterMarkInfo, OutputStream out) {
  28. @Cleanup PdfReader reader = new PdfReader(source);
  29. // 这里需要关闭PdfStamper才能让生成的pdf字节数据刷到输出流中
  30. @Cleanup PdfStamper pdfStamper = new PdfStamper(reader, out);
  31. BaseFont font = BaseFont.createFont(waterMarkInfo.getFontName(), waterMarkInfo.getEncoding(), BaseFont.EMBEDDED);
  32. PdfGState gs = new PdfGState();
  33. gs.setFillOpacity(waterMarkInfo.getOpacity());
  34. // 给每页pdf生成水印
  35. for (int i = 1; i <= reader.getNumberOfPages(); i++) {
  36. PdfContentByte waterMarker = pdfStamper.getUnderContent(i);
  37. waterMarker.beginText();
  38. // 设置水印透明度
  39. waterMarker.setGState(gs);
  40. // 设置水印字体和大小
  41. waterMarker.setFontAndSize(font, waterMarkInfo.getFontSize());
  42. // 设置水印位置、内容、旋转角度
  43. float X = reader.getPageSize(i).getWidth() * waterMarkInfo.getX() / 100;
  44. float Y = reader.getPageSize(i).getHeight() * waterMarkInfo.getY() / 100;
  45. waterMarker.showTextAligned(Element.ALIGN_CENTER, waterMarkInfo.getWaterMark(), X, Y, waterMarkInfo.getRotation());
  46. // 设置水印颜色
  47. waterMarker.setColorFill(BaseColor.GRAY);
  48. waterMarker.endText();
  49. }
  50. }
  51. }

3.3、整合实现

  1. @Slf4j
  2. @Service("generatePdfService")
  3. public class GeneratePdfServiceImpl implements RestService {
  4. @Autowired
  5. private FreeMarkerService freeMarkerService;
  6. @Autowired
  7. private PdfService pdfService;
  8. @Override
  9. @SneakyThrows
  10. public GeneratePdfResp service(GeneratePdfReq generatePdfReq) {
  11. log.info("开始生成pdf文件,请求报文:{}", JSONUtil.toJsonPrettyStr(generatePdfReq));
  12. /*
  13. 1.根据freemarker模板填充业务数据获取完整的html字符串
  14. */
  15. String html = freeMarkerService.getHtml(generatePdfReq.getTemplateName(), generatePdfReq.getDataModel());
  16. /*
  17. 2.生成pdf文件(内存)
  18. */
  19. byte[] bytes = pdfService.html2Pdf(html, generatePdfReq.getWidth(), generatePdfReq.getHeight(), generatePdfReq.getWaterMarkInfo());
  20. /*
  21. 3.本地保存pdf文件
  22. */
  23. File targetFile = new File(generatePdfReq.getAbsolutePath());
  24. // 上级目录不存在则创建
  25. if (!targetFile.getParentFile().exists()) {
  26. targetFile.getParentFile().mkdirs();
  27. }
  28. // 根据不同文件名后缀生成对应文件
  29. if (generatePdfReq.getAbsolutePath().endsWith("pdf")) {
  30. FileUtils.writeByteArrayToFile(targetFile, bytes);
  31. } else {
  32. @Cleanup PDDocument document = PDDocument.load(bytes);
  33. PDFRenderer renderer = new PDFRenderer(document);
  34. BufferedImage bufferedImage = renderer.renderImageWithDPI(0, 150);// 只打第一页,dpi越大图片越高清也越耗时
  35. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  36. ImageIO.write(bufferedImage, "jpg", baos);
  37. FileUtils.writeByteArrayToFile(targetFile, baos.toByteArray());
  38. }
  39. log.info("文件本地保存完成,文件路径:[{}]", targetFile.getAbsolutePath());
  40. /*
  41. 4.组织返回
  42. */
  43. GeneratePdfResp generatePdfResp = new GeneratePdfResp();
  44. generatePdfResp.setAbsolutePath(targetFile.getAbsolutePath());
  45. return generatePdfResp;
  46. }
  47. }

3.4、controller

  1. @Slf4j
  2. @RestController
  3. public class PdfController {
  4. @Autowired
  5. private RestService generatePdfService;
  6. @PostMapping(value = "/html2Pdf")
  7. public GeneratePdfResp html2Pdf(@RequestBody @Validated GeneratePdfReq req) {
  8. GeneratePdfResp resp = generatePdfService.service(req);
  9. return resp;
  10. }
  11. }

4、应用

4.1、freemarker模板(html模板)

  1. <html>
  2. <head>
  3. <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  4. <meta http-equiv="Content-Style-Type" content="text/css"/>
  5. <style>
  6. body {
  7. font-family: SimSun
  8. }
  9. </style>
  10. <title>html模板</title>
  11. </head>
  12. <body>
  13. <div>
  14. <p style="margin:0pt; orphans:0; text-align:center; widows:0">
  15. <span style="font-family:SimSun; font-size:16pt">html模板</span><br/>
  16. </p>
  17. <p>姓名:${name}</p>
  18. <p>证件号码:${cardNo}</p>
  19. <p>日期:${date}</p>
  20. </div>
  21. </body>
  22. </html>

4.2、接口调用生成pdf

image.png
image.png

5、说明

  1. 根据参数后缀名可以生成pdf或jpg文件,生成的pdf文件默认为A4大小,也可以通过请求参数设置大小。
  2. pdf文件会根据html模板内容大小自动分页。
  3. 如果生成图片,多页不会生成多张图片,可以把高度设置大一些,最后会生成长图。
  4. 水印每页都会自动添加。
  5. 为了提高代码的复用性和可维护性,工程内渲染html模板、生成pdf文件、添加水印都有单独的接口实现。

    参考链接