如何使用 itext 或 pdfbox 旋转 pdf 中已经存在的特定文本?

How to rotate a specific text in a pdf which is already present in the pdf using itext or pdfbox?

我知道我们可以使用 itext 将文本旋转插入 pdf。但是我想旋转 pdf 中已经存在的文本。

首先,在您的问题中,您只讨论了如何旋转特定文本,但在您的示例中,您还旋转了一个红色矩形。这个答案侧重于旋转文本。猜测哪些图形可能与文本相关并因此可能应该一起旋转的过程本身就是一个主题。

您还提到您正在寻找解决方案 使用 itext 或 pdfbox 并使用标签 , , and 。对于这个答案,我选择了 iText 7。

您没有解释要旋转的文本片段类型,但提供了具有代表性的 PDF 示例。在那个例子中,我看到要旋转的文本是使用单个文本显示指令绘制的,这是页面内容流中包含的文本对象中唯一的此类指令。因此,为了使答案中的代码保持简单,我可以假设要旋转的文本是在连续的文本序列中绘制的,这些文本显示了页面内容流中文本对象中的指令,这些指令由不是显示文本的指令构成。这是对你的案例的概括。

另外,你没有提到旋转中心。根据您的示例文件,我假设它大约是要旋转的文本基线的起点。

一个简单的实现

编辑 PDF 内容流时,了解每条指令的当前图形状态很有帮助,例如要正确识别文本显示操作绘制的文本,需要知道当前字体以将字符代码映射到 Unicode 字符。 iText 中的文本提取框架已经包含了跟踪图形状态的代码。因此,在 中,在文本提取框架之上开发了一个基础 PdfCanvasEditor class。

我们可以在 class 稍作扩展后为手头的任务制定解决方案; class 最初将文本提取事件侦听器设置为虚拟实现,但在这里我们需要一个自定义实现。所以我们需要添加一个额外的构造函数来接受这样一个自定义事件监听器作为参数:

public PdfCanvasEditor(IEventListener listener)
{
    super(listener);
}

(附加PdfCanvasEditor构造函数)

基于这个扩展PdfCanvasEditor我们可以通过逐个指令检查现有页面内容流来实现任务。对于显示指令的连续文本序列,我们检索序列前后的文本矩阵,如果序列绘制的文本结果是要旋转的文本,我们在该序列之前插入一条指令,将初始文本矩阵设置为自身的旋转版本和该序列之后的另一个版本,将文本矩阵设置回原来的位置。

我们的实现 LimitedTextRotater 接受表示所需旋转的 Matrix 和匹配要旋转的字符串的 Predicate

public class LimitedTextRotater extends PdfCanvasEditor {
    public LimitedTextRotater(Matrix rotation, Predicate<String> textMatcher) {
        super(new TextRetrievingListener());
        ((TextRetrievingListener)getEventListener()).limitedTextRotater = this;
        this.rotation = rotation;
        this.textMatcher = textMatcher;
    }

    @Override
    protected void write(PdfCanvasProcessor processor, PdfLiteral operator, List<PdfObject> operands) {
        String operatorString = operator.toString();

        if (TEXT_SHOWING_OPERATORS.contains(operatorString)) {
            recentTextOperations.add(new ArrayList<>(operands));
        } else {
            if (!recentTextOperations.isEmpty()) {
                boolean rotate = textMatcher.test(text.toString());
                if (rotate)
                    writeSetTextMatrix(processor, rotation.multiply(initialTextMatrix));
                for (List<PdfObject> recentOperation : recentTextOperations) {
                    super.write(processor, (PdfLiteral) recentOperation.get(recentOperation.size() - 1), recentOperation);
                }
                if (rotate)
                    writeSetTextMatrix(processor, finalTextMatrix);
                recentTextOperations.clear();
                text.setLength(0);
                initialTextMatrix = null;
            }
            super.write(processor, operator, operands);
        }
    }

    void writeSetTextMatrix(PdfCanvasProcessor processor, Matrix textMatrix) {
        PdfLiteral operator = new PdfLiteral("Tm\n");
        List<PdfObject> operands = new ArrayList<>();
        operands.add(new PdfNumber(textMatrix.get(Matrix.I11)));
        operands.add(new PdfNumber(textMatrix.get(Matrix.I12)));
        operands.add(new PdfNumber(textMatrix.get(Matrix.I21)));
        operands.add(new PdfNumber(textMatrix.get(Matrix.I22)));
        operands.add(new PdfNumber(textMatrix.get(Matrix.I31)));
        operands.add(new PdfNumber(textMatrix.get(Matrix.I32)));
        operands.add(operator);
        super.write(processor, operator, operands);
    }

    void eventOccurred(TextRenderInfo textRenderInfo) {
        Matrix textMatrix = textRenderInfo.getTextMatrix();
        if (initialTextMatrix == null)
            initialTextMatrix = textMatrix;
        finalTextMatrix = new Matrix(textRenderInfo.getUnscaledWidth(), 0).multiply(textMatrix);

        text.append(textRenderInfo.getText());
    }

    static class TextRetrievingListener implements IEventListener {
        @Override
        public void eventOccurred(IEventData data, EventType type) {
            if (data instanceof TextRenderInfo) {
                limitedTextRotater.eventOccurred((TextRenderInfo) data);
            }
        }

        @Override
        public Set<EventType> getSupportedEvents() {
            return null;
        }

        LimitedTextRotater limitedTextRotater;
    }

    final static List<String> TEXT_SHOWING_OPERATORS = Arrays.asList("Tj", "'", "\"", "TJ");

    final Matrix rotation;
    final Predicate<String> textMatcher;

    final List<List<PdfObject>> recentTextOperations = new ArrayList<>();
    final StringBuilder text = new StringBuilder();
    Matrix initialTextMatrix = null;
    Matrix finalTextMatrix = null;
}

(LimitedTextRotater)

您可以像这样将其应用于文档:

try (   PdfReader pdfReader = new PdfReader(...);
        PdfWriter pdfWriter = new PdfWriter(...);
        PdfDocument pdfDocument = new PdfDocument(pdfReader, pdfWriter) )
{
    PdfCanvasEditor editor = new LimitedTextRotater(new Matrix(0, -1, 1, 0, 0, 0), text -> true);
    for (int i = 1; i <= pdfDocument.getNumberOfPages(); i++){
        editor.editPage(pdfDocument, i);
    }
}

(RotateText 测试 testBeforeAkhilNagaSai)

这里使用的Predicatetext -> true,匹配任意文本。如果您的示例 PDF 没问题,因为要旋转的文本是唯一的文本。一般来说,您可能需要更具体的检查,例如text -> text.equals("The text to be rotated")。不过,一般来说尽量不要太具体,因为提取的文本可能会略微偏离预期,例如通过额外的空格。

结果:

如您所见,文本已旋转。但是,与您的 After.pdf 相比,红色矩形没有旋转。原因是——正如开头已经提到的——那个矩形绝不是文本的一部分。

一些想法

首先,有 PdfCanvasEditor 到 iText 5 的端口(this answer) and PDFBox (the PdfContentStreamEditor in 中的 PdfContentStreamEditor)。因此,如果您最终更愿意切换到这些 PDF 库中的任何一个,则可以创建等效的实现。

然后,如果假设要旋转的文本是在显示指令的连续文本序列中绘制的,这些指令在页面内容流中的文本对象中由不是显示指令的指令构成 不适合你,你可以在这里概括一下实现。查看 中的 SimpleTextRemover 以获取灵感,它基于 iText 5 的 PdfContentStreamEditor 。这里还有一些文本在显示说明的文本中的某处开始并在另一文本中的某处结束是处理这需要一些更详细的数据保存和现有文本绘图指令的拆分。

此外,如果您想旋转图形以及人类观众可能认为与其关联的文本(如示例文件中的红色矩形),您可以尝试相应地扩展示例,例如通过还提取旋转文本的坐标,并在一秒钟内 运行 尝试猜测这些坐标周围的哪些图形是相关的,并沿着这些图形旋转。不过,这并非微不足道。

最后注意构造函数中提供的Matrix不限于旋转,它可以表示任意的仿射变换。因此,除了旋转文本之外,您还可以移动、缩放或倾斜文本,...