如何在 iText 7 中查找文本位置和边界

How to find text position and boundary in iText 7

正如评论所说,这个工作很难,所以我想一步一步解决它,看看限制。首先我会关注下面的第一个问题。

来源:

我想替换 PDF 文件中的文本以进行翻译,例如将英文PDF转换为中文PDF。

我的解决方案是:

  1. 查找所有文本及其位置矩形
  2. 用白色填充所有矩形
  3. 将翻译后的文本绘制回相应的矩形(重新计算适当的字体大小)

具体来说,我实现了 IEventListener 接口来获取渲染信息,并使用此渲染信息来查找具有位置矩形的文本。

但是我遇到了一些问题:

  1. 使用渲染信息,我无法获取文本的确切位置(起点是准确的,但终点有时不准确)
  2. 字体大小因语言和字体而异,例如字体大小 18 在一种语言的一种字体中可能比另一种语言的另一种字体占用更多 space。
  3. 难以合并行或识别段落(不同行中的文本应作为一个块翻译)

有没有比当前解决方案更好的方法来实现我的目标?

或者,谁能针对以上问题提供一些建议?

已更新:

第一题举例:

我只是记录文本及其在渲染中遇到的位置,并在每个文本块周围绘制一个矩形。代码是:

主要在Main.java

PdfDocument pdfDoc = new PdfDocument(new PdfReader(srcFileName), new PdfWriter(destFileName));
SimplePositionalTextEventListener listener = new SimplePositionalTextEventListener();
new PdfCanvasProcessor(listener).processPageContent(pdfDoc.getFirstPage());
List<SimpleTextWithRectangle> result = listener.getResultantTextWithPosition();

int R = 0, G = 0, B = 0;
for(SimpleTextWithRectangle textWithRectangle: result) {
    R += 40; R = R % 256;
    G += 20; G = G % 256;
    B += 80; B = B % 256;
    PdfCanvas canvas = new PdfCanvas(pdfDoc.getPage(pageNumber));
    canvas.setStrokeColor(new DeviceRgb(R, G, B));
    canvas.rectangle(textWithRectangle.getRectangle());
    canvas.stroke();
}

pdfDoc.close();

SimplePositionalTextEventListener.java(implements IEventListener):

private List<SimpleTextWithRectangle> textWithRectangleList = new ArrayList<>();

private void renderText(TextRenderInfo renderInfo) {
    if (renderInfo.getText().trim().length() == 0)
        return;
    LineSegment ascent = renderInfo.getAscentLine();
    LineSegment descent = renderInfo.getDescentLine();

    float initX = descent.getStartPoint().get(0);
    float initY = descent.getStartPoint().get(1);
    float endX = ascent.getEndPoint().get(0);
    float endY = ascent.getEndPoint().get(1);

    Rectangle rectangle = new Rectangle(initX, initY, endX - initX, endY - initY);

    SimpleTextWithRectangle textWithRectangle = new SimpleTextWithRectangle(rectangle, renderInfo.getText());
    textWithRectangleList.add(textWithRectangle);
}

public List<SimpleTextWithRectangle> getResultantTextWithPosition() {
    return textWithRectangleList;
}

@Override
public void eventOccurred(IEventData data, EventType type) {
    renderText((TextRenderInfo) data);
}

@Override
public Set<EventType> getSupportedEvents() {
    return Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList(EventType.RENDER_TEXT)));
}

SimpleTextWithRectangle.java

private Rectangle rectangle;
private String text;

public SimpleTextWithRectangle(Rectangle rectangle, String text) {
    this.rectangle = rectangle;
    this.text = text;
}

public Rectangle getRectangle() {
    return rectangle;
}

文件是: PDF file

处​​理后,header为: 正如我们所看到的,有一些隐藏文本可以在渲染信息中找到,但在 PDF reader 应用程序中不可见。如果我们深入研究每个文本块,我们会发现 renderInfo.getText() 有时无法与我们在 PDF 中看到的文本完全匹配。

处​​理后,页脚为:
我们可以看到,矩形边界不能完全覆盖文字,也就是我在问题1中提到的

不正确的框坐标是 iText 7 CMap 处理中的错误造成的。

错误

解析 Type 0 字体的命名 Encoding CMap 时,例如GBK-EUC-H,使用了这个CMapEncoding构造函数的else分支:

public CMapEncoding(String cmap, String uniMap) {
    this.cmap = cmap;
    this.uniMap = uniMap;
    if (cmap.equals(PdfEncodings.IDENTITY_H) || cmap.equals(PdfEncodings.IDENTITY_V)) {
        cid2Uni = FontCache.getCid2UniCmap(uniMap);
        isDirect = true;
        this.codeSpaceRanges = IDENTITY_H_V_CODESPACE_RANGES;
    } else {
        cid2Code = FontCache.getCid2Byte(cmap);
        code2Cid = cid2Code.getReversMap();
        this.codeSpaceRanges = cid2Code.getCodeSpaceRanges();
    }
}

现在 FontCache.getCid2Byte(cmap) 使用 CMapCidByte 在以下位置构建映射:

public static CMapCidByte getCid2Byte(String cmap) {
    CMapCidByte cidByte = new CMapCidByte();
    return parseCmap(cmap, cidByte);
}

CMapCidByte(可能还有其他 CMap 类)的一个特点是它存储逆映射:

private Map<Integer, byte[]> map = new HashMap<>();
[...]
void addChar(String mark, CMapObject code) {
    if (code.isNumber()) {
        byte[] ser = decodeStringToByte(mark);
        map.put((int)code.getValue(), ser);
    }
}

可能是这样做的,因为最常用的查找方向是相反的。只要原始映射是单射的,即所有键都映射到不同的值,这就可以了。

不幸的是,CMap 不需要是单射的。 例如对于 GBK-EUC-H 我们有 cidrange 条目

<21> <7e> 814 

<aaa1> <aafe> 814 
<aba1> <abc0> 908 

因此,当导入此编码时,后面的映射会覆盖字符代码 0x21..0x7e 的许多映射。

对文本提取边界框的影响

在手头的文档中确实有一种字体,其编码为 GBK-EUC-H,用于页脚文本。因此,对于这种字体,iText 的字体信息中缺少许多单字节代码 0x21..0x7e。

此代码范围以其他等宽字体对成比例的西方字符进行编码,特别是替代代码 0xaaa1..0xaafe 和 0xaba1..0xabc0 将相同的西方字符编码为等宽字符。

在您的示例文档的页脚区域中,使用了这些成比例的拉丁字符。由于缺少映射,某些 iText 7 代码路径中的这些字符被替换为 替换字符符号 (例如,文本提取本身不会 return 西方字符,而是“�” ), 在某些路径中它们完全丢失(例如,当计算文本块的长度时,这些西方字符被忽略)。

因此,字符块的长度计算不正确,因此边界框的大小和位置错误。

这也解释了为什么每行上错位的边界框从该行上第一次出现的西方字符开始,以及为什么在西方字符最多的行上缺少最大的框大小。