功能:
实现根据freemarker模板生成对应的PDF文件;
可以指定文字、位置、页数生成指定的印章(图片),可以指定印章大小;
指定字体、字体大小、文字方向、颜色等生成文字水印
maven依赖:
<dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf-itext5</artifactId> <version>9.1.18</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.27-incubating</version> </dependency>
将html模板和数据进行匹配,然后用流的方式生成PDF文件
生成PDF工具类源码:
package com.lifengdi.file.pdf; import com.itextpdf.text.*; import com.itextpdf.text.pdf.*; import com.itextpdf.text.pdf.parser.PdfReaderContentParser; import com.lifengdi.config.GlobalConfig; import com.lifengdi.config.PDFFontConfig; import com.lifengdi.config.SystemConfig; import com.lifengdi.file.pdf.listener.MyTextLocationListener; import com.lifengdi.model.pdf.Location; import com.lifengdi.model.pdf.PDFStamperConfig; import com.lifengdi.model.pdf.PDFTempFile; import com.lifengdi.model.pdf.PDFWatermarkConfig; import com.lifengdi.util.GeneratedKey; import com.lifengdi.util.MyStringUtil; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateExceptionHandler; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; import org.xhtmlrenderer.pdf.ITextFontResolver; import org.xhtmlrenderer.pdf.ITextRenderer; import javax.annotation.Resource; import java.io.*; import java.util.Objects; /** * 生成PDF工具类 * * @author 李锋镝 * @date Create at 10:42 2019/5/9 */ @Component @Slf4j public class HtmlToPDF { @Resource private SystemConfig systemConfig; @Resource private GeneratedKey generatedKey; /** * 模板和数据匹配 * * @param data 数据 * @param pdfTempFile pdfTempFile * @return html */ public String matchDataToHtml(Object data, PDFTempFile pdfTempFile) { StringWriter writer = new StringWriter(); String html; try { // FreeMarker配置 Configuration config = new Configuration(Configuration.VERSION_2_3_25); config.setDefaultEncoding(GlobalConfig.DEFAULT_ENCODING); // 注意这里是模板所在文件夹,不是模版文件 String parentPath, tempFileName = pdfTempFile.getTemplateFileName(); if (MyStringUtil.isHttpUrl(pdfTempFile.getTemplateFileParentPath())) { parentPath = systemConfig.getLocalTempPath(); } else { parentPath = pdfTempFile.getTemplateFileParentPath(); // 将项目中的文件copy到服务器本地 if (!parentPath.endsWith(File.separator)) parentPath = parentPath + File.separator; String localTempPath = systemConfig.getLocalTempPath(); String target = (localTempPath.endsWith(File.separator) ? localTempPath : localTempPath + File.separator) + tempFileName; InputStream tempFileInputStream = new ClassPathResource(parentPath + tempFileName).getInputStream(); FileUtils.copyInputStreamToFile(tempFileInputStream, new File(target)); } log.info("模板和数据匹配,模板文件parentPath:{}", parentPath); config.setDirectoryForTemplateLoading(new File(systemConfig.getLocalTempPath())); config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); config.setLogTemplateExceptions(false); // 根据模板名称 获取对应模板 Template template = config.getTemplate(tempFileName); // 模板和数据的匹配 template.process(data, writer); writer.flush(); html = writer.toString(); return html; } catch (Exception e) { log.error("PDF模板和数据匹配异常", e); } finally { try { writer.close(); } catch (IOException e) { log.error("StringWriter close exception.", e); } } return null; } /** * 生成PDF * * @param html html字符串 * @param targetFileName 目标文件名 * @return 生成文件的名称 * @throws Exception e */ public String createPDF(String html, String targetFileName) throws Exception { if (StringUtils.isBlank(html)) { return null; } targetFileName = getFileName(targetFileName); log.info("生成PDF,targetFileName:{}", targetFileName); String targetFilePath = getTargetFileTempPath(targetFileName); FileOutputStream outFile = new FileOutputStream(targetFilePath); ITextRenderer renderer = new ITextRenderer(); renderer.setDocumentFromString(html); // 解决中文支持问题 log.info("加载字体"); ITextFontResolver fontResolver = renderer.getFontResolver(); fontResolver.addFont(SystemConfig.SourceHanSansCN_Regular_TTF, "SourceHanSansCN", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED, null); fontResolver.addFont(SystemConfig.FONT_PATH_SONG, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); renderer.layout(); renderer.createPDF(outFile); log.info("生成PDF,targetFileName:{},生成成功", targetFileName); return targetFileName; } /** * 渲染文件 * * @param filePath PDF文件路径 * @param outFilePath PDF文件路径 * @param pdfStamperConfig pdfStamperConfig * @param pdfWatermarkConfig pdfWatermarkConfig */ public void renderLayer(String filePath, String outFilePath, PDFStamperConfig pdfStamperConfig, PDFWatermarkConfig pdfWatermarkConfig) { log.info("渲染文件,filePath:{},outFilePath:{}", filePath, outFilePath); InputStream inputStream = null; try { filePath = getTargetFileTempPath(filePath); inputStream = new FileInputStream(filePath); PdfReader reader = new PdfReader(inputStream); PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(getTargetFileTempPath(outFilePath))); if (Objects.nonNull(pdfStamperConfig) && pdfStamperConfig.getGenerate()) { // 印章 log.info("添加印章,pdfStamperConfig:{}", pdfStamperConfig); stamper(pdfStamperConfig, reader, stamper); } if (Objects.nonNull(pdfWatermarkConfig) && pdfWatermarkConfig.getGenerate()) { // 水印 log.info("添加水印,pdfWatermarkConfig:{}", pdfWatermarkConfig); watermark(reader, stamper, pdfWatermarkConfig); } stamper.close(); reader.close(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (null != inputStream) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } /** * 印章 * * @param pdfStamperConfig pdfStamperConfig * @param reader reader * @param stamper stamper * @throws IOException IOException * @throws DocumentException DocumentException */ private void stamper(PDFStamperConfig pdfStamperConfig, PdfReader reader, PdfStamper stamper) throws IOException, DocumentException { Image image; if (!MyStringUtil.isHttpUrl(pdfStamperConfig.getStamperUrl())) { // 将项目中的文件copy到服务器本地 String imagePath = pdfStamperConfig.getStamperUrl(); String imageName = imagePath.substring(imagePath.indexOf("/") + 1, imagePath.length()); String localTempPath = systemConfig.getLocalTempPath(); String target = (localTempPath.endsWith(File.separator) ? localTempPath : localTempPath + File.separator) + imageName; InputStream tempFileInputStream = new ClassPathResource(pdfStamperConfig.getStamperUrl()).getInputStream(); FileUtils.copyInputStreamToFile(tempFileInputStream, new File(target)); image = Image.getInstance(target); } else { image = Image.getInstance(pdfStamperConfig.getStamperUrl()); } PdfReaderContentParser parser = new PdfReaderContentParser(reader); Document document = new Document(); log.info("A4纸width:{},height:{}", document.getPageSize().getWidth(), document.getPageSize().getHeight()); // // 取所在页和坐标,左下角为起点 // float x = document.getPageSize().getWidth() - 240; // float y = document.getPageSize().getHeight() - 680; int pageSize = reader.getNumberOfPages(); Location location = pdfStamperConfig.getLocation(); if (Objects.nonNull(location)) { Integer page = location.getPage();// -1:全部,0:最后一页,1:首页 if (Objects.nonNull(page)) { switch (page) { case -1: for (int pageNumber = 1; pageNumber <= pageSize; pageNumber++) { insertImage(pdfStamperConfig, stamper, image, parser, pageSize); } break; case 0: insertImage(pdfStamperConfig, stamper, image, parser, pageSize); break; default: if (page > 0) { insertImage(pdfStamperConfig, stamper, image, parser, page); } break; } } } } /** * 向指定位置插入图片 * * @param pdfStamperConfig pdfStamperConfig * @param stamper stamper * @param image image * @param parser parser * @param pageNumber pageNumber * @throws IOException IOException * @throws DocumentException DocumentException */ private void insertImage(PDFStamperConfig pdfStamperConfig, PdfStamper stamper, Image image, PdfReaderContentParser parser, int pageNumber) throws IOException, DocumentException { float x, y; Location xy = new Location(); if (StringUtils.isNotBlank(pdfStamperConfig.getLocation().getWord())) { parser.processContent(pageNumber, new MyTextLocationListener(pdfStamperConfig, xy)); } else { xy = pdfStamperConfig.getLocation(); } if (Objects.isNull(xy)) { return; } if (Objects.isNull(xy.getX()) || Objects.isNull(xy.getY())) { return; } x = xy.getX(); y = xy.getY(); if (x < 0 || y < 0) { return; } // 读图片 // 获取操作的页面 PdfContentByte under = stamper.getOverContent(pageNumber); // 根据域的大小缩放图片 // image.scaleToFit(Objects.isNull(pdfStamperConfig.getFitWidth()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_WIDTH : pdfStamperConfig.getFitWidth(), // Objects.isNull(pdfStamperConfig.getFitHeight()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_HEIGHT : pdfStamperConfig.getFitHeight()); image.scaleAbsolute(Objects.isNull(pdfStamperConfig.getFitWidth()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_WIDTH : pdfStamperConfig.getFitWidth(), Objects.isNull(pdfStamperConfig.getFitHeight()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_HEIGHT : pdfStamperConfig.getFitHeight()); // 添加图片 image.setAbsolutePosition(x, y); under.addImage(image); } /** * 水印 * * @param reader reader * @param stamper stamper * @param pdfWatermarkConfig pdfWatermarkConfig */ private void watermark(PdfReader reader, PdfStamper stamper, PDFWatermarkConfig pdfWatermarkConfig) { if (Objects.isNull(pdfWatermarkConfig) || !pdfWatermarkConfig.getGenerate()) { return; } PdfContentByte under; // 字体 BaseFont font = PDFFontConfig.FONT_MAP.get(PDFFontConfig.SIM_SUN); String fontFamily = pdfWatermarkConfig.getFontFamily(); if (StringUtils.isNotBlank(fontFamily) && PDFFontConfig.FONT_MAP.containsKey(fontFamily)) { font = PDFFontConfig.FONT_MAP.get(fontFamily); } // 原pdf文件的总页数 int pageSize = reader.getNumberOfPages(); PdfGState gs = new PdfGState(); // 设置填充字体不透明度为0.1f gs.setFillOpacity(0.1f); Document document = new Document(); float documentWidth = document.getPageSize().getWidth(), documentHeight = document.getPageSize().getHeight(); Location location = pdfWatermarkConfig.getLocation(); if (Objects.isNull(location)) { location = new Location(); } final float xStart = 0, yStart = 0, xInterval = Objects.nonNull(location.getXInterval()) ? location.getXInterval() : GlobalConfig.DEFAULT_X_INTERVAL, yInterval = Objects.nonNull(location.getYInterval()) ? location.getYInterval() : GlobalConfig.DEFAULT_Y_INTERVAL, rotation = 45, fontSize = Objects.isNull(pdfWatermarkConfig.getFontSize()) ? GlobalConfig.DEFAULT_FONT_SIZE : pdfWatermarkConfig.getFontSize(); String watermarkWord = pdfWatermarkConfig.getWatermarkWord(); int red = -1, green = -1, blue = -1; String[] colorArray = pdfWatermarkConfig.getWatermarkColor().split(","); if (colorArray.length >= 3) { red = Integer.parseInt(colorArray[0]); green = Integer.parseInt(colorArray[1]); blue = Integer.parseInt(colorArray[2]); } for (int i = 1; i <= pageSize; i++) { // 水印在之前文本下 if (Objects.nonNull(location.getOverContent()) && location.getOverContent()) { under = stamper.getOverContent(i); } else { under = stamper.getUnderContent(i); } under.beginText(); // 文字水印 颜色 if (red >= 0) { under.setColorFill(new BaseColor(red, green, blue)); } else { under.setColorFill(BaseColor.GRAY); } // 文字水印 字体及字号 under.setFontAndSize(font, fontSize); under.setGState(gs); // 文字水印 起始位置 under.setTextMatrix(xStart, yStart); if (StringUtils.isNotBlank(watermarkWord)) { for (float x = xStart; x <= documentWidth + xInterval; x += xInterval) { for (float y = yStart; y <= documentHeight + yInterval; y += yInterval) { under.showTextAligned(Element.ALIGN_CENTER, watermarkWord, x, y, rotation); } } } under.endText(); } } /** * 获取生成的PDF文件本地临时路径 * * @param targetFileName 目标文件名 * @return 本地临时路径 */ public String getTargetFileTempPath(String targetFileName) { String localTempPath = systemConfig.getLocalTempPath(); if (!localTempPath.endsWith(File.separator)) { localTempPath = localTempPath + File.separator; } return localTempPath + targetFileName; } private String getFileName(String targetFileName) { if (StringUtils.isBlank(targetFileName)) { targetFileName = generatedKey.generatorKey(); } if (!StringUtils.endsWithIgnoreCase(targetFileName, GlobalConfig.PDF_SUFFIX)) { targetFileName = targetFileName + GlobalConfig.PDF_SUFFIX; } return targetFileName; } }
获取指定文字坐标,用来在指定文字上生成印章,主要实现了RenderListener来获取指定文字的坐标。
源码:
package com.lifengdi.file.pdf.listener; import com.itextpdf.awt.geom.Rectangle2D; import com.itextpdf.text.pdf.parser.ImageRenderInfo; import com.itextpdf.text.pdf.parser.RenderListener; import com.itextpdf.text.pdf.parser.TextRenderInfo; import com.lifengdi.config.GlobalConfig; import com.lifengdi.model.pdf.Location; import com.lifengdi.model.pdf.PDFStamperConfig; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import java.util.Objects; /** * @author 李锋镝 * @date Create at 15:32 2019/5/9 */ @Slf4j public class MyTextLocationListener implements RenderListener { private String text; private PDFStamperConfig pdfStamperConfig; private Location location; public MyTextLocationListener(PDFStamperConfig pdfStamperConfig, Location location) { if (StringUtils.isNotBlank(pdfStamperConfig.getLocation().getWord())) { this.text = pdfStamperConfig.getLocation().getWord(); } this.pdfStamperConfig = pdfStamperConfig; this.location = location; } @Override public void beginTextBlock() { } @Override public void renderText(TextRenderInfo renderInfo) { String renderInfoText = renderInfo.getText(); if (!StringUtils.isEmpty(renderInfoText) && renderInfoText.contains(text)) { Rectangle2D.Float base = renderInfo.getBaseline().getBoundingRectange(); float leftX = (float) base.getMinX(); float leftY = (float) base.getMinY() - 1; float rightX = (float) base.getMaxX(); float rightY = (float) base.getMaxY() + 1; Rectangle2D.Float rect = new Rectangle2D.Float(leftX, leftY, rightX - leftX, rightY - leftY); // 当前行长度 int length = renderInfoText.length(); // 单个字符的长度 float wordWidth = rect.width / length; // 指定字符串首次出现的索引 int i = renderInfoText.indexOf(text); Float fitWidth = Objects.isNull(pdfStamperConfig.getFitWidth()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_WIDTH : pdfStamperConfig.getFitWidth(); Float fitHeight = Objects.isNull(pdfStamperConfig.getFitHeight()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_HEIGHT : pdfStamperConfig.getFitHeight(); float fitWidthRadius = 0f, fitHeightRadius = 0f; if (fitWidth > 0) { fitWidthRadius = fitWidth / 2; } if (fitHeight > 0) { fitHeightRadius = fitHeight / 2; } // 偏移量 Float xOffset = Objects.isNull(pdfStamperConfig.getXOffset()) ? 0F : pdfStamperConfig.getXOffset(); Float yOffset = Objects.isNull(pdfStamperConfig.getYOffset()) ? 0F : pdfStamperConfig.getYOffset(); // 设置印章的XY坐标 float x, y; if (rect.x < 60) { x = wordWidth * i + fitHeightRadius + xOffset; } else { x = rect.x + xOffset; } y = rect.y - fitWidthRadius + yOffset; location.setY(y > 0 ? y : 0); location.setX(x > 0 ? x : 0); log.info("text:{}, location:{}", text, location); } } @Override public void endTextBlock() { } @Override public void renderImage(ImageRenderInfo renderInfo) { } }
其他配置类:
package com.lifengdi.model.pdf; import lombok.Data; /** * PDF模板配置 * @author 李锋镝 * @date Create at 11:10 2019/5/9 */ @Data public class PDFTempFile { /** * PDF模板文件类型 */ private String pdfType; /** * 模板文件名称 */ private String templateFileName; /** * 模板文件所在父级路径 */ private String templateFileParentPath; /** * 模板文件本地地址 */ private String templateFileLocalPath; }
package com.lifengdi.config; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; /** * @author 李锋镝 * @date Create at 10:51 2019/5/9 */ @Getter @Configuration public class SystemConfig { /** * 文件本地缓存目录 */ @Value("${file.localTempPath}") private String localTempPath; /** * 项目中自带字体的目录 */ public static final String FONT_PATH_SONG = "font/simsun.ttf"; public static final String SourceHanSansCN_Regular_TTF = "font/SourceHanSansCN-Regular.ttf"; /** * 项目中的缓存目录 */ @Value("${file.appTempFolder:file/}") private String appTemplateFolder; }
package com.lifengdi.model.pdf; import lombok.Data; /** * PDF印章配置 * @author 李锋镝 * @date Create at 11:16 2019/5/9 */ @Data public class PDFStamperConfig { /** * 是否生成印章 */ private Boolean generate = false; /** * 印章文件路径(PNG格式) */ private String stamperUrl; /** * 生成印章的位置 */ private Location location; /** * 印章宽度 */ private Float fitWidth; /** * 印章高度 */ private Float fitHeight; /** * X偏移量 */ private Float xOffset; /** * Y偏移量 */ private Float yOffset; }
package com.lifengdi.model.pdf; import lombok.Data; /** * PDF文件水印配置 * @author 李锋镝 * @date Create at 11:17 2019/5/9 */ @Data public class PDFWatermarkConfig { /** * 是否生成水印 */ private Boolean generate; /** * 水印文字 */ private String watermarkWord; /** * 水印文字颜色 red,green,blue */ private String watermarkColor = "128,128,128"; /** * 水印透明度 */ private Float fillOpacity = 0.1F; /** * 水印文字大小 */ private Integer fontSize = 38; /** * 水印文字字体 */ private String fontFamily; /** * 生成水印的位置,不指定则默认整页 */ private Location location; }
package com.lifengdi.model.pdf; import lombok.Data; /** * 页面位置坐标 * @author 李锋镝 * @date Create at 11:35 2019/5/9 */ @Data public class Location { /** * X坐标 */ private Float x; /** * Y坐标 */ private Float y; /** * 横向间隔 */ private Float xInterval; /** * 纵向间隔 */ private Float yInterval; /** * 指定页 -1:全部,0:最后一页,1:首页 */ private Integer page; /** * 在指定文字上盖章 */ private String word; /** * 水印是否在内容上边 */ private Boolean overContent; }
package com.lifengdi.model.pdf; import com.lifengdi.model.IType; import lombok.Data; /** * @author 李锋镝 * @date Create at 14:07 2019/5/9 */ @Data public class PDFType implements IType { private PDFTempFile pdfTempFile; private PDFStamperConfig pdfStamperConfig; private PDFWatermarkConfig pdfWatermarkConfig; }
package com.lifengdi.config; import com.lifengdi.model.pdf.PDFType; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * PDF配置 * @author 李锋镝 * @date Create at 11:10 2019/5/9 */ @Data @Component @ConfigurationProperties("pdf") public class PDFConfig { private Map<String, PDFType> pdfType = new HashMap<>(); }
package com.lifengdi.config; /** * @author 李锋镝 * @date Create at 10:54 2019/5/9 */ public class GlobalConfig { /** * 默认字符集 */ public static final String DEFAULT_ENCODING = "UTF-8"; /** * PDF文件后缀 */ public static final String PDF_SUFFIX = ".pdf"; /** * PDF印章默认宽度 */ public static final float PDF_STAMPER_DEFAULT_FIT_WIDTH = 100f; /** * PDF印章默认高度 */ public static final float PDF_STAMPER_DEFAULT_FIT_HEIGHT = 100f; /** * 默认字体大小 */ public static final float DEFAULT_FONT_SIZE = 38; /** * 默认水印X方向间隔 */ public static final float DEFAULT_X_INTERVAL = 38; /** * 默认水印Y方向间隔 */ public static final float DEFAULT_Y_INTERVAL = 38; }
在application.yml配置如下:
# 生成PDF文件配置 pdf: pdfType: test: pdfTempFile: pdfType: test templateFileName: test.ftl templateFileParentPath: file/ pdfStamperConfig: generate: true # 印章所在路径 stamperUrl: image/666.png xOffset: 0 yOffset: 45 fitWidth: 120 fitHeight: 120 location: x: -1 y: -1 page: 1 word: 检测日期 pdfWatermarkConfig: generate: true watermarkWord: 水印 watermarkColor: fillOpacity: fontSize: fontFamily: SourceHanSansCN location: xInterval: 100 yInterval: 120 page: overContent: true
生成PDF效果截图:
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接
您好,MyStringUtil、GeneratedKey、 IType 这些类和接口没有呀
@石头 文章最下面有源码链接,可以下载下源码看一下,里边都有的。
@李锋镝 好的好的,看见了,感谢
GlobalConfig类是什么?
@唐 GlobalConfig是一个默认的配置类,文章已更新,翻到最下边就可以看到。