iText5.x 在不破坏密封的情况下设置按钮外观
iText5.x Setting pushbutton appearance without breaking Seal
上下文如下:
- 我们向现有 pdf 添加两个空页面,每个页面包含一个空按钮字段
- 我们应用了 PAdES B-B 印章,对文档拥有所有修改权
- 我们修改一个按钮以在其中插入图像
当我们尝试修改按钮外观以设置图像时,无论我们尝试什么,印章有效性都会因 "unauthorized modification" 而中断。
这是一个代码示例:
PdfReader pdfReader = new PdfReader("test.pdf");
PdfStamper pdfStamper = new PdfStamper(pdfReader, output, pdfReader.getPdfVersion(), true);
AcroFields acroFields = pdfStamper.getAcroFields();
String imageFieldId = "imageField1";
acroFields.setField(imageFieldId, Base64.encodeBytes(consentImage));
pdfStamper.close();
pdfReader.close();
我们也尝试了文档中推荐的方法,但没有成功:
PushbuttonField pbField = acroFields.getNewPushbuttonFromField(imageFieldId);
pbField.setImage(Image.getInstance("image1.jpg"));
acroFields.replacePushbuttonField(imageFieldId, pbField.getField());
问题是:我不知道 iText 是否支持这种类型的修改,或者我们修改按钮的方式是否错误?
更新:
如果将认证换成简单的签名,我们就可以在不破坏的情况下设置按钮的外观。
为什么认证签名被破坏
你说
We apply a PAdES B-B seal with all modification rights on the document
这并不意味着允许对文档进行所有可以想象的修改,而是允许所有允许的修改。根据 PDF 规范,选择是:
- No changes to the document shall be permitted; any change to the document shall invalidate the signature.
- Permitted changes shall be filling in forms, instantiating page templates, and signing; other changes shall invalidate the signature.
- Permitted changes shall be the same as for 2, as well as annotation creation, deletion, and modification; other changes shall invalidate the signature.
因此,对于您的文档,允许的更改包括表单填写和任意注释操作。
不幸的是,当设置 AcroForm 按钮的“值”时,iText 5 不仅将按钮外观设置为按钮,而是
PushbuttonField pb = getNewPushbuttonFromField(name);
pb.setImage(img);
replacePushbuttonField(name, pb.getField());
即它本质上 将 以前的按钮替换为类似的按钮。这是不允许的。
为什么一个简单的认可签名没有被破坏
PDF 规范不限制仅由批准签名签署的文档允许的更改(除非在 FieldMDP 转换中明确给出限制)。
Adobe 曾经声称他们确实限制允许对已签名但未经认证的文档进行更改,例如限制值 3 加上“添加签名字段”的认证文档,cf。 this answer,但显然他们在其他方面也有点宽松。特别是当前的 Adobe Reader 版本仅在手头的案例中警告“具有 属性 更改的表单字段”。
额外的并发症
问题中的PDF其实并没有只有AcroForm的表单定义,而是有一个类似XFA的表单定义,是一个混合表单文档。因此,要更改两个表单定义中的图像,还必须考虑 XFA 表单的填充。
幸运的是,iText 5 将图像填充到 XFA 表单中的方式不会让 Adobe Reader 假设封条已损坏。
如何设置按钮图片而不是破坏密封
为了不破坏封印,我们必须设置按钮图像而不改变基础形式,只改变小部件。因此,以下代码仅尝试更改按钮的外观:
PdfReader pdfReader = new PdfReader(SOURCE);
PdfStamper pdfStamper = new PdfStamper(pdfReader, TARGET, pdfReader.getPdfVersion(), true);
byte[] bytes = IMAGE_BYTES;
AcroFields acroFields = pdfStamper.getAcroFields();
String name = "mainform[0].subform_0[0].image_0_0[0]";
String value = Base64.getEncoder().encodeToString(bytes);
Image image = Image.getInstance(bytes);
XfaForm xfa = acroFields.getXfa();
if (xfa.isXfaPresent()) {
name = xfa.findFieldName(name, acroFields);
if (name != null) {
String shortName = XfaForm.Xml2Som.getShortName(name);
Node xn = xfa.findDatasetsNode(shortName);
if (xn == null) {
xn = xfa.getDatasetsSom().insertNode(xfa.getDatasetsNode(), shortName);
}
xfa.setNodeText(xn, value);
}
}
PdfDictionary widget = acroFields.getFieldItem(name).getWidget(0);
PdfArray boxArray = widget.getAsArray(PdfName.RECT);
Rectangle box = new Rectangle(boxArray.getAsNumber(0).floatValue(), boxArray.getAsNumber(1).floatValue(), boxArray.getAsNumber(2).floatValue(), boxArray.getAsNumber(3).floatValue());
float ratioImage = image.getWidth() / image.getHeight();
float ratioBox = box.getWidth() / box.getHeight();
boolean fillHorizontally = ratioImage > ratioBox;
float width = fillHorizontally ? 1 : ratioBox / ratioImage;
float height = fillHorizontally ? ratioImage / ratioBox : 1;
float xOffset = 0; // centered: (width - 1) / 2;
float yOffset = height - 1; // centered: (height - 1) / 2;
PdfAppearance app = PdfAppearance.createAppearance(pdfStamper.getWriter(), width, height);
app.addImage(image, 1, 0, 0, 1, xOffset, yOffset);
PdfDictionary dic = (PdfDictionary)widget.get(PdfName.AP);
if (dic == null)
dic = new PdfDictionary();
dic.put(PdfAnnotation.APPEARANCE_NORMAL, app.getIndirectReference());
widget.put(PdfName.AP, dic);
pdfStamper.markUsed(widget);
pdfStamper.close();
pdfReader.close();
(SetImageInSignedPdf 测试 testSetInXfaAndAppearanceSampleCert
)
在我的测试中,这导致图像在支持 XFA 表单的查看器和不支持 XFA 表单的查看器中都可见,并且 Adobe 不认为密封被破坏 Reader。
不过请注意,我只使用您的示例文档开发和测试了它;很可能有些边界条件可能不会被考虑。
上下文如下:
- 我们向现有 pdf 添加两个空页面,每个页面包含一个空按钮字段
- 我们应用了 PAdES B-B 印章,对文档拥有所有修改权
- 我们修改一个按钮以在其中插入图像
当我们尝试修改按钮外观以设置图像时,无论我们尝试什么,印章有效性都会因 "unauthorized modification" 而中断。
这是一个代码示例:
PdfReader pdfReader = new PdfReader("test.pdf");
PdfStamper pdfStamper = new PdfStamper(pdfReader, output, pdfReader.getPdfVersion(), true);
AcroFields acroFields = pdfStamper.getAcroFields();
String imageFieldId = "imageField1";
acroFields.setField(imageFieldId, Base64.encodeBytes(consentImage));
pdfStamper.close();
pdfReader.close();
我们也尝试了文档中推荐的方法,但没有成功:
PushbuttonField pbField = acroFields.getNewPushbuttonFromField(imageFieldId);
pbField.setImage(Image.getInstance("image1.jpg"));
acroFields.replacePushbuttonField(imageFieldId, pbField.getField());
问题是:我不知道 iText 是否支持这种类型的修改,或者我们修改按钮的方式是否错误?
更新:
如果将认证换成简单的签名,我们就可以在不破坏的情况下设置按钮的外观。
为什么认证签名被破坏
你说
We apply a PAdES B-B seal with all modification rights on the document
这并不意味着允许对文档进行所有可以想象的修改,而是允许所有允许的修改。根据 PDF 规范,选择是:
- No changes to the document shall be permitted; any change to the document shall invalidate the signature.
- Permitted changes shall be filling in forms, instantiating page templates, and signing; other changes shall invalidate the signature.
- Permitted changes shall be the same as for 2, as well as annotation creation, deletion, and modification; other changes shall invalidate the signature.
因此,对于您的文档,允许的更改包括表单填写和任意注释操作。
不幸的是,当设置 AcroForm 按钮的“值”时,iText 5 不仅将按钮外观设置为按钮,而是
PushbuttonField pb = getNewPushbuttonFromField(name);
pb.setImage(img);
replacePushbuttonField(name, pb.getField());
即它本质上 将 以前的按钮替换为类似的按钮。这是不允许的。
为什么一个简单的认可签名没有被破坏
PDF 规范不限制仅由批准签名签署的文档允许的更改(除非在 FieldMDP 转换中明确给出限制)。
Adobe 曾经声称他们确实限制允许对已签名但未经认证的文档进行更改,例如限制值 3 加上“添加签名字段”的认证文档,cf。 this answer,但显然他们在其他方面也有点宽松。特别是当前的 Adobe Reader 版本仅在手头的案例中警告“具有 属性 更改的表单字段”。
额外的并发症
问题中的PDF其实并没有只有AcroForm的表单定义,而是有一个类似XFA的表单定义,是一个混合表单文档。因此,要更改两个表单定义中的图像,还必须考虑 XFA 表单的填充。
幸运的是,iText 5 将图像填充到 XFA 表单中的方式不会让 Adobe Reader 假设封条已损坏。
如何设置按钮图片而不是破坏密封
为了不破坏封印,我们必须设置按钮图像而不改变基础形式,只改变小部件。因此,以下代码仅尝试更改按钮的外观:
PdfReader pdfReader = new PdfReader(SOURCE);
PdfStamper pdfStamper = new PdfStamper(pdfReader, TARGET, pdfReader.getPdfVersion(), true);
byte[] bytes = IMAGE_BYTES;
AcroFields acroFields = pdfStamper.getAcroFields();
String name = "mainform[0].subform_0[0].image_0_0[0]";
String value = Base64.getEncoder().encodeToString(bytes);
Image image = Image.getInstance(bytes);
XfaForm xfa = acroFields.getXfa();
if (xfa.isXfaPresent()) {
name = xfa.findFieldName(name, acroFields);
if (name != null) {
String shortName = XfaForm.Xml2Som.getShortName(name);
Node xn = xfa.findDatasetsNode(shortName);
if (xn == null) {
xn = xfa.getDatasetsSom().insertNode(xfa.getDatasetsNode(), shortName);
}
xfa.setNodeText(xn, value);
}
}
PdfDictionary widget = acroFields.getFieldItem(name).getWidget(0);
PdfArray boxArray = widget.getAsArray(PdfName.RECT);
Rectangle box = new Rectangle(boxArray.getAsNumber(0).floatValue(), boxArray.getAsNumber(1).floatValue(), boxArray.getAsNumber(2).floatValue(), boxArray.getAsNumber(3).floatValue());
float ratioImage = image.getWidth() / image.getHeight();
float ratioBox = box.getWidth() / box.getHeight();
boolean fillHorizontally = ratioImage > ratioBox;
float width = fillHorizontally ? 1 : ratioBox / ratioImage;
float height = fillHorizontally ? ratioImage / ratioBox : 1;
float xOffset = 0; // centered: (width - 1) / 2;
float yOffset = height - 1; // centered: (height - 1) / 2;
PdfAppearance app = PdfAppearance.createAppearance(pdfStamper.getWriter(), width, height);
app.addImage(image, 1, 0, 0, 1, xOffset, yOffset);
PdfDictionary dic = (PdfDictionary)widget.get(PdfName.AP);
if (dic == null)
dic = new PdfDictionary();
dic.put(PdfAnnotation.APPEARANCE_NORMAL, app.getIndirectReference());
widget.put(PdfName.AP, dic);
pdfStamper.markUsed(widget);
pdfStamper.close();
pdfReader.close();
(SetImageInSignedPdf 测试 testSetInXfaAndAppearanceSampleCert
)
在我的测试中,这导致图像在支持 XFA 表单的查看器和不支持 XFA 表单的查看器中都可见,并且 Adobe 不认为密封被破坏 Reader。
不过请注意,我只使用您的示例文档开发和测试了它;很可能有些边界条件可能不会被考虑。