天天看点

itext操作PDF初探

PDF是一种常见的文档格式,因为能保证跨平台印刷质量而受欢迎,很多正式的文档都会以PDF格式发布。当然,也许编辑的时候是使用MS word,WPS等工具。

网上流传的大部分资料都是关于抽取PDF文档中的纯文本,这样就会丢掉大部分的样式信息,导致显示时难以复原。下图是百度搜索PDF高亮效果。

itext操作PDF初探

这样的效果显然难以令人满意。

那么为什么PDF格式解析这么困难呢?先前我们尝试解析过word,发现其实是由许多个xml文件与资源文件组成的,word的正文本体其实是xml与html非常相似,学习过前端技术的同学应该能很快理解。但是用notepad++打开pdf你会发现,pdf的正文是二进制流,并不构成这样结构。好在itext中有一个RenderListener可以监听这个二进制流渲染时的情况。在word中,我们存在paragraph,run,table,row,cell这样的概念,可以帮助我们对文档内容结构化,但是pdf中并不存在。pdf中存在的是“最小渲染单元”可以是一个或一段文字,也可能是一张图片,除此以外,这个单元还有相对于页面的绝对定位坐标,换而言之,可以理解为一张矢量图(毕竟PDF是由postscript发展而来的)。用前端的话来说,就是文档是由很多绝对定位的div组成的,如果接手这样的网站,前端同学估计估计要疯掉了。

itext操作PDF初探

上面说道最小渲染单元可能包含一个或多个字符,显然这样对我们识别是不利的,因为中文、英文、标点、数字的宽度各不相同,要精确计算字符位置非常困难,好在itext提供了一个拆分功能,可以将包含多个字符的单元拆分为一个字符一个单元,这样确保了单元的一致性。但是itext也仅仅能做到这样了。

因为我们要做搜索,所以接下去我们要做的就是把这些最小渲染单元分组排序,变成一块一块“段落”。那么,段落都有哪些特征呢?首先有两个很明显的特征:1.属于一个段落的文字,字号必然相同,除非编辑PDF的人有特殊癖好。2.属于一个段落的文字,每一行的y坐标必然是对齐的。

但这两个都是必要条件,比如很多文档是两列排版的,很可能符合上面的两个特征,但是换行的却是错位的

itext操作PDF初探

基于上述问题,我们进行如下的改进。

1.将y坐标相同,字号相同的字符放在一起,并按照x坐标从小到大排序

2.从第一个字符开始计算前后字符的x距离的差值,如果大于2倍字号,则视为断开,这样我们就把字符,拼成了一个个“行”,行的第一个字符的x坐标为行的开始,最后一个字符的x坐标为行的结束

3.大段文本一般是左对齐的,将那些x开始坐标相差不到一个字号的行合并在一起,而一般的文本是1.5倍行间距,如果两个“行”的y距离大于3倍字号,则视为断开

这样,段落由字的数组组成,只要纯文本中我们匹配到了关键词,我们很快就能根据数组下标找到对应的字,以及它的字体、字号、颜色、坐标等信息。

接下去就是高亮的部分,虽然已经知道原理,由于笔者仍未实践,故暂且保留。

参考代码:

import com.alibaba.fastjson.JSON;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.timerchina.bean.RenderUnit;

import java.util.ArrayList;
import java.util.List;

public class ContentExtractListener implements RenderListener {

    private List<RenderUnit> renderUnits = new ArrayList<>();

    public List<RenderUnit> getRenderUnits() {
        return renderUnits;
    }

    @Override
    public void beginTextBlock() {

    }

    @Override
    public void endTextBlock() {

    }

    @Override
    public void renderText(TextRenderInfo textRenderInfo) {
        List<TextRenderInfo> characterRenderInfos = textRenderInfo.getCharacterRenderInfos();
        for (TextRenderInfo characterRenderInfo : characterRenderInfos) {
            System.out.println(characterRenderInfo.getText());
            System.out.println(JSON.toJSONString(characterRenderInfo.getFont().getFullFontName()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getFont().getPostscriptFontName()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getFont().getAllNameEntries()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getFont().getFamilyFontName()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getFont().getFontDictionary()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getFont().getFontMatrix()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getFillColor()));
            System.out.println(JSON.toJSONString(characterRenderInfo.getStrokeColor()));
            System.out.println("----------------------------");
            RenderUnit renderUnit = new RenderUnit();
            renderUnit.setText(characterRenderInfo.getText());
            renderUnit.setMinX(characterRenderInfo.getBaseline().getBoundingRectange().getMinX());
            renderUnit.setMaxX(characterRenderInfo.getBaseline().getBoundingRectange().getMaxX());
            renderUnit.setMinY(characterRenderInfo.getDescentLine().getBoundingRectange().getY());
            renderUnit.setMaxY(characterRenderInfo.getAscentLine().getBoundingRectange().getY());
            System.out.println(renderUnit);
            renderUnits.add(renderUnit);
        }
    }

    @Override
    public void renderImage(ImageRenderInfo imageRenderInfo) {

    }
}
           
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.timerchina.bean.PDFPage;
import com.timerchina.bean.RenderUnit;
import com.timerchina.listener.ContentExtractListener;

import java.util.ArrayList;
import java.util.List;

public class PDFReader {
    
    public void read(){
        try {
            List<PDFPage> pdfPages=new ArrayList<>();
            PdfReader reader = new PdfReader("D:\\test.pdf");
            PdfReaderContentParser parser = new PdfReaderContentParser(reader);
            int pageCount = reader.getNumberOfPages();
            for (int i = 1; i <= pageCount; i++) {
                ContentExtractListener listener = new ContentExtractListener();
                parser.processContent(i, listener);
                List<RenderUnit> renderUnitList = listener.getRenderUnits();   
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           
public class RenderUnit {

    private String text;
    private Double minX;
    private Double maxX;
    private Double minY;
    private Double maxY;

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public Double getMinX() {
        return minX;
    }

    public void setMinX(Double minX) {
        this.minX = minX;
    }

    public Double getMaxX() {
        return maxX;
    }

    public void setMaxX(Double maxX) {
        this.maxX = maxX;
    }

    public Double getMinY() {
        return minY;
    }

    public void setMinY(Double minY) {
        this.minY = minY;
    }

    public Double getMaxY() {
        return maxY;
    }

    public void setMaxY(Double maxY) {
        this.maxY = maxY;
    }

    @Override
    public String toString() {
        return "RenderUnit{" +
                "text='" + text + '\'' +
                ", minX=" + minX +
                ", maxX=" + maxX +
                ", minY=" + minY +
                ", maxY=" + maxY +
                '}';
    }
}