使用 itext7 .NET 更新现有标记(自由文本标注)PDF

Updating existing markup (FreeText Callout) PDF using itext7 .NET

我有下面的代码使用 itext7 .NET 更新现有标记(自由文本标注)PDF。它显示不正确,但在 bluebeam 中对其进行编辑,然后显示正确的内容,如下图所示: 我错过了什么?

public void UpdateMarkupCallout()
{
    string inPDF = @"C:\in PDF.pdf";
    string outPDF = @"C:\out PDF.pdf";
    PdfDocument pdfDoc = new PdfDocument(new PdfReader(inPDF), new PdfWriter(outPDF));
    int numberOfPages = pdfDoc.GetNumberOfPages();
    for (int i = 1; i <= numberOfPages; i++)
    {
        PdfDictionary page = pdfDoc.GetPage(i).GetPdfObject();
        PdfArray annotArray = page.GetAsArray(PdfName.Annots);
        if (annotArray == null)
        {
            continue;
        }
        int size = annotArray.Size();
        for (int x = 0; x < size; x++)
        {
            PdfDictionary curAnnot = annotArray.GetAsDictionary(x);
            if (curAnnot.GetAsString(PdfName.Contents) != null)
            {
                string contents = curAnnot.GetAsString(PdfName.Contents).ToString();
                if (contents != "" && contents.Contains("old content"))
                {
                    curAnnot.Put(PdfName.Contents, new PdfString("new content"));
                }
            }
        }
    }
    pdfDoc.Close();
}

附件:here

答案在 Java 中,但转换为 C# 应该是一些简单的字母大小写替换和小调整的问题。

不幸的是,这里没有银弹解决方案,至少在不付出巨大努力的情况下是这样。

1。偏正解

这里有几个问题。首先,您只更新 /Contents 键,而您正在编辑的注释也有 /RC 键,代表 A rich text string (see Adobe XML Architecture, XML Forms Architecture (XFA) Specification, version 3.3) that shall be used to generate the appearance of the annotation. (ISO 32000).

最重要的是,必须重新生成外观(/AP 条目)。按照规范规定。这不是 iText 目前能够做的,所以你必须自己做。

您需要确定必须绘制文本的区域,将 /RD 或 rect diff 条目考虑在内。

要创建您的外观,您可以使用 pdfHTML 附加组件,它会将来自 /RC 的富文本表示处理为布局元素,您可以将这些元素传输到 XObject 中,您可以将其放入 /AP.

代码类似如下:

PdfDocument pdfDocument = new PdfDocument(new PdfReader("in PDF.pdf"),
        new PdfWriter("out PDF.pdf"));

int numberOfPages = pdfDocument.getNumberOfPages();
for (int i = 1; i <= numberOfPages; i++) {
    PdfDictionary page = pdfDocument.getPage(i).getPdfObject();
    PdfArray annotArray = page.getAsArray(PdfName.Annots);
    if (annotArray == null) {
        continue;
    }
    int size = annotArray.size();
    for (int x = 0; x < size; x++) {
        PdfDictionary curAnnot = annotArray.getAsDictionary(x);
        if (curAnnot.getAsString(PdfName.Contents) != null) {
            String contents = curAnnot.getAsString(PdfName.Contents).toString();
            if (!contents.isEmpty() && contents.contains("old content")) //set layer for a FreeText with this content
            {
                curAnnot.put(PdfName.Contents, new PdfString("new content"));
                String richText = curAnnot.getAsString(PdfName.RC).toUnicodeString();
                Document document = Jsoup.parse(richText);
                for (Element element : document.select("p")) {
                    element.html("new content");
                }
                curAnnot.put(PdfName.RC, new PdfString(document.body().outerHtml()));

                Rectangle bbox = curAnnot.getAsRectangle(PdfName.Rect);

                Rectangle textBbox = bbox.clone();
                // left, top, right, bottom
                PdfArray rectDiff = curAnnot.getAsArray(PdfName.RD);
                if (rectDiff != null) {
                    textBbox.applyMargins(rectDiff.getAsNumber(1).floatValue(),
                            rectDiff.getAsNumber(2).floatValue(),
                            rectDiff.getAsNumber(3).floatValue(),
                            rectDiff.getAsNumber(0).floatValue(), false);
                }
                float leftRectDiff = rectDiff != null ? rectDiff.getAsNumber(0).floatValue() : 0;
                float topRectDiff = rectDiff != null ? rectDiff.getAsNumber(1).floatValue() : 0;

                List<IElement> elements = HtmlConverter.convertToElements(document.body().outerHtml());
                PdfFormXObject appearance = new PdfFormXObject(
                        new Rectangle(0, 0, bbox.getWidth(), bbox.getHeight()));
                Canvas canvas = new Canvas(new PdfCanvas(appearance, pdfDocument),
                        new Rectangle(leftRectDiff, topRectDiff, textBbox.getWidth(), textBbox.getHeight()));
                canvas.setProperty(Property.RENDERING_MODE, RenderingMode.HTML_MODE);
                for (IElement ele : elements) {
                    if (ele instanceof IBlockElement) {
                        canvas.add((IBlockElement) ele);
                    }
                }
                curAnnot.getAsDictionary(PdfName.AP).put(PdfName.N, appearance.getPdfObject());
            }
        }
    }
}

pdfDocument.close();

您会得到如下所示的结果:

您可以看到新文本按预期显示,但整体视觉表现与我们的预期相去甚远——背景填充、边框和箭头缺失。因此,要正确生成外观,您必须进一步探索其他 PDF 属性,例如 /CL(箭头描述符)、/BS(边框样式)、/C(背景颜色)等。这需要相当长的时间 - 阅读规范,解析相关条目并将其应用到您的绘图操作中。您可以从 PdfFormField class 实现中获得一些灵感。

2。没有任何保证的简单解决方案

如果您希望注释中的文本仅由一行组成,是纯拉丁文本,并且通常输入文档的可变性很小,您可以采用当前外观并假设文本字符串将以一块的形式写在那里(输入文档就是这种情况)。

请注意,这是一种容易产生许多潜在问题的 hacky 方法 errors/bugs

示例代码:

PdfDocument pdfDocument = new PdfDocument(new PdfReader("in PDF.pdf"),
        new PdfWriter("out PDF.pdf"));

int numberOfPages = pdfDocument.getNumberOfPages();
for (int i = 1; i <= numberOfPages; i++) {
    PdfDictionary page = pdfDocument.getPage(i).getPdfObject();
    PdfArray annotArray = page.getAsArray(PdfName.Annots);
    if (annotArray == null) {
        continue;
    }
    int size = annotArray.size();
    for (int x = 0; x < size; x++) {
        PdfDictionary curAnnot = annotArray.getAsDictionary(x);
        if (curAnnot.getAsString(PdfName.Contents) != null) {
            String contents = curAnnot.getAsString(PdfName.Contents).toString();
            String oldContent = "old content";
            if (!contents.isEmpty() && contents.contains(oldContent)) {
                String newContent = "new content";
                curAnnot.put(PdfName.Contents, new PdfString(newContent));
                String richText = curAnnot.getAsString(PdfName.RC).toUnicodeString();
                Document document = Jsoup.parse(richText);
                for (Element element : document.select("p")) {
                    element.html(newContent);
                }
                curAnnot.put(PdfName.RC, new PdfString(document.body().outerHtml()));

                PdfStream currentAppearance = curAnnot.getAsDictionary(PdfName.AP).getAsStream(PdfName.N);
                String currentBytes = new String(currentAppearance.getBytes(), StandardCharsets.UTF_8);
                currentBytes = currentBytes.replace("(" + oldContent + ") Tj", "(" + newContent + ") Tj");
                currentAppearance.setData(currentBytes.getBytes(StandardCharsets.UTF_8));
            }
        }
    }
}

pdfDocument.close();

视觉效果(如您所见,这就是我们想要的):

3。不合规的解决方案

另一种方法,不符合 PDF 规范,是删除任何 /AP 条目。您可以在与 curAnnot.remove(PdfName.AP); 完全相同的循环中执行此操作。大多数主要的 PDF 查看器都会自行重新生成外观。但是,我的查看器生成的外观并不是最吸引人的方式:

如您所见,结果将取决于 PDF 查看器,这很好地说明了 PDF 规范要求存在 /AP 的原因。 再次说明,这种方式不符合 PDF 规范