org.xhtmlrenderer
flying-saucer-pdf-itext5
9.1.18
org.freemarker
freemarker
2.3.27-incubating
将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 mons.io.FileUtils;
import mons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.ponent;
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 /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 mons.lang3.StringUtils;
import java.util.Objects;
/**
* @author 李锋镝
* @date Create at 15:32 /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 /5/9
*/
@Data
public class PDFTempFile {
/**
* PDF模板文件类型
*/
private String pdfType;
/**
* 模板文件名称
*/
private String templateFileName;
/**
* 模板文件所在父级路径
*/
private String templateFileParentPath;
/**
* 模板文件本地地址
*/
private String templateFileLocalPath;
}