Need To Add A New Page To A Pdf Document That Already Has A Digital Signature
Solution 1:
This answer shows 90% of a solution of the problem up to the problem mentioned in my comments to the original question.
Helper class for page template handling
Years ago, when Adobe Reader started to consider a signature broken as soon as additional pages with new content were added to the PDF, I experimented with page template instantiation. As it turns out, that code could easily be adapted to the current 5.5.x iText versions (and additionally to Java generics). I have not tried adaption to iText 7 yet.
Due to limited visibility of iText API methods used here, this class has to be put into the package com.itextpdf.text.pdf
. Alternatively this class may be changed to make considerable use of reflection magic.
publicclassPdfStamperHelper
{
publicstaticfinalPdfNameTEMPLATES=newPdfName("Templates");
publicstaticfinalPdfNameTEMPLATE=newPdfName("Template");
publicstaticfinalPdfNameTEMPLATE_INSTANTIATED=newPdfName("TemplateInstantiated");
/**
* This method names a given page. The page in question already has
* to exist in the original document the given PdfStamper works on.
*/publicstaticvoidcreateTemplate(PdfStamper pdfStamper, String name, int page)throws IOException, DocumentException
{
PdfDictionarypageDic= pdfStamper.stamper.reader.getPageNRelease(page);
if (pageDic != null && pageDic.getIndRef() != null)
{
HashMap<String, PdfObject> namedPages = getNamedPages(pdfStamper);
namedPages.put(name, pageDic.getIndRef());
storeNamedPages(pdfStamper);
}
}
/**
* This method hides a given visible named page.
*/publicstaticvoidhideTemplate(PdfStamper pdfStamper, String name)throws IOException, DocumentException
{
HashMap<String, PdfObject> namedPages = getNamedPages(pdfStamper);
PdfObjectobject= namedPages.get(name);
if (object == null)
thrownewDocumentException("Document contains no visible template " + name + '.');
namedPages.remove(name);
storeNamedPages(pdfStamper);
if (removePage(pdfStamper, (PRIndirectReference)pdfStamper.stamper.reader.getCatalog().get(PdfName.PAGES), (PRIndirectReference) object))
{
pdfStamper.stamper.reader.pageRefs.reReadPages();
// TODO: correctAcroFieldPages
}
PdfDictionarypageDict= (PdfDictionary)PdfReader.getPdfObject(object);
if (pageDict != null)
{
pdfStamper.stamper.markUsed(pageDict);
pageDict.remove(PdfName.PARENT);
pageDict.remove(PdfName.B);
pageDict.put(PdfName.TYPE, TEMPLATE);
}
HashMap<String, PdfObject> templates = getNamedTemplates(pdfStamper);
templates.put(name, object);
storeNamedTemplates(pdfStamper);
}
/**
* This method returns a template dictionary.
*/publicstatic PdfDictionary getTemplate(PdfStamper pdfStamper, String name)throws DocumentException
{
HashMap<String, PdfObject> namedTemplates = getNamedTemplates(pdfStamper);
PdfObjectobject= (PdfObject) namedTemplates.get(name);
if (object == null) {
HashMap<String, PdfObject> namedPages = getNamedPages(pdfStamper);
object = namedPages.get(name);
}
return (PdfDictionary)PdfReader.getPdfObject(object);
}
/**
* This method spawns a template inserting it at the given page number.
*/publicstaticvoidspawnTemplate(PdfStamper pdfStamper, String name, int pageNumber)throws DocumentException, IOException
{
PdfDictionarytemplate= getTemplate(pdfStamper, name);
if (template == null)
thrownewDocumentException("Document contains no template " + name + '.');
PdfReaderreader= pdfStamper.stamper.reader;
// contRef: reference to the content stream of the spawned page;// it only inserts the template XObjectPRIndirectReferencecontRef= reader.addPdfObject(getTemplateStream(name, reader.getPageSize(template)));
// resRef: reference to resources dictionary containing a /XObject// dictionary in turn containing the template XObject resource// carrying the actual template contentPdfDictionaryxobjDict=newPdfDictionary();
xobjDict.put(newPdfName(name), reader.addPdfObject(getFormXObject(reader, template, pdfStamper.stamper.getCompressionLevel(), name)));
PdfDictionaryresources=newPdfDictionary();
resources.put(PdfName.XOBJECT, xobjDict);
PRIndirectReferenceresRef= reader.addPdfObject(resources);
// page: dictionary of the spawned template pagePdfDictionarypage=newPdfDictionary();
page.put(PdfName.TYPE, PdfName.PAGE); // not PdfName.TEMPLATE!
page.put(TEMPLATE_INSTANTIATED, newPdfName(name));
page.put(PdfName.CONTENTS, contRef);
page.put(PdfName.RESOURCES, resRef);
page.mergeDifferent(template); // actually a bit too much. TODO: treat annotations as they should be treatedPRIndirectReferencepref= reader.addPdfObject(page);
PdfDictionary parent;
PRIndirectReference parentRef;
if (pageNumber > reader.getNumberOfPages()) {
PdfDictionarylastPage= reader.getPageNRelease(reader.getNumberOfPages());
parentRef = (PRIndirectReference)lastPage.get(PdfName.PARENT);
parentRef = newPRIndirectReference(reader, parentRef.getNumber());
parent = (PdfDictionary)PdfReader.getPdfObject(parentRef);
PdfArraykids= (PdfArray)PdfReader.getPdfObject(parent.get(PdfName.KIDS), parent);
kids.add(pref);
pdfStamper.stamper.markUsed(kids);
reader.pageRefs.insertPage(pageNumber, pref);
}
else {
if (pageNumber < 1)
pageNumber = 1;
PdfDictionaryfirstPage= reader.getPageN(pageNumber);
PRIndirectReferencefirstPageRef= reader.getPageOrigRef(pageNumber);
reader.releasePage(pageNumber);
parentRef = (PRIndirectReference)firstPage.get(PdfName.PARENT);
parentRef = newPRIndirectReference(reader, parentRef.getNumber());
parent = (PdfDictionary)PdfReader.getPdfObject(parentRef);
PdfArraykids= (PdfArray)PdfReader.getPdfObject(parent.get(PdfName.KIDS), parent);
ArrayList<PdfObject> ar = kids.getArrayList();
intlen= ar.size();
intnum= firstPageRef.getNumber();
for (intk=0; k < len; ++k) {
PRIndirectReferencecur= (PRIndirectReference)ar.get(k);
if (num == cur.getNumber()) {
ar.add(k, pref);
break;
}
}
if (len == ar.size())
thrownewRuntimeException("Internal inconsistence.");
pdfStamper.stamper.markUsed(kids);
reader.pageRefs.insertPage(pageNumber, pref);
pdfStamper.stamper.correctAcroFieldPages(pageNumber);
}
page.put(PdfName.PARENT, parentRef);
while (parent != null) {
pdfStamper.stamper.markUsed(parent);
PdfNumbercount= (PdfNumber)PdfReader.getPdfObjectRelease(parent.get(PdfName.COUNT));
parent.put(PdfName.COUNT, newPdfNumber(count.intValue() + 1));
parent = (PdfDictionary)PdfReader.getPdfObject(parent.get(PdfName.PARENT));
}
}
//// helper methods///**
* This method recursively removes a given page from the given page tree.
*/staticbooleanremovePage(PdfStamper pdfStamper, PRIndirectReference pageTree, PRIndirectReference pageToRemove)
{
PdfDictionarypageDict= (PdfDictionary)PdfReader.getPdfObject(pageTree);
PdfArraykidsPR= (PdfArray)PdfReader.getPdfObject(pageDict.get(PdfName.KIDS));
if (kidsPR != null) {
ArrayList<PdfObject> kids = kidsPR.getArrayList();
booleanremoved=false;
for (intk=0; k < kids.size(); ++k){
PRIndirectReferenceobj= (PRIndirectReference)kids.get(k);
if (pageToRemove.getNumber() == obj.getNumber() && pageToRemove.getGeneration() == obj.getGeneration())
{
kids.remove(k);
pdfStamper.stamper.markUsed(pageTree);
removed = true;
break;
}
elseif (removePage(pdfStamper, (PRIndirectReference)obj, pageToRemove))
{
removed = true;
break;
}
}
if (removed)
{
PdfNumbercount= (PdfNumber) PdfReader.getPdfObjectRelease(pageDict.get(PdfName.COUNT));
pageDict.put(PdfName.COUNT, newPdfNumber(count.intValue() + 1));
pdfStamper.stamper.markUsed(pageTree);
returntrue;
}
}
returnfalse;
}
/**
* This method returns the uncompressed bytes of a content PDF object.
*/staticbyte[] pageContentsToArray(PdfReader reader, PdfObject contents, RandomAccessFileOrArray file) throws IOException{
if (contents == null)
returnnewbyte[0];
if (file == null)
file = reader.getSafeFile();
ByteArrayOutputStreambout=null;
if (contents.isStream()) {
return PdfReader.getStreamBytes((PRStream)contents, file);
}
elseif (contents.isArray()) {
PdfArrayarray= (PdfArray)contents;
ArrayList<PdfObject> list = array.getArrayList();
bout = newByteArrayOutputStream();
for (intk=0; k < list.size(); ++k) {
PdfObjectitem= PdfReader.getPdfObjectRelease(list.get(k));
if (item == null || !item.isStream())
continue;
byte[] b = PdfReader.getStreamBytes((PRStream)item, file);
bout.write(b);
if (k != list.size() - 1)
bout.write('\n');
}
return bout.toByteArray();
}
elsereturnnewbyte[0];
}
/**
* This method returns a PDF stream object containing a copy of the
* contents of the given template page with the given name.<br>
* To make Acrobat 9 happy with this template XObject when checking
* for signature validity, the /Size has to be changed to be the size
* of the stream that would have been generated by Acrobat itself
* when spawning the given template.
*/static PdfStream getFormXObject(PdfReader reader, PdfDictionary page, int compressionLevel, String name)throws IOException {
RectanglepageSize= reader.getPageSize(page);
finalPdfLiteralMATRIX=newPdfLiteral("[1 0 0 1 " + -getXOffset(pageSize) + " " + -getYOffset(pageSize) + "]");
PdfDictionarydic=newPdfDictionary();
dic.put(PdfName.RESOURCES, PdfReader.getPdfObjectRelease(page.get(PdfName.RESOURCES)));
dic.put(PdfName.TYPE, PdfName.XOBJECT);
dic.put(PdfName.SUBTYPE, PdfName.FORM);
dic.put(PdfName.BBOX, page.get(PdfName.MEDIABOX));
dic.put(PdfName.MATRIX, MATRIX);
dic.put(PdfName.FORMTYPE, PdfReaderInstance.ONE);
dic.put(PdfName.NAME, newPdfName(name));
PdfStream stream;
PdfObjectcontents= PdfReader.getPdfObjectRelease(page.get(PdfName.CONTENTS));
byte bout[] = null;
if (contents != null)
bout = pageContentsToArray(reader, contents, reader.getSafeFile());
elsebout=newbyte[0];
byte[] embedded = newbyte[bout.length + 4];
System.arraycopy(bout, 0, embedded, 2, bout.length);
embedded[0] = 'q';
embedded[1] = 10;
embedded[embedded.length - 2] = 'Q';
embedded[embedded.length - 1] = 10;
stream = newPdfStream(embedded);
stream.putAll(dic);
stream.flateCompress(compressionLevel);
PdfObjectfilter= stream.get(PdfName.FILTER);
if (filter != null && !(filter instanceof PdfArray))
stream.put(PdfName.FILTER, newPdfArray(filter));
return stream;
}
/**
* This method returns the content stream object for a spawned
* template.
*/static PdfStream getTemplateStream(String name, Rectangle pageSize)
{
intx= getXOffset(pageSize);
inty= getYOffset(pageSize);
Stringcontent="q 1 0 0 1 " + x + " " + y + " cm /" + name + " Do Q";
returnnewPdfStream(PdfEncodings.convertToBytes(content, null));
}
/**
* This method returns the center x offset for the given page rectangle.
*/staticintgetXOffset(Rectangle pageSize)
{
return Math.round((pageSize.getLeft() + pageSize.getRight()) / 2);
}
/**
* This method returns the center y offset for the given page rectangle.
*/staticintgetYOffset(Rectangle pageSize)
{
return Math.round((pageSize.getTop() + pageSize.getBottom()) / 2);
}
/**
* This method returns the /Names name dictionary of the document; if
* the document does not have one yet, it generates one.<br>
* Beware! If the document contains a name dictionary as an indirect
* object, the dictionary shall be written to but once; this /includes/
* writes by the {@link PdfStamper}.
*/static PdfDictionary getNameDictionary(PdfStamper pdfStamper)
{
PdfDictionarycatalog= pdfStamper.stamper.reader.getCatalog();
PdfDictionarynames= (PdfDictionary)PdfReader.getPdfObject(catalog.get(PdfName.NAMES), catalog);
if (names == null) {
names = newPdfDictionary();
catalog.put(PdfName.NAMES, names);
pdfStamper.stamper.markUsed(catalog);
}
return names;
}
finalstatic Map<PdfStamper, HashMap<String, PdfObject>> namedPagesByStamper = newHashMap<>();
static HashMap<String, PdfObject> getNamedPages(PdfStamper pdfStamper)throws DocumentException
{
if (namedPagesByStamper.containsKey(pdfStamper))
return namedPagesByStamper.get(pdfStamper);
finalPdfDictionarynameDictionary= getNameDictionary(pdfStamper);
PdfObjectpagesObject= PdfReader.getPdfObjectRelease(nameDictionary.get(PdfName.PAGES));
if (pagesObject != null && !(pagesObject instanceof PdfDictionary))
thrownewDocumentException("Pages name dictionary is neither a PdfDictionary nor null");
HashMap<String, PdfObject> namesMap = PdfNameTree.readTree((PdfDictionary)pagesObject);
namedPagesByStamper.put(pdfStamper, namesMap);
return namesMap;
}
staticvoidstoreNamedPages(PdfStamper pdfStamper)throws IOException
{
if (namedPagesByStamper.containsKey(pdfStamper))
{
final HashMap<String, PdfObject> pages = namedPagesByStamper.get(pdfStamper);
finalPdfDictionarynameDictionary= getNameDictionary(pdfStamper);
pdfStamper.stamper.markUsed(nameDictionary);
if (pages.isEmpty())
nameDictionary.remove(PdfName.PAGES);
else {
finalPdfDictionarytree= PdfNameTree.writeTree(pages, pdfStamper.stamper);
nameDictionary.put(PdfName.PAGES, pdfStamper.stamper.addToBody(tree).getIndirectReference());
}
}
}
finalstatic Map<PdfStamper, HashMap<String, PdfObject>> namedTemplatesByStamper = newHashMap<>();
static HashMap<String, PdfObject> getNamedTemplates(PdfStamper pdfStamper)throws DocumentException
{
if (namedTemplatesByStamper.containsKey(pdfStamper))
return namedTemplatesByStamper.get(pdfStamper);
finalPdfDictionarynameDictionary= getNameDictionary(pdfStamper);
PdfObjecttemplatesObject= PdfReader.getPdfObjectRelease(nameDictionary.get(TEMPLATES));
if (templatesObject != null && !(templatesObject instanceof PdfDictionary))
thrownewDocumentException("Templates name dictionary is neither a PdfDictionary nor null");
HashMap<String, PdfObject> templatesMap = PdfNameTree.readTree((PdfDictionary)templatesObject);
namedTemplatesByStamper.put(pdfStamper, templatesMap);
return templatesMap;
}
staticvoidstoreNamedTemplates(PdfStamper pdfStamper)throws IOException
{
if (namedTemplatesByStamper.containsKey(pdfStamper))
{
final HashMap<String, PdfObject> templates = namedTemplatesByStamper.get(pdfStamper);
finalPdfDictionarynameDictionary= getNameDictionary(pdfStamper);
pdfStamper.stamper.markUsed(nameDictionary);
if (templates.isEmpty())
nameDictionary.remove(TEMPLATES);
else {
finalPdfDictionarytree= PdfNameTree.writeTree(templates, pdfStamper.stamper);
nameDictionary.put(TEMPLATES, pdfStamper.stamper.addToBody(tree).getIndirectReference());
}
}
}
}
Using the helper class
The helper class assumes you already have a PDF and want to make some page in it a named page template or instantiate an existing named template.
You can make an existing page a named template like this:
PdfReaderpdfReader=newPdfReader(resource);
PdfStamperpdfStamper=newPdfStamper(pdfReader, target, '\0', true);
PdfStamperHelper.createTemplate(pdfStamper, "template", 1);
pdfStamper.close();
(BasicTemplating.java test testNameTest
)
The page does remain visible. If you don't want that, hide it using PdfStamperHelper.hideTemplate
after naming.
You can spawn an existing template like this:
pdfReader = new PdfReader(...);
pdfStamper = new PdfStamper(pdfReader, target, '\0', true);
PdfStamperHelper.spawnTemplate(pdfStamper, "template", 1);
pdfStamper.close();
(BasicTemplating.java test testNameSpawnTest
)
Issue in concert with Adobe Reader
I took a PDF, created a named page template in it, and signed that PDF.
Then I spawned the named template using the code above, cf. BasicTemplating.java test testSpawnPdfaNamedSigned
, and then inspected the result in Adobe Acrobat Reader DC, I unfortunately saw
My older Acrobat Pro 9.5 after pressing "Compute Modifications List" is even aware that only a page template has been instantiated but still calls the signature INVALID:
Experiments showed that Adobe Acrobat Reader executes one test that does not make any sense in the light of the PDF specification: It expects the page template form xobject to have the same Size entry value after compression (!!) as if it was compressed by the Reader itself. As different implementations of the deflate compression can result in different stream sizes (iText's implementation in the case at hand created a stream 3 bytes shorter), I have no idea yet how to generically pass this test.
After patching the specific stream's Size entry in the PDF generated above from 159 to 162, Adobe Acrobat Reader shows:
(The validity is unknown because revocation information had not been added in time.)
Post a Comment for "Need To Add A New Page To A Pdf Document That Already Has A Digital Signature"