项目场景:


因为打印功能的模板和背景图每年都会更换,但是数据基本不会发生改变,因此将原来项目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

themleaf 生成pdf freemaker生成pdf_java

总结:


这里就是我碰到的问题和解决方案,反正就是碰到什么问题,就会不断尝试去解决,期间我有几次都只想换方式解决了,后来想想还是尝试下好,不然换新的也可能会碰到更多的问题,最后终于解决了。