项目场景:
因为打印功能的模板和背景图每年都会更换,但是数据基本不会发生改变,因此将原来项目itext生成pdf重构为页面和数据分离的模式。
目录:
一、引用jar包
1、flying-saucer-core-9.1.5.jar
2、flying-saucer-pdf-9.1.5.jar
3、freemarker.jar
因为我的项目仅仅是个web项目,还不是maven项目,只能单独引入jar包,如果报错好不到某些方法,可以再单独去下载。这个问题好解决。
二、自定义工具类
1、引入字体解决ITextRenderer不支持中文的问题
C:\Windows\Fonts可以在这里找到自己需要的字体,我这里用到了宋体和微软雅黑,所以就加载了四个字体
2、ExportPdfUtils
freemarker不支持远程模板,需要自定义,提供了接口URLTemplateLoader,可以根据自己需要实现不同的实现,我的工具类就简单的将一个http请求变为url,所以就不需要传模板名称了。
package cn.com.mjsoft.sub.common;
import cn.com.mjsoft.sub.dto.PrintProfessionCardDto;
import com.alibaba.fastjson.JSONObject;
import com.itextpdf.text.pdf.BaseFont;
import com.lowagie.text.DocumentException;
import com.sun.istack.internal.NotNull;
import freemarker.cache.URLTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
public final class ExportPdfUtils {
private Configuration cfg;
private Template template;
//字体文件夹路径
private String fontDirectoryPaths = "/statics/fonts/";
//字体文件名称:宋体-微软雅黑
private String[] fontFileNames = new String[]{"simsun.ttc", "msyh.ttc", "msyhbd.ttc", "msyhl.ttc"};
//本地模板文件夹
private String baseTemplateDir = "/statics/template/";
public ExportPdfUtils() {
}
/**
* 特别注意项目本地图片需要单独在img上加上data:image/前缀
*
* @param data
* @param request
* @return
*/
public byte[] createPdf(PrintProfessionCardDto data, HttpServletRequest request) {
data.setStampPic(request.getSession().getServletContext().getRealPath(data.getStampPic()));
JSONObject obj = (JSONObject) JSONObject.toJSON(data);
PdfTemplate pdfTemplate = new PdfTemplate(data.getTemplatePath());
return this.createPdf(obj, pdfTemplate, true, true);
}
public byte[] createPdf(JSONObject data, @NotNull PdfTemplate pdfTemplate, boolean isRemoteTemplate, boolean hasImage) {
try (StringWriter writer = new StringWriter()) {
cfg = new Configuration(Configuration.VERSION_2_3_23);
cfg.setEncoding(Locale.CHINA, "UTF-8");
//模板为远程文件
if (isRemoteTemplate) {
RemoteTemplateLoader templateLoader = new RemoteTemplateLoader(pdfTemplate.getTemplatePackagePath());
cfg.setTemplateLoader(templateLoader);
template = cfg.getTemplate("", "UTF-8");
} else {
//模板为项目本地文件
cfg.setDirectoryForTemplateLoading(new File(pdfTemplate.getTemplatePackagePath()));
template = cfg.getTemplate(pdfTemplate.getTemplateFileName(), "UTF-8");
}
// 将数据输出到html中
template.process(data, writer);
writer.flush();
String html = writer.toString();
return this.transStreamToPdf(html, hasImage);
} catch (TemplateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private byte[] transStreamToPdf(String html, boolean hasImage) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ITextRenderer renderer = new ITextRenderer();
ITextFontResolver fontResolver = renderer.getFontResolver();
for (int i = 0; i < fontFileNames.length; i++) {
String fontPackagePath = ExportPdfUtils.class.getResource("/").getPath() + fontDirectoryPaths + fontFileNames[i];
fontResolver.addFont(fontPackagePath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
}
renderer.setDocumentFromString(html);
// 设置模板中的图片路径 (这里的images在resources目录下) 模板中img标签src路径需要相对路径加图片名 如<img src="images/xh.jpg"/>
SharedContext sharedContext = renderer.getSharedContext();
if (hasImage) {
sharedContext.setReplacedElementFactory(new B64ImgReplacedElementFactory());
sharedContext.getTextRenderer().setSmoothingThreshold(0);
}
renderer.layout();
renderer.createPDF(out);
renderer.finishPDF();
out.flush();
return out.toByteArray();
} catch (DocumentException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
class PdfTemplate {
//本地模板:模板名称不能为空
private String templateFileName;
private String templatePackagePath;
public PdfTemplate(String templatePackagePath) {
this.templatePackagePath = templatePackagePath;
}
public PdfTemplate(String templateFileName, String templatePackagePath) {
this.templateFileName = templateFileName;
this.templatePackagePath = templatePackagePath;
}
public String getTemplateFileName() {
return templateFileName;
}
public void setTemplateFileName(String templateFileName) {
this.templateFileName = templateFileName;
}
public String getTemplatePackagePath() {
return templatePackagePath;
}
public void setTemplatePackagePath(String templatePackagePath) {
this.templatePackagePath = templatePackagePath;
}
}
//自定义实现远程模板文件的获取
final class RemoteTemplateLoader extends URLTemplateLoader {
private String remoteTemplatePath;
public RemoteTemplateLoader(String remoteTemplatePath) {
this.remoteTemplatePath = remoteTemplatePath;
}
@Override
protected URL getURL(String s) {
try {
URL url = new URL(remoteTemplatePath);
return url;
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
}
}
3、B64ImgReplacedElementFactory
由于我的项目不是maven项目,导致需要按默认获取webapp下的文件不可以,所以就自己实现了ITextRenderer
的html解析工厂类,主要是自定义了本地项目的附件获取方式;
package cn.com.mjsoft.sub.common;
import com.lowagie.text.BadElementException;
import com.lowagie.text.Image;
import org.w3c.dom.Element;
import org.xhtmlrenderer.extend.FSImage;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.ReplacedElementFactory;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.pdf.ITextFSImage;
import org.xhtmlrenderer.pdf.ITextImageElement;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
import java.io.IOException;
/**
* @description:
* @author:zxp
* @create:2021-12-30 14-09
*/
public class B64ImgReplacedElementFactory implements ReplacedElementFactory {
/**
* 实现createReplacedElement 替换html中的Img标签
*
* @param c 上下文
* @param box 盒子
* @param uac 回调
* @param cssWidth css宽
* @param cssHeight css高
* @return ReplacedElement
*/
public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac,
int cssWidth, int cssHeight) {
Element e = box.getElement();
if (e == null) {
return null;
}
String nodeName = e.getNodeName();
// 找到img标签
if (nodeName.equals("img")) {
String attribute = e.getAttribute("src");
FSImage fsImage;
try {
// 生成itext图像
fsImage = buildImage(attribute, uac);
} catch (BadElementException e1) {
fsImage = null;
} catch (IOException e1) {
fsImage = null;
}
if (fsImage != null) {
// 对图像进行缩放
if (cssWidth != -1 || cssHeight != -1) {
fsImage.scale(cssWidth, cssHeight);
}
return new ITextImageElement(fsImage);
}
}
return null;
}
/**
* 直接根据url获取项目本地图片或者三方服务器图片并生成itext图像
* 主要是这里自定义
* @param srcAttr 属性
* @param uac 回调
* @return FSImage
* @throws IOException io异常
* @throws BadElementException BadElementException
*/
protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException,
BadElementException {
FSImage fsImage;
if (srcAttr.startsWith("data:image/")) {
String imageName = srcAttr.substring("data:image/".length());
//这里可以通过自定义方式获取文件
//例如通过file的方式将外部的文件转换为流,然后将字节转换为image,这样就可以只提供一个外部文件的地址,将文件名称写入到模板中,每次仅仅修改模板就可以更改文件了。
fsImage = new ITextFSImage(Image.getInstance(imageName));
} else {
fsImage = uac.getImageResource(srcAttr).getImage();
}
return fsImage;
}
/**
* 实现remove
*
* @param e 元素
*/
public void remove(Element e) {
}
/**
* 实现reset
*/
public void reset() {
}
/**
* 实现setFormSubmissionListener
*
* @param formsubmissionlistener 监听
*/
public void setFormSubmissionListener(FormSubmissionListener formsubmissionlistener) {
}
}
三、解决样式问题
1、主要是css3的某些样式不生效,所以尽量不使用css的样式
2、还有就是默认居中的问题,这里需要使用css3的样式解决,这样就可以去除页边距了;设置字体为微软雅黑,不然无法识别中文。
@page {
margin: 0;
}
body{font-family: "Microsoft YaHei";}
四、模板
我是用的是html,没有使用ftl,这个官网文档上有解释,这里贴上官方文档链接http://freemarker.foofun.cn/pgui_config_templateloading.html
总结:
这里就是我碰到的问题和解决方案,反正就是碰到什么问题,就会不断尝试去解决,期间我有几次都只想换方式解决了,后来想想还是尝试下好,不然换新的也可能会碰到更多的问题,最后终于解决了。