从pdf中提取文本信息

Text info extraction from pdf

如何提取文本位置坐标、宽高等文本信息e.t.c.,?? 我用 'Pdf clown' 库尝试了这个,它对普通文本工作得很好,但是,对于 rotated text (90/-90degrees)它输出 width/height 作为 0(零)。

并且缩放因子 (scaleX, scaleY) 对于(90/-90 deg) 的文本分别显示为(0, 0),对于倒置文本(旋转 180 度)是 (-1, -1).

我想要旋转文本的信息以突出显示它们(因为宽度值为零,我无法突出显示它们)。请帮我。我在 .NET 环境中工作。

我正在使用的文件: https://nofile.io/f/Kvf2DkXvfj4/edit9.pdf

代码: 使用来自 pdfclown 示例的 TextInfoExtractionSample.cs

输出 (对于上面文件中文本的三种不同对齐方式)

文本 [x:283,y:104,w:126,h:-23] [字体大小:-24, 字体样式: ArialMT]: inverted_text

文本 [x:265,y:244,w:0,h:121] [字体 size:0,字体样式:ArialMT]:vertical_text

文本 [x:347,y:131,w:0,h:167] [字体 size:0,字体样式:ArialMT]:vertical_minus90

因为我对 Java 比 .Net 更熟悉,所以我分析了这个问题并在 PDF Clown / Java 中创建了第一个解决方法;我稍后会尝试将其移植到 .Net。不过,自己做应该不会太难。

问题

您提供的示例文件通过 PDF Clown TextInfoExtractionSample.

运行 时非常清楚地说明了问题

edit9.pdf的截图:

应用TextInfoExtractionSampleedit9.pdf的截图:

竖排文字

一切正常。

颠倒文字

单个字符框(绿色)看起来不错,但整个字符串的框 "inverted_text"(黑色虚线)不包括最外面的字符。

竖排文字

单个字符框缩小为 0x0 矩形(在屏幕截图中不可见,但在内容流分析中很明显)。整个字符串的方框在字符串的 基线 上减少为一条线(黑色虚线),缺少一点长度。

中间有角度的文本

字符框是直立的,与页面边框平行,其基线段在框内。但是,由于文本是倾斜的,因此字符的上部和下部部分位于各自的字符框之外,而相邻字符则部分位于字符框内。

整个字符串的框也与页面平行。

简而言之

文本字符和字符串框仅适用于直立文本。

在来源中

这与在源代码中找到的内容相符:

  • The Java Rectangle2D and .Net RectangleF classes 用于字符框的设计用于平行于坐标的矩形系统轴,并在 PDF Clown 中以这种方式使用。因此,它们不能正确表示任意角度的字符宽度和高度。

  • PDF 小丑 classes 不包含 Angle 属性来表示角色的旋转。

  • 字符框维度的计算只考虑聚合变换矩阵主对角线上的值,即ScaleXScaleY,而忽略ShearXShearY。但是,对于非直立或倒置的文本,ShearXShearY 很重要,对于垂直文本,ScaleXScaleY 是 0.

  • 从基线(本机 PDF 文本定位方式)到字符顶部(PDF 小丑文本定位)的过渡仅通过更改 y 坐标完成,因此仅适用于竖直和颠倒的文本。

解决方法

要真正解决此问题,需要对字符框和字符串框使用完全不同的 class,即以任意角度对矩形建模的 class。

不过,一个更快的解决方法是将 angle 成员添加到 TextChar class 和 ITextString 以及实现中,然后考虑处理盒子时的那个角度。此解决方法已在此处实施。

如上所述,解决方法首先在 Java 中实施。

在Java

首先我们向 TextChar 添加一个角度成员,在 ShowText 操作 class 中计算框尺寸和角度的正确值,并在 [=42] 中正确设置这些值=].

然后我们向 TextStringWrapper(通常是 ITextString)添加一个角度 getter,其中 returns 是字符串第一个文本字符的角度。并且我们改进了 TextStringWrapper 方法 getBox 以在确定字符串框时考虑文本字符的角度。

最后,我们将扩展 TextInfoExtractionSample 以在绘制框时考虑角度值。

我将那个角成员命名为 Alpha 就像我在我的草图中命名那个角 α 一样。事后看来 Theta 或简单地 Angle 会更合适。

文本字符

新增成员变量alpha

  private final double alpha;

一个新的和改变的构造函数

  // <constructors>
  public TextChar(
    char value,
    Rectangle2D box,
    TextStyle style,
    boolean virtual
    )
  {
      this(value, box, 0, style, virtual);
  }

  public TextChar(
    char value,
    Rectangle2D box,
    double alpha,
    TextStyle style,
    boolean virtual
    )
  {
    this.value = value;
    this.box = box;
    this.alpha = alpha;
    this.style = style;
    this.virtual = virtual;
  }
  // </constructors>

A getter 角度

  public double getAlpha() {
      return alpha;
  }

(TextChar.java)

显示文本

更新内部接口[=52​​=]方法scanChar传输角度

void scanChar(
  char textChar,
  Rectangle2D textCharBox,
  double alpha
  );

(ShowText.java内接口[=52​​=])

更新 scan 方法以正确计算矩形尺寸和角度并将它们转发给 IScanner 实现

[...]
for(char textChar : textString.toCharArray())
{
  double charWidth = font.getWidth(textChar) * scaledFactor;

  if(textScanner != null)
  {
    /*
      NOTE: The text rendering matrix is recomputed before each glyph is painted
      during a text-showing operation.
    */
    AffineTransform trm = (AffineTransform)ctm.clone(); trm.concatenate(tm);
    double charHeight = font.getHeight(textChar,fontSize);

    // vvv--- changed
    double ascent = font.getAscent(fontSize);
    double x = trm.getTranslateX() + ascent * trm.getShearX();
    double y = contextHeight - trm.getTranslateY() - ascent * trm.getScaleY();
    double dx = charWidth * trm.getScaleX();
    double dy = charWidth * trm.getShearY();
    double alpha = Math.atan2(dy, dx);
    double w = Math.sqrt(dx*dx + dy*dy);
    dx = charHeight * trm.getShearX();
    dy = charHeight * trm.getScaleY();
    double h = Math.sqrt(dx*dx + dy*dy);
    Rectangle2D charBox = new Rectangle2D.Double(x, y, w, h);

    textScanner.scanChar(textChar,charBox, alpha);
    // ^^^--- changed
  }

  /*
    NOTE: After the glyph is painted, the text matrix is updated
    according to the glyph displacement and any applicable spacing parameter.
  */
  tm.translate(charWidth + charSpace + (textChar == ' ' ? wordSpace : 0), 0);
}
[...]

(ShowText.java)

ContentScanner 内部 class TextStringWrapper

更新 TextStringWrapper 构造函数 ShowText.IScanner 回调以接受角度参数并将其用于构造 TextChar

getBaseDataObject().scan(
  state,
  new ShowText.IScanner()
  {
    @Override
    public void scanChar(
      char textChar,
      Rectangle2D textCharBox,
      double alpha
      )
    {
      textChars.add(
        new TextChar(
          textChar,
          textCharBox,
          alpha,
          style,
          false
          )
        );
    }
  }
  );

A getter 角度

public double getAlpha() {
    return textChars.isEmpty() ? 0 : textChars.get(0).getAlpha();
}

考虑角度的getBox实现

public Rectangle2D getBox(
  )
{
  if(box == null)
  {
    AffineTransform rot = null;
    Rectangle2D tempBox = null;
    for(TextChar textChar : textChars)
    {
      Rectangle2D thisBox = textChar.getBox();
      if (rot == null) {
          rot = AffineTransform.getRotateInstance(textChar.getAlpha(), thisBox.getX(), thisBox.getY());
          tempBox = (Rectangle2D)thisBox.clone();
      } else {
          Point2D corner = new Point2D.Double(thisBox.getX(), thisBox.getY());
          rot.transform(corner, corner);
          tempBox.add(new Rectangle2D.Double(corner.getX(), corner.getY(), thisBox.getWidth(), thisBox.getHeight()));
      }
    }
    if (tempBox != null) {
        try {
            Point2D corner = new Point2D.Double(tempBox.getX(), tempBox.getY());
            rot.invert();
            rot.transform(corner, corner);
            box = new Rectangle2D.Double(corner.getX(), corner.getY(), tempBox.getWidth(), tempBox.getHeight());
        } catch (NoninvertibleTransformException e) {
            e.printStackTrace();
        }
    }
  }
  return box;
}

(ContentScanner.java内classTextStringWrapper)

ITextString

新角度getter

  public double getAlpha();

(ITextString.java)

TextExtractor 内部classTextString

新角度getter

public double getAlpha() {
    return textChars.isEmpty() ? 0 : textChars.get(0).getAlpha();
}

(TextExtractor.java)

TextInfoExtractionSample

更改为 extract 以正确使用角度勾勒框

[...]
for (ContentScanner.TextStringWrapper textString : text.getTextStrings())
{
    Rectangle2D textStringBox = textString.getBox();
    System.out.println("Text [" + "x:" + Math.round(textStringBox.getX()) + "," + "y:" + Math.round(textStringBox.getY()) + "," + "w:"
            + Math.round(textStringBox.getWidth()) + "," + "h:" + Math.round(textStringBox.getHeight()) + "] [font size:"
            + Math.round(textString.getStyle().getFontSize()) + "]: " + textString.getText());

    // Drawing text character bounding boxes...
    colorIndex = (colorIndex + 1) % textCharBoxColors.length;
    composer.setStrokeColor(textCharBoxColors[colorIndex]);
    for (TextChar textChar : textString.getTextChars())
    {
        // vvv--- changed
        Rectangle2D box = textChar.getBox();
        composer.beginLocalState();
        AffineTransform rot = AffineTransform.getRotateInstance(textChar.getAlpha());
        composer.applyMatrix(rot.getScaleX(), rot.getShearY(), rot.getShearX(), rot.getScaleY(),
                box.getX(), composer.getScanner().getContextSize().getHeight() - box.getY());
        composer.add(new DrawRectangle(0, - box.getHeight(), box.getWidth(), box.getHeight()));

        composer.stroke();
        composer.end();
        // ^^^--- changed
    }

    // Drawing text string bounding box...
    composer.beginLocalState();
    composer.setLineDash(new LineDash(new double[] { 5 }));
    composer.setStrokeColor(textStringBoxColor);
    // vvv--- changed
    AffineTransform rot = AffineTransform.getRotateInstance(textString.getAlpha());
    composer.applyMatrix(rot.getScaleX(), rot.getShearY(), rot.getShearX(), rot.getScaleY(),
            textStringBox.getX(), composer.getScanner().getContextSize().getHeight() - textStringBox.getY());
    composer.add(new DrawRectangle(0, - textStringBox.getHeight(), textStringBox.getWidth(), textStringBox.getHeight()));
    // ^^^--- changed
    composer.stroke();
    composer.end();
}
[...]

(TextInfoExtractionSample方法extract)

结果

字符框和字符串框现在都符合预期:

所以宽度和高度输出现在也可以了:

Text [x:415,y:104,w:138,h:23] [font size:-24]: inverted_text
Text [x:247,y:365,w:128,h:23] [font size:0]: vertical_text
Text [x:364,y:131,w:180,h:23] [font size:0]: vertical_minus90