iText 7 html2pdf 使用总结

最近在项目里需要对简历进行pdf导出,之前试用了一下iText 5,功能倒是没什么问题,但是XMLWorkerHelper对于CSS的支持程度是在是让我蛋疼,生成的pdf内容排版各种问题,遂不得不放弃iText 5,后来上iText官网发现有最新版iText 7.0.4,而且试用了官网上的示例程序,对css的支持也还可以,所以决定就用它了。

1.依赖jar包

<dependency>
    <groupId>org.freemarkergroupId>
    <artifactId>freemarkerartifactId>
    <version>2.3.26-incubatingversion>
dependency>
<dependency>
    <groupId>com.google.code.gsongroupId>
    <artifactId>gsonartifactId>
dependency>
<dependency>
    <groupId>com.itextpdfgroupId>
    <artifactId>itext7-coreartifactId>
    <version>7.0.4version>
dependency>


<dependency>
    <groupId>com.itextpdfgroupId>
    <artifactId>html2pdfartifactId>
    <version>1.0.1version>
dependency>


<dependency>
    <groupId>com.itextpdfgroupId>
    <artifactId>itext-licensekeyartifactId>
    <version>2.0.4version>
dependency>

由于html2pdf和itext-licensekey不是开源的,所以这两个包在maven中央仓库没有,这里需要添加iText maven仓库

<repositories>
  <repository>
    <id>itextid>
    <name>iText Repository - releasesname>
    <url>https://repo.itextsupport.com/releasesurl>
  repository>
repositories>

具体怎么添加请自行百度

2.功能开发

jar包下好了,我们就可以进行开发了,由于项目中使用的是freemarker作为前台模板,所以需要将ftl模板和数据生成html字符串

freemarker工具类

/**
 * Created by lc on 2017/8/23
 * FREEMARKER 模板工具类
 *
 */
public class FreeMarkerUtil {
    /**
     * @description 获取模板
     */
    public static String getContent(String fileName,Object data){
        String templatePath=getPDFTemplatePath(fileName).replace("\\","/");
        String templateFileName=getTemplateName(templatePath);
        String templateFilePath=getTemplatePath(templatePath);
        if(StringUtils.isEmpty(templatePath)){
            throw new FreeMarkerException("templatePath can not be empty!");
        }
        try{
            Configuration config = new Configuration(Configuration.VERSION_2_3_25);
            config.setDefaultEncoding("UTF-8");
            config.setDirectoryForTemplateLoading(new File(templateFilePath));
            config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
            config.setLogTemplateExceptions(false);
            Template template = config.getTemplate(templateFileName);
            StringWriter writer = new StringWriter();
            template.process(data, writer);
            writer.flush();
            String html = writer.toString();
            return html;
        }catch (Exception ex){
            throw new FreeMarkerException("FreeMarkerUtil process fail",ex);
        }
    }
    private static String getTemplatePath(String templatePath) {
        if(StringUtils.isEmpty(templatePath)){
            return "";
        }
        String path=templatePath.substring(0,templatePath.lastIndexOf("/"));
        return path;
    }
    private static String getTemplateName(String templatePath) {
        if(StringUtils.isEmpty(templatePath)){
            return "";
        }
        String fileName=templatePath.substring(templatePath.lastIndexOf("/")+1);
        return fileName;
    }
    /**
     * @description 获取PDF的模板路径,
     * 默认按照PDF文件名匹对应模板
     * @param fileName PDF文件名
     * @return         匹配到的模板名
     */
    public static String getPDFTemplatePath(String fileName){
        String  classpath=CreatePDF.class.getClassLoader().getResource("").getPath();
        String templatePath=classpath+"pdfHtml/resumePDF";
        File file=new File(templatePath);
        if(!file.isDirectory()){
            throw new PDFException("PDF模板文件不存在,请检查pdfHtml文件夹!");
        }
        String pdfFileName=fileName.substring(0,fileName.lastIndexOf("."));
        File defaultTemplate=null;
        File matchTemplate=null;
        for(File f:file.listFiles()){
            if(!f.isFile()){
                continue;
            }
            String templateName=f.getName();
            if(templateName.lastIndexOf(".ftl")==-1){
                continue;
            }
            if(defaultTemplate==null){
                defaultTemplate=f;
            }
            if(StringUtils.isEmpty(fileName)&&defaultTemplate!=null){
                break;
            }
            templateName=templateName.substring(0,templateName.lastIndexOf("."));
            if(templateName.toLowerCase().equals(pdfFileName.toLowerCase())){
                matchTemplate=f;
                break;
            }
        }
        if(matchTemplate!=null){
            return matchTemplate.getAbsolutePath();
        }
        if(defaultTemplate!=null){
            return defaultTemplate.getAbsolutePath();
        }
        return null;
    }
}

自定义异常类

public class FreeMarkerException extends BaseException {
    public FreeMarkerException(){
        super("FreeMarker异常");
    }

    public FreeMarkerException(int errorCode, String errorMsg){
        super(errorMsg);
        this.errorCode=errorCode;
        this.errorMsg=errorMsg;
    }
    public FreeMarkerException(String errorMsg){
        super(errorMsg);
        this.errorCode=500;
        this.errorMsg=errorMsg;
    }
    public FreeMarkerException(String errorMsg, Exception e){
        super(errorMsg,e);
        this.errorCode=500;
        this.errorMsg=errorMsg;
    }
}
public class PDFException extends BaseException {

    public PDFException(){
        super("PDF异常");
    }

    public PDFException(int errorCode, String errorMsg){
        super(errorMsg);
        this.errorCode=errorCode;
        this.errorMsg=errorMsg;
    }
    public PDFException(String errorMsg){
        super(errorMsg);
        this.errorCode=500;
        this.errorMsg=errorMsg;
    }
    public PDFException(String errorMsg, Exception e){
        super(errorMsg,e);
        this.errorCode=500;
        this.errorMsg=errorMsg;
    }
}

重点来了,CreatePDF类

public class CreatePDF {
    public static final String FONT = CreatePDF.class.getClassLoader().getResource("").getPath()+"pdfHTML/resumePDF/msyh.ttf";

    public void createPdf(String src, String resources, Object object,HttpServletResponse response) throws IOException {
        try {
            PdfFont msyh = PdfFontFactory.createFont(FONT, PdfEncodings.IDENTITY_H);

            WriterProperties writerProperties = new WriterProperties();
            //Add metadata
            writerProperties.addXmpMetadata();

            PdfWriter pdfWriter = new PdfWriter(response.getOutputStream(), writerProperties);

            PdfDocument pdfDoc = new PdfDocument(pdfWriter);
            pdfDoc.getCatalog().setLang(new PdfString("UTF-8"));
            //Set the document to be tagged
            pdfDoc.setTagged();
            pdfDoc.getCatalog().setViewerPreferences(new PdfViewerPreferences().setDisplayDocTitle(true));

            //Set meta tags
            PdfDocumentInfo pdfMetaData = pdfDoc.getDocumentInfo();
            pdfMetaData.setAuthor("XX");
            pdfMetaData.addCreationDate();
            pdfMetaData.getProducer();
            pdfMetaData.setCreator("XXX");
            pdfMetaData.setKeywords("resume");
            pdfMetaData.setSubject("PDF resume");
            //Title is derived from html

            //Create event-handlers
            String footer = "来自:XX网 - www.XXX.com";
            Footer footerHandler = new Footer(footer,msyh);
//            PageXofY footerHandler = new PageXofY(pdfDoc);

            //Assign event-handlers
            pdfDoc.addEventHandler(PdfDocumentEvent.END_PAGE,footerHandler);

            // pdf conversion
            ConverterProperties props = new ConverterProperties();
            FontProvider fp = new FontProvider();
            fp.addStandardPdfFonts();
            fp.addDirectory(resources);//The noto-nashk font file (.ttf extension) is placed in the resources

            props.setFontProvider(fp);
            props.setBaseUri(resources);
            //Setup custom tagworker factory for better tagging of headers
            //DefaultTagWorkerFactory tagWorkerFactory = new TagWorkerFactory();
            //props.setTagWorkerFactory(tagWorkerFactory);

            HtmlConverter.convertToPdf(new ByteArrayInputStream(FreeMarkerUtil.getContent(src,object).getBytes("UTF-8")), pdfDoc, props);
            pdfDoc.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //Header event handler
    protected class Header implements IEventHandler {
        String header;
        public Header(String header) {
            this.header = header;
        }
        @Override
        public void handleEvent(Event event) {
            //Retrieve document and
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            Rectangle pageSize = page.getPageSize();
            PdfCanvas pdfCanvas = new PdfCanvas(
                    page.getLastContentStream(), page.getResources(), pdf);
            Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
            canvas.setFontSize(18f);
            //Write text at position
            canvas.showTextAligned(header,
                    pageSize.getWidth() / 2,
                    pageSize.getTop() - 30, TextAlignment.CENTER);
        }
    }
    //Header event handler
    protected class Footer implements IEventHandler {
        String footer;
        PdfFont pdfFont;
        public Footer(String footer,PdfFont pdfFont) {
            this.footer = footer;
            this.pdfFont = pdfFont;
        }
        @Override
        public void handleEvent(Event event) {
            //Retrieve document and
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            Rectangle pageSize = page.getPageSize();
            PdfCanvas pdfCanvas = new PdfCanvas(
                    page.getLastContentStream(), page.getResources(), pdf);
            Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
            canvas.setFontSize(8f);
            canvas.setFont(pdfFont);
            //Write text at position
            canvas.showTextAligned(footer,
                    pageSize.getWidth() / 2,
                    pageSize.getBottom() + 30, TextAlignment.CENTER);
        }
    }

    //page X of Y
    protected class PageXofY implements IEventHandler {
        protected PdfFormXObject placeholder;
        protected float side = 20;
        protected float x = 300;
        protected float y = 25;
        protected float space = 4.5f;
        protected float descent = 3;
        public PageXofY(PdfDocument pdf) {
            placeholder =
                    new PdfFormXObject(new Rectangle(0, 0, side, side));
        }
        @Override
        public void handleEvent(Event event) {
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            int pageNumber = pdf.getPageNumber(page);
            Rectangle pageSize = page.getPageSize();
            PdfCanvas pdfCanvas = new PdfCanvas(
                    page.getLastContentStream(), page.getResources(), pdf);
            Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
            Paragraph p = new Paragraph()
                    .add("Page ").add(String.valueOf(pageNumber)).add(" of");
            canvas.showTextAligned(p, x, y, TextAlignment.RIGHT);
            pdfCanvas.addXObject(placeholder, x + space, y - descent);
            pdfCanvas.release();
        }
        public void writeTotal(PdfDocument pdf) {
            Canvas canvas = new Canvas(placeholder, pdf);
            canvas.showTextAligned(String.valueOf(pdf.getNumberOfPages()),
                    0, descent, TextAlignment.LEFT);
        }
    }
}

当然,我这里是直接获取response的outputstream,你也可以自己创建输出流下载到本地

然后是业务调用代码

public static final String sourceFolder = FilePathUtil.getRealFilePath(CreatePDF.class.getClassLoader().getResource("").getPath()+"pdfHTML/resumePDF/");
    //License key path
    public static final String LICENSE = CreatePDF.class.getClassLoader().getResource("").getPath()+"pdfHTML/itextkey-0.xml";

@RequestMapping("downloadPDF")
    public void downloadPDF(Long id,HttpServletResponse response){
        try{
            //...这里是业务调用逻辑
            Map map=new HashMap<>();
            map.put("demo","demo");

            String fileName = "demo.pdf";

            response.setContentType("application/force-download");// 设置强制下载不打开
            response.setHeader("Content-Disposition", "attachment; filename="+fileName);
            LicenseKey.loadLicenseFile(LICENSE);//加载iText License
            String htmlSource = "resume.ftl";
            String resourceFolder = sourceFolder;

            new CreatePDF().createPdf(htmlSource,resourceFolder,map,response);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

以上就是实现将ftl模板载入数据后拼装成的html生成pdf

3.使用问题

下面说说遇到的几个问题

1)字体问题

在咱这儿使用iText最常见的问题就是中文字体问题了,在iText5里面处理起来还挺麻烦,但是到了iText7,字体问题变得很好解决。
使用字体分以下两种情况
①在html中使用字体

// pdf conversion
ConverterProperties props = new ConverterProperties();
FontProvider fp = new FontProvider();
fp.addStandardPdfFonts();
fp.addDirectory(resources);

props.setFontProvider(fp);
props.setBaseUri(resources);

这里要注意,BaseUri是你存放生成pdf需要使用的资源文件的基础路径,比如css、字体和图片,而这个路径,在windows环境下绝对不能有前面的斜杠,因为
class.getClassLoader().getResource(“”).getPath()
获取到的路径默认前面是带斜杠的,比如/E:/test,而BaseUri只支持E:/test

②在页眉、页脚或自定义元素中使用字体
首先定义字体

PdfFont msyh = PdfFontFactory.createFont("font/msyh.ttf", PdfEncodings.IDENTITY_H);

然后很多地方都可以直接setFont

PdfCanvas pdfCanvas = new PdfCanvas(page.getLastContentStream(), page.getResources(), pdf);
Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
canvas.setFontSize(8f);
canvas.setFont(msyh);

2)base64图片识别问题

这个问题在iText5中需要单独处理,但是在iText7中不需要处理,这也是我找了半天实在没有解决办法的情况下直接把base64丢进去之后才发现的~~

3)css问题

因dead line所限,我没有做更多的测试,目前就我发现无法使用的css有以下2种:
overflow、border-radio

如果是想要将image调整为圆形的话,可以使用以下方法:
①做一个内容为圆形,剩余部分透明的图片~~
②通过剪裁,将图片切成圆形,代码:

ImageData img = ImageDataFactory.create(imgSrc);
Image imgModel = new Image(img);
float w = imgModel.getImageScaledWidth();
float h = imgModel.getImageScaledHeight();
PdfFormXObject xObject = new PdfFormXObject(new Rectangle(850, 600));
PdfCanvas xObjectCanvas = new PdfCanvas(xObject, pdfDoc);
xObjectCanvas.ellipse(0, 0, 850, 600, 5);
xObjectCanvas.clip();
xObjectCanvas.newPath();
xObjectCanvas.addImage(img, w, 0, 0, h, 0, -600);
com.itextpdf.layout.element.Image clipped = new com.itextpdf.layout.element.Image(xObject);
clipped.scale(0.5f, 0.5f);

原文详见
http://developers.itextpdf.com/content/best-itext-questions-stackoverview/image-examples/itext7-how-give-image-rounded-corners

这需要将html转换为element列表,然后再对相应的element进行操作,然后再用List生成pdf。
还是dead line的原因,我没有使用这个方法,因为我没找到该怎么用List生成pdf,我直接把这部分图片给去掉了。

4)html标签

我有一段html代码是这样的

<ul>
    <li>测试1<br>测试2li>
ul>

生成pdf时报错

ERROR com.itextpdf.html2pdf.attach.impl.DefaultHtmlProcessor - Worker of type com.itextpdf.html2pdf.attach.impl.tags.LiTagWorker unable to process com.itextpdf.html2pdf.attach.impl.tags.BrTagWorker

虽然会报这样的错误,但是pdf有的时候还是能生成的,这个问题困扰了我将近1个小时,各种google也找不到这个报错的相关信息,然后突然间就我想了一下,为什么我要把它当成英文报错,而不是中文报错。

字面意思,就是LiTagWorker不能处理BrTagWorker,对于tagWorker的处理机制我不太了解,所以从我的理解来看,就是
标签不能放在

  • 标签中。

    这简单~把html改改就是了~

    这个问题引发我的反思,像我这种英文不好的开发人员,遇到问题的第一反应不应该是google,而是先把错误信息翻译出来再说

    tips:

    html2pdf 是属于收费功能,如果要使用html2pdf,需要在iText官网联系客服购买license(话说我昨天发的邮件这会儿还没人回我,看来我注定只能使用试用版了吗?)

    使用license代码

    LicenseKey.loadLicenseFile("pdfHTML/itextkey-0.xml");

    如果没有这段代码或者你的license无效,生成pdf会报错

    com.itextpdf.licensekey.LicenseKeyException: Signature was corrupted.

    pdf最终也会下载下来,但是打不开

    也有试用版,在iText官网,申请试用
    http://pages.itextpdf.com/iText-7-Free-Trial-Landing-Page-1.html

    填写信息后会发邮件到你的邮箱,给你一个账户名密码,登陆网站下载试用的license,这个license有30天的试用期。

    另外,我试了一下,多个邮箱可以申请多个license重复使用

    当然要是有经济能力的话最好还是支持一下购买license

    以上就是昨天使用iText7的总结,如果有写的不好的地方请大家指出,我好改正

    重点更新:

    今天使用功能的时候又发现问题,当纯html长度超出5000的时候,pdf死活导不出来,并且没有报错,然后只能打着断点一步一步的跟,发现执行到中间某一块的时候,代码就停了,而且后续代码并不执行,也没有异常抛出,具体代码位置

    com.itextpdf.html2pdf.attach.impl.DefaultHtmlProcessor

    代码段

    PageBreakApplierUtil.addPageBreakElementBefore(this.context, this.context.getState().top(), element, tagWorker);
    boolean childProcessed = this.context.getState().top().processTagChild(tagWorker, this.context);
    PageBreakApplierUtil.addPageBreakElementAfter(this.context, this.context.getState().top(), element, tagWorker);

    当功能扫描到某一个html节点树的时候,执行到第二句

    this.context.getState().top().processTagChild(tagWorker, this.context);

    就停了,一开始我以为还是br标签的问题,结果发现去掉了br标签还是不行,可是各种资料都找不到相关信息,只能靠蒙了

    检查css,发现有一个样式信息可以去掉,对整体样式没什么影响

    display: inline-block;

    去掉后再跑一次,发现果然没问题了,再增加一倍html内容也正常执行。
    唉,itext7资料是在太少,特别是中文资料,这次能解决也算是缘分吧

  • 你可能感兴趣的