iText5.x Setting pushbutton appearance without bre

2019-08-10 23:22发布

问题:

Here is the context:

  1. We add two empty pages to an existing pdf, each containing an empty pushbutton field
  2. We apply a PAdES B-B seal with all modification rights on the document
  3. We modify a pushbutton to insert an image in it

When we try to modify the pushbutton appearance to set an image, the seal validity breaks with "unauthorized modification" no matter what we try.

Here is a code sample:

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();

We also tried with the recommanded way in documentation without success:

PushbuttonField pbField = acroFields.getNewPushbuttonFromField(imageFieldId);
pbField.setImage(Image.getInstance("image1.jpg"));
acroFields.replacePushbuttonField(imageFieldId, pbField.getField());

Problem is: i don't know if that type of modification is supported by iText or if it's our way of modifying the button which is wrong?

Update:

If the certification is replaced by a simple signature, we can set the pushbutton appearance without breaking it.

回答1:

Why the certification signature is broken

You say

We apply a PAdES B-B seal with all modification rights on the document

which does not mean that all imaginable modifications of the document are allowed but instead that all allowable modifications are allowed. According to the PDF specification the choices are:

  1. No changes to the document shall be permitted; any change to the document shall invalidate the signature.
  2. Permitted changes shall be filling in forms, instantiating page templates, and signing; other changes shall invalidate the signature.
  3. Permitted changes shall be the same as for 2, as well as annotation creation, deletion, and modification; other changes shall invalidate the signature.

Thus, in case of your document the allowed changes include form fill-ins and arbitrary annotation manipulation.

Unfortunately iText 5, when setting "the value" of an AcroForm push button, does not merely set the button appearance to the button but instead

PushbuttonField pb = getNewPushbuttonFromField(name);
pb.setImage(img);
replacePushbuttonField(name, pb.getField());

I.e. it essentially replaces the former push button with a similar one. This as such is not allowed.

Why a mere approval signature is not broken

The PDF specification does not restrict the changes allowed to a document signed by a mere approval signature (unless restrictions explicitly are given in a FieldMDP transform).

Adobe once claimed that they do restrict changes allowed to signed but not certified documents like those to a certified document with restriction value 3 plus "Adding signature fields", cf. this answer, but apparently they are a bit laxer in other respects, too. In particular current Adobe Reader versions only warn about "Form Fields with Property Changes" in the case at hand.

An additional complication

The PDF in question actually does not have only the AcroForm form definition, instead it has a similar XFA form definition, it is a hybrid form document. Thus, to change the image in both form definitions, one has to consider the filling of the XFA form, too.

Fortunately, the way iText 5 fills in the image into the XFA form does not make Adobe Reader assume the seal broken.

How to set the button image instead to not break the seal

To not break the seal, we have to set the button image without changing the underlying form, merely the widget. Thus, the following code attempts to only change the appearance of the button:

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 test testSetInXfaAndAppearanceSampleCert)

In my tests this results in the image being visible both in viewers that support XFA forms and those that don't, and the seal not being considered broken by Adobe Reader.

Beware, though, I only developed and tested this with your sample document; chances are that some border conditions might not be considered.