由于我更熟悉 Java 而不是 .Net,因此我分析了问题并在 PDF Clown / Java 中创建了第一个解决方法;稍后我会尝试将其移植到.Net。不过,自己做应该不会太难。
问题
您提供的示例文件在通过 PDF Clown 运行时使问题变得非常清楚TextInfoExtractionSample
。
截图edit9.pdf
:

edit9.pdf
申请后截图TextInfoExtractionSample
:

直立文字
一切看起来都很好。
颠倒的文字
单个字符框(绿色)看起来不错,但整个字符串“inverted_text”(黑色虚线)的框不包括最外面的字符。
竖排文本
单个字符框缩小为 0x0 矩形(在屏幕截图中不可见,但在内容流分析中很明显)。整个字符串的框在缺少位长度的字符串的基线上缩小为一条线(黑色虚线)。
中间夹角的文字
字符框是直立的,与页面边界平行,其基线段位于框内。但是,由于文本是倾斜的,字符的上部和下部部分位于其各自的字符框之外,而相邻的字符部分位于其内部。
整个字符串的框也与页面平行。
简而言之
文本字符和字符串框仅适用于直立文本。
在来源
这与在源代码中找到的内容相匹配:
设计用于字符框的 JavaRectangle2D
和 .Net类适用于平行于坐标系轴的矩形,并在 PDF Clown 中以这种方式使用。RectangleF
因此,它们不能以任意角度正确表示字符的宽度和高度。
PDF Clown 类不包含Angle
表示字符旋转的属性。
字符框尺寸的计算只考虑聚合变换矩阵的主对角线上的值,即ScaleX
和ScaleY
,而忽略ShearX
和ShearY
。但是,对于不直立或颠倒的文本ShearX
并且ShearY
很重要,对于垂直文本ScaleX
并且ScaleY
为 0。
从基线(定位文本的本机 PDF 方式)到字符顶部(PDF Clown 文本定位)的转换是通过单独更改 y 坐标完成的,因此,仅适用于直立和倒置文本。
一种解决方法
真正解决这个问题需要对字符框和字符串框使用完全不同的类,该类可以对任意角度的矩形进行建模。
但是,更快的解决方法是向 类和实现添加angle
成员,然后在处理框时考虑该角度。此解决方法在此处实施。TextChar
ITextString
正如上面已经提到的,解决方法首先是在 Java 中实现的。
在 Java 中
首先我们添加一个角度成员,在操作类TextChar
中计算盒子尺寸和角度的正确值,并在.ShowText
ContentScanner.TextStringWrapper
然后我们添加一个角度获取器TextStringWrapper
(ITextString
通常),它返回字符串的第一个文本字符的角度。我们改进了在确定字符串框时考虑文本字符角度的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>
角度的吸气剂
public double getAlpha() {
return alpha;
}
( TextChar.java )
显示文本
更新内部接口IScanner
方法scanChar
以传输角度
void scanChar(
char textChar,
Rectangle2D textCharBox,
double alpha
);
(ShowText.java内部接口IScanner
)
更新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 内部类 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
)
);
}
}
);
角度的吸气剂
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内部类TextStringWrapper
)
文本字符串
新角度吸气剂
public double getAlpha();
( ITextString.java )
TextExtractor 内部类 TextString
新角度吸气剂
public double getAlpha() {
return textChars.isEmpty() ? 0 : textChars.get(0).getAlpha();
}
( TextExtractor.java )
文本信息提取示例
更改为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