从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
的截图:
应用TextInfoExtractionSample
后edit9.pdf
的截图:
竖排文字
一切正常。
颠倒文字
单个字符框(绿色)看起来不错,但整个字符串的框 "inverted_text"(黑色虚线)不包括最外面的字符。
竖排文字
单个字符框缩小为 0x0 矩形(在屏幕截图中不可见,但在内容流分析中很明显)。整个字符串的方框在字符串的 基线 上减少为一条线(黑色虚线),缺少一点长度。
中间有角度的文本
字符框是直立的,与页面边框平行,其基线段在框内。但是,由于文本是倾斜的,因此字符的上部和下部部分位于各自的字符框之外,而相邻字符则部分位于字符框内。
整个字符串的框也与页面平行。
简而言之
文本字符和字符串框仅适用于直立文本。
在来源中
这与在源代码中找到的内容相符:
The Java Rectangle2D
and .Net RectangleF
classes 用于字符框的设计用于平行于坐标的矩形系统轴,并在 PDF Clown 中以这种方式使用。因此,它们不能正确表示任意角度的字符宽度和高度。
PDF 小丑 classes 不包含 Angle
属性来表示角色的旋转。
字符框维度的计算只考虑聚合变换矩阵主对角线上的值,即ScaleX
和ScaleY
,而忽略ShearX
和 ShearY
。但是,对于非直立或倒置的文本,ShearX
和 ShearY
很重要,对于垂直文本,ScaleX
和 ScaleY
是 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;
}
显示文本
更新内部接口[=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);
}
[...]
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();
TextExtractor 内部classTextString
新角度getter
public double getAlpha() {
return textChars.isEmpty() ? 0 : textChars.get(0).getAlpha();
}
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
如何提取文本位置坐标、宽高等文本信息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
的截图:
应用TextInfoExtractionSample
后edit9.pdf
的截图:
竖排文字
一切正常。
颠倒文字
单个字符框(绿色)看起来不错,但整个字符串的框 "inverted_text"(黑色虚线)不包括最外面的字符。
竖排文字
单个字符框缩小为 0x0 矩形(在屏幕截图中不可见,但在内容流分析中很明显)。整个字符串的方框在字符串的 基线 上减少为一条线(黑色虚线),缺少一点长度。
中间有角度的文本
字符框是直立的,与页面边框平行,其基线段在框内。但是,由于文本是倾斜的,因此字符的上部和下部部分位于各自的字符框之外,而相邻字符则部分位于字符框内。
整个字符串的框也与页面平行。
简而言之
文本字符和字符串框仅适用于直立文本。
在来源中
这与在源代码中找到的内容相符:
The Java
Rectangle2D
and .NetRectangleF
classes 用于字符框的设计用于平行于坐标的矩形系统轴,并在 PDF Clown 中以这种方式使用。因此,它们不能正确表示任意角度的字符宽度和高度。PDF 小丑 classes 不包含
Angle
属性来表示角色的旋转。字符框维度的计算只考虑聚合变换矩阵主对角线上的值,即
ScaleX
和ScaleY
,而忽略ShearX
和ShearY
。但是,对于非直立或倒置的文本,ShearX
和ShearY
很重要,对于垂直文本,ScaleX
和ScaleY
是 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;
}
显示文本
更新内部接口[=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);
}
[...]
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();
TextExtractor 内部classTextString
新角度getter
public double getAlpha() {
return textChars.isEmpty() ? 0 : textChars.get(0).getAlpha();
}
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