使用PDFBox提取PDF时,是否可以保留文本格式?
我有一个解析PDF文档以获取信息的程序。当发布新版本的PDF时,作者使用粗体或斜体文本表示新的信息,并使用删除线或下划线删除所指示的省略文本。在PDFbox中使用基本的Stripper类会返回所有文本,但是格式会被删除,因此我无法判断文本是新的还是省略的。我目前正在使用以下项目示例代码:
Dim doc As PDDocument = Nothing
Try
doc = PDDocument.load(RFPFilePath)
Dim stripper As New PDFTextStripper()
stripper.setAddMoreFormatting(True)
stripper.setSortByPosition(True)
rtxt_DocumentViewer.Text = stripper.getText(doc)
Finally
If doc IsNot Nothing Then
doc.close()
End If
End Try
Run Code Online (Sandbox Code Playgroud)
如果我简单地将PDF文本复制并粘贴到保留格式的richtextbox中,我的解析代码就可以正常工作。我当时想通过打开PDF,全选,复制,关闭文档,然后将其粘贴到我的richtextbox中,以编程方式进行此操作,但这似乎很笨拙。
正如OP在评论中提到的Java示例可以做到的那样,而我还只将PDFBox与Java一起使用,因此此答案具有Java示例。此外,仅使用PDFBox版本1.8.11开发和测试此示例。
正如评论中已经提到的那样,
OP的示例文档中的粗体和斜体效果是通过使用不同的字体(包含字母的粗体或斜体版本)绘制文本来生成的。样本文档中的下划线和删除线效果是通过在/下方通过文本线绘制一个矩形而产生的,该矩形具有文本线的宽度和很小的高度。因此,要提取这些信息,必须将扩展为
PDFTextStripper以某种方式对字体更改和附近文本的矩形做出反应。
这是一个示例类,其扩展PDFTextStripper如下:
public class PDFStyledTextStripper extends PDFTextStripper
{
public PDFStyledTextStripper() throws IOException
{
super();
registerOperatorProcessor("re", new AppendRectangleToPath());
}
@Override
protected void writeString(String text, List<TextPosition> textPositions) throws IOException
{
for (TextPosition textPosition : textPositions)
{
Set<String> style = determineStyle(textPosition);
if (!style.equals(currentStyle))
{
output.write(style.toString());
currentStyle = style;
}
output.write(textPosition.getCharacter());
}
}
Set<String> determineStyle(TextPosition textPosition)
{
Set<String> result = new HashSet<>();
if (textPosition.getFont().getBaseFont().toLowerCase().contains("bold"))
result.add("Bold");
if (textPosition.getFont().getBaseFont().toLowerCase().contains("italic"))
result.add("Italic");
if (rectangles.stream().anyMatch(r -> r.underlines(textPosition)))
result.add("Underline");
if (rectangles.stream().anyMatch(r -> r.strikesThrough(textPosition)))
result.add("StrikeThrough");
return result;
}
class AppendRectangleToPath extends OperatorProcessor
{
public void process(PDFOperator operator, List<COSBase> arguments)
{
COSNumber x = (COSNumber) arguments.get(0);
COSNumber y = (COSNumber) arguments.get(1);
COSNumber w = (COSNumber) arguments.get(2);
COSNumber h = (COSNumber) arguments.get(3);
double x1 = x.doubleValue();
double y1 = y.doubleValue();
// create a pair of coordinates for the transformation
double x2 = w.doubleValue() + x1;
double y2 = h.doubleValue() + y1;
Point2D p0 = transformedPoint(x1, y1);
Point2D p1 = transformedPoint(x2, y1);
Point2D p2 = transformedPoint(x2, y2);
Point2D p3 = transformedPoint(x1, y2);
rectangles.add(new TransformedRectangle(p0, p1, p2, p3));
}
Point2D.Double transformedPoint(double x, double y)
{
double[] position = {x,y};
getGraphicsState().getCurrentTransformationMatrix().createAffineTransform().transform(
position, 0, position, 0, 1);
return new Point2D.Double(position[0],position[1]);
}
}
static class TransformedRectangle
{
public TransformedRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3)
{
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}
boolean strikesThrough(TextPosition textPosition)
{
Matrix matrix = textPosition.getTextPos();
// TODO: This is a very simplistic implementation only working for horizontal text without page rotation
// and horizontal rectangular strikeThroughs with p0 at the left bottom and p2 at the right top
// Check if rectangle horizontally matches (at least) the text
if (p0.getX() > matrix.getXPosition() || p2.getX() < matrix.getXPosition() + textPosition.getWidth() - textPosition.getFontSizeInPt() / 10.0)
return false;
// Check whether rectangle vertically is at the right height to underline
double vertDiff = p0.getY() - matrix.getYPosition();
if (vertDiff < 0 || vertDiff > textPosition.getFont().getFontDescriptor().getAscent() * textPosition.getFontSizeInPt() / 1000.0)
return false;
// Check whether rectangle is small enough to be a line
return Math.abs(p2.getY() - p0.getY()) < 2;
}
boolean underlines(TextPosition textPosition)
{
Matrix matrix = textPosition.getTextPos();
// TODO: This is a very simplistic implementation only working for horizontal text without page rotation
// and horizontal rectangular underlines with p0 at the left bottom and p2 at the right top
// Check if rectangle horizontally matches (at least) the text
if (p0.getX() > matrix.getXPosition() || p2.getX() < matrix.getXPosition() + textPosition.getWidth() - textPosition.getFontSizeInPt() / 10.0)
return false;
// Check whether rectangle vertically is at the right height to underline
double vertDiff = p0.getY() - matrix.getYPosition();
if (vertDiff > 0 || vertDiff < textPosition.getFont().getFontDescriptor().getDescent() * textPosition.getFontSizeInPt() / 500.0)
return false;
// Check whether rectangle is small enough to be a line
return Math.abs(p2.getY() - p0.getY()) < 2;
}
final Point2D p0, p1, p2, p3;
}
final List<TransformedRectangle> rectangles = new ArrayList<>();
Set<String> currentStyle = Collections.singleton("Undefined");
}
Run Code Online (Sandbox Code Playgroud)
除了做什么PDFTextStripper,这个课也
AppendRectangleToPath,determineStyle,和writeString。当心:这仅仅是概念的证明!特别是
使用PDFStyledTextStripper像这样
String extractStyled(PDDocument document) throws IOException
{
PDFTextStripper stripper = new PDFStyledTextStripper();
stripper.setSortByPosition(true);
return stripper.getText(document);
}
Run Code Online (Sandbox Code Playgroud)
(来自ExtractText.java,从测试方法调用testExtractStyledFromExampleDocument)
一个得到结果
[]This is an example of plain text
[Bold]This is an example of bold text
[]
[Underline]This is an example of underlined text[]
[Italic]This is an example of italic text
[]
[StrikeThrough]This is an example of strike through text[]
[Italic, Bold]This is an example of bold, italic text
Run Code Online (Sandbox Code Playgroud)
OP的样本文件
PSPDFStyledTextStripper同时,其代码已稍作更改,以也适用于github问题中共享的示例文档,尤其是其内部类的代码TransformedRectanglecf。在这里。