PDF是一种常见的文档格式,因为能保证跨平台印刷质量而受欢迎,很多正式的文档都会以PDF格式发布。当然,也许编辑的时候是使用MS word,WPS等工具。
网上流传的大部分资料都是关于抽取PDF文档中的纯文本,这样就会丢掉大部分的样式信息,导致显示时难以复原。下图是百度搜索PDF高亮效果。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLsp1MiNXNXRmeWdEZoplMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zROBlLwYDO2EDNwQTM5ITOwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
这样的效果显然难以令人满意。
那么为什么PDF格式解析这么困难呢?先前我们尝试解析过word,发现其实是由许多个xml文件与资源文件组成的,word的正文本体其实是xml与html非常相似,学习过前端技术的同学应该能很快理解。但是用notepad++打开pdf你会发现,pdf的正文是二进制流,并不构成这样结构。好在itext中有一个RenderListener可以监听这个二进制流渲染时的情况。在word中,我们存在paragraph,run,table,row,cell这样的概念,可以帮助我们对文档内容结构化,但是pdf中并不存在。pdf中存在的是“最小渲染单元”可以是一个或一段文字,也可能是一张图片,除此以外,这个单元还有相对于页面的绝对定位坐标,换而言之,可以理解为一张矢量图(毕竟PDF是由postscript发展而来的)。用前端的话来说,就是文档是由很多绝对定位的div组成的,如果接手这样的网站,前端同学估计估计要疯掉了。
上面说道最小渲染单元可能包含一个或多个字符,显然这样对我们识别是不利的,因为中文、英文、标点、数字的宽度各不相同,要精确计算字符位置非常困难,好在itext提供了一个拆分功能,可以将包含多个字符的单元拆分为一个字符一个单元,这样确保了单元的一致性。但是itext也仅仅能做到这样了。
因为我们要做搜索,所以接下去我们要做的就是把这些最小渲染单元分组排序,变成一块一块“段落”。那么,段落都有哪些特征呢?首先有两个很明显的特征:1.属于一个段落的文字,字号必然相同,除非编辑PDF的人有特殊癖好。2.属于一个段落的文字,每一行的y坐标必然是对齐的。
但这两个都是必要条件,比如很多文档是两列排版的,很可能符合上面的两个特征,但是换行的却是错位的
基于上述问题,我们进行如下的改进。
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 +
'}';
}
}