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

时间:2018-06-17 07:41:14

标签: java itext7

正如评论所说,这项工作很难,所以我想逐步解决这个问题。首先,我将重点关注下面的第一个问题。

产地:

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

我的解决方案是:

  1. 查找所有带有位置矩形的文字
  2. 用白色填充所有矩形
  3. 将翻译后的文本绘制回相应的矩形(具有适当重新计算的字体大小)
  4. 具体来说,我实现IEventListener接口来获取渲染信息,并使用此渲染信息查找位置矩形的文本。

    但我遇到了一些问题:

    1. 使用渲染信息,我无法获得文本的确切位置(起点准确,但终点有时不准确)
    2. 字体大小因语言和字体而异,例如字体大小18可能在一种语言的一种字体中占用比另一种语言中的另一种字体更多的空间。
    3. 很难合并线条或识别段落(不同行中的文本应该翻译成块)
    4. 有没有比现有解决方案更好的方法来实现我的目标?

      或者,任何人都可以就上述问题提出一些建议吗?

      已更新

      第一个问题的例子:

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

      主要在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

      经过处理后,标题是: enter image description here 我们可以看到,有一些隐藏的文本可以在渲染信息中找到,但在PDF阅读器应用程序中是不可见的。如果我们深入研究每个文本块,我们可以看到renderInfo.getText()有时不能与我们在PDF中看到的文本完全匹配。

      在处理之后,页脚是: enter image description here
      我们可以看到,矩形边界不能完全覆盖文本,即我在问题1中提到的

1 个答案:

答案 0 :(得分:2)

不正确的框坐标是iText 7 CMap处理错误的结果。

错误

解析类型为0的命名的 Encoding CMap时,例如 GBK-EUC-H ,使用了此else构造函数的CMapEncoding分支:

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代码路径中的这些字符被替换为替换字符符号(例如,文本提取本身不返回西方字符,而是返回“。”),在某些路径中它们是完全丢失(例如,当计算文本块的长度时,这些西方字符将被忽略)。

因此,字符块的长度计算不正确,因此边界框的尺寸和放错了位置。

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