Java生成pdf文件或jpg图片的案例讲解
在一些业务场景中,需要生成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、请求
@Data public 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、水印
@Data public 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、响应
@Data public class GeneratePdfResp { /** * 生成pdf的绝对路径 */ private String absolutePath; }
3、应用代码
3.1、渲染freemarker模板获取html网页
@Service("freeMarkerService") @Slf4j public class FreeMarkerServiceImpl implements FreeMarkerService { @Autowired private FreeMarkerConfigurer freeMarkerConfigurer; /** * 渲染html后获取整个页面内容 * * @param templatePath 模板路径 * @param dataModel 业务数据,一般以map形式传入 * @return */ @Override public 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") @Slf4j public class PdfServiceImpl implements PdfService { public static final String FONT_PATH = "fonts/simsun.ttc,1"; @Autowired private WaterMarkerService waterMarkerService; /** * html页面内容转pdf,并给每页附上水印 * * @param html html页面内容 * @param width pdf的宽 * @param height pdf的高 * @param waterMarkInfo 水印信息 * @return */ @Override public 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 @SneakyThrows public 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() { @Override public 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; } }; // 生成pdf try { 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") @Slf4j public class WaterMarkerServiceImpl implements WaterMarkerService { /** * 给pdf文件每页添加水印 * * @param source pdf文件的字节数组形式 * @param waterMarkInfo 水印信息 * @return */ @Override public 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 @SneakyThrows public 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 { @Autowired private FreeMarkerService freeMarkerService; @Autowired private PdfService pdfService; @Override @SneakyThrows public 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 @RestController public class PdfController { @Autowired private 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>
4.2、接口调用生成pdf
5、说明
1、根据参数后缀名可以生成pdf或jpg文件,生成的pdf文件默认为A4大小,也可以通过请求参数设置大小。
2、pdf文件会根据html模板内容大小自动分页。
3、如果生成图片,多页不会生成多张图片,可以把高度设置大一些,最后会生成长图。
4、水印每页都会自动添加。
5、为了提高代码的复用性和可维护性,工程内渲染html模板、生成pdf文件、添加水印都有单独的接口实现。
代码地址
github:https://github.com/senlinmu1008/spring-boot/tree/master/html2pdf
gitee:https://gitee.com/ppbin/spring-boot/tree/master/html2pdf
以上为个人经验,希望能给大家一个参考,也希望大家多多支持猪先飞。如有错误或未考虑完全的地方,望不吝赐教。
相关文章
- 这篇文章主要介绍了如何利用java语言实现经典《复杂迷宫》游戏,文中采用了swing技术进行了界面化处理,感兴趣的小伙伴可以动手试一试...2022-02-01
java 运行报错has been compiled by a more recent version of the Java Runtime
java 运行报错has been compiled by a more recent version of the Java Runtime (class file version 54.0)...2021-04-01- 这篇文章主要介绍了在java中获取List集合中最大的日期时间操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了教你怎么用Java获取国家法定节假日,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下...2021-04-23
- 这篇文章主要介绍了Java如何发起http请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-31
- 说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别...2020-06-25
- 这篇文章主要介绍了解决Java处理HTTP请求超时的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了java 判断两个时间段是否重叠的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
java 画pdf用itext调整表格宽度、自定义各个列宽的方法
这篇文章主要介绍了java 画pdf用itext调整表格宽度、自定义各个列宽的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-31- 这篇文章主要介绍了超简洁java实现双色球若干注随机号码生成(实例代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-02
- 这篇文章主要介绍了Java生成随机姓名、性别和年龄的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-01
- 这篇文章主要介绍了java正则表达式判断前端参数修改表中另一个字段的值,需要的朋友可以参考下...2021-05-07
Java使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
这篇文章主要介绍了Java使用ScriptEngine动态执行代码,并且分享Java几种动态执行代码比较,需要的朋友可以参考下...2021-04-15- 这篇文章主要介绍了Java开发实现人机猜拳游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-03
- 这篇文章主要介绍了Java List集合返回值去掉中括号('[ ]')的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-29
Java 8 Stream 的终极技巧——Collectors 功能与操作方法详解
这篇文章主要介绍了Java 8 Stream Collectors 功能与操作方法,结合实例形式详细分析了Java 8 Stream Collectors 功能、操作方法及相关注意事项,需要的朋友可以参考下...2020-05-20Java中lombok的@Builder注解的解析与简单使用详解
这篇文章主要介绍了Java中lombok的@Builder注解的解析与简单使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-06- 下面小编就为大家带来一篇java中String类型变量的赋值问题介绍。小编觉得挺不错的。现在分享给大家,给大家一个参考。...2016-03-28
- 这篇文章主要介绍了Java连接数据库oracle中文乱码解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-05-16
- 这篇文章主要介绍了Java线程池中的各个参数如何合理设置操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-19