如何使用Apache PDFBox按原顺序提取PDF中的文本与图片?
当然可以!借助Apache PDFBox获取的文本位置信息,完全能实现按文档原有顺序同时提取文本和图片。我来给你梳理具体的思路和实操步骤:
核心思路
PDF文档里的所有内容(文本、图片、图形等)都是按照绘制顺序存储在页面的内容流中的。这意味着,如果你能遍历页面的内容流并捕获每个元素的绘制动作,就能直接按原文档顺序收集文本和图片。如果已经分开提取了文本和图片,也可以通过它们的页面坐标来排序合并——不过遍历内容流的方式会更准确,因为它能直接还原PDF渲染时的原始顺序,避免坐标排序可能出现的层级或重叠问题。
具体实现步骤
捕获带位置的文本与图片
继承PDFBox的PDFTextStripper类,重写两个关键方法:writeString:捕获每个文本块的内容和坐标位置,封装成文本对象。processOperator:监听内容流中的Do操作符(用于绘制图片/XObject),提取图片对象并获取其绘制位置(通过当前图形状态的变换矩阵),封装成图片对象。
按顺序处理内容
遍历收集到的所有文本和图片对象,它们的顺序就是PDF文档中的原始绘制顺序,直接按此顺序输出或保存即可。如果是分开提取的场景,可以按页面号→y坐标(PDF的y轴从下往上,值越大位置越靠上)→x坐标的优先级排序,也能还原大致顺序。
代码示例
下面是一个自定义的内容提取器,能同时按顺序捕获文本和图片:
import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.graphics.PDXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; import org.apache.pdfbox.util.Matrix; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import javax.imageio.ImageIO; public class OrderedContentExtractor extends PDFTextStripper { private final List<ContentItem> contentItems = new ArrayList<>(); public OrderedContentExtractor() throws IOException { super(); } // 捕获文本块及其位置 @Override protected void writeString(String text, List<TextPosition> textPositions) throws IOException { TextPosition firstPos = textPositions.get(0); contentItems.add(new TextContent(text, firstPos.getPageNumber(), firstPos.getX(), firstPos.getY())); super.writeString(text, textPositions); } // 捕获图片绘制操作 @Override protected void processOperator(Operator operator, List<COSBase> operands) throws IOException { String opName = operator.getName(); if ("Do".equals(opName)) { COSName xObjName = (COSName) operands.get(0); PDResources resources = getCurrentPage().getResources(); PDXObject xObject = resources.getXObject(xObjName); if (xObject instanceof PDImageXObject) { PDImageXObject image = (PDImageXObject) xObject; Matrix transformMatrix = getGraphicsState().getCurrentTransformationMatrix(); float imgX = transformMatrix.getTranslateX(); float imgY = transformMatrix.getTranslateY(); contentItems.add(new ImageContent(image, getCurrentPage().getNumber(), imgX, imgY)); } } super.processOperator(operator, operands); } public List<ContentItem> getOrderedContent() { return contentItems; } // 抽象内容项基类 abstract static class ContentItem { final int pageNumber; final float x; final float y; ContentItem(int pageNumber, float x, float y) { this.pageNumber = pageNumber; this.x = x; this.y = y; } } // 文本内容项 static class TextContent extends ContentItem { final String text; TextContent(String text, int pageNumber, float x, float y) { super(pageNumber, x, y); this.text = text; } } // 图片内容项 static class ImageContent extends ContentItem { final PDImageXObject image; ImageContent(PDImageXObject image, int pageNumber, float x, float y) { super(pageNumber, x, y); this.image = image; } } // 测试使用 public static void main(String[] args) { try (PDDocument doc = PDDocument.load(new File("your-document.pdf"))) { OrderedContentExtractor extractor = new OrderedContentExtractor(); extractor.setSortByPosition(true); extractor.getText(doc); List<ContentItem> content = extractor.getOrderedContent(); for (ContentItem item : content) { if (item instanceof TextContent) { System.out.println("提取文本:" + ((TextContent) item).text); } else if (item instanceof ImageContent) { PDImageXObject img = ((ImageContent) item).image; String imgPath = "extracted-image-" + UUID.randomUUID() + "." + img.getSuffix(); ImageIO.write(img.getImage(), img.getSuffix(), new File(imgPath)); System.out.println("提取图片:" + imgPath); } } } catch (IOException e) { e.printStackTrace(); } } }
注意事项
- 变换矩阵处理:有些PDF会对内容进行旋转、缩放等变换,需要通过
getCurrentTransformationMatrix获取准确的绘制位置,避免坐标偏移。 - 文本块合并:
writeString可能会把连续的文本拆分成多个调用,你可以根据相邻文本块的坐标和间距,手动合并成完整的段落。 - 层级问题:如果图片被文本覆盖,绘制顺序是先画图片再画文本,遍历内容流的方式会正确捕获这个顺序;而单纯按坐标排序可能会因为y值的问题搞反顺序,所以优先用内容流遍历的方案。
内容的提问来源于stack exchange,提问作者Amine




