使用 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 规范。
我有下面的代码使用 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 规范。