Необходимо добавить новую страницу в PDF-документ, который уже имеет цифровую подпись
Мне нужно добавить новую страницу, когда на последней странице документа больше нет свободного места. Я видел цифровую книгу жестов itext, и там говорится, что я не могу просто использовать метод insertPage(), и вот как я Сделайте сейчас, чтобы цифровые подписи были сломаны, как говорится в книге.
ПРИМЕЧАНИЕ. Помните, что "действия по добавлению страницы разрешены" не означает, что вы можете использовать метод insertPage(). Это сообщение относится к созданию экземпляра шаблона страницы, как описано в Справочном руководстве JavaScript Adobe Acrobat, которое выходит за рамки данной статьи.
но я не могу найти, как добавить новую страницу с помощью javascript и itext, у кого-то из вас есть такая же проблема, которая может помочь мне, мне действительно нужна новая страница без признаков поломки
я не могу найти код java-скрипта и интегрировать его с itext, я обнаружил, что это не работает:
String js = "var aTemplates = this.templates;"
+ "aTemplates[0].spawn({nPage: 0, bRename: true, bOverlay: false});";
var a = this.getTemplate("MyTemplate");
a.spawn (this.pageNums);
и этот
//get the array of the template object for the PDF;
var aTemplates = this.templates;
// create a new page from the first template placing it at the end of the PDF and renaming the fields;
// rename the fields, do not overlay;
aTemplates[0].spawn({nPage: 0, bRename: true, bOverlay: false});
тогда я использую itext
эти два разных способа использования javascript, но он не работает, не добавляя новую страницу в конце документа.
PdfAction.javaScript (js, stamper.getWriter ());
stamper.addJavaScript (JS);
1 ответ
Этот ответ показывает 90% решения проблемы вплоть до проблемы, упомянутой в моих комментариях к исходному вопросу.
Вспомогательный класс для обработки шаблона страницы
Несколько лет назад, когда Adobe Reader начал считать подпись неработающей, как только в PDF были добавлены дополнительные страницы с новым содержанием, я экспериментировал с созданием шаблона страницы. Как оказалось, этот код можно легко адаптировать к текущим версиям 5.5.x iText (и дополнительно к дженерикам Java). Я еще не пробовал адаптацию к iText 7.
Из-за ограниченной видимости методов iText API, используемых здесь, этот класс должен быть включен в пакет com.itextpdf.text.pdf
, В качестве альтернативы этот класс может быть изменен для значительного использования магии отражения.
public class PdfStamperHelper
{
public static final PdfName TEMPLATES = new PdfName("Templates");
public static final PdfName TEMPLATE = new PdfName("Template");
public static final PdfName TEMPLATE_INSTANTIATED = new PdfName("TemplateInstantiated");
/**
* This method names a given page. The page in question already has
* to exist in the original document the given PdfStamper works on.
*/
public static void createTemplate(PdfStamper pdfStamper, String name, int page) throws IOException, DocumentException
{
PdfDictionary pageDic = 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.
*/
public static void hideTemplate(PdfStamper pdfStamper, String name) throws IOException, DocumentException
{
HashMap<String, PdfObject> namedPages = getNamedPages(pdfStamper);
PdfObject object = namedPages.get(name);
if (object == null)
throw new DocumentException("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
}
PdfDictionary pageDict = (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.
*/
public static PdfDictionary getTemplate(PdfStamper pdfStamper, String name) throws DocumentException
{
HashMap<String, PdfObject> namedTemplates = getNamedTemplates(pdfStamper);
PdfObject object = (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.
*/
public static void spawnTemplate(PdfStamper pdfStamper, String name, int pageNumber) throws DocumentException, IOException
{
PdfDictionary template = getTemplate(pdfStamper, name);
if (template == null)
throw new DocumentException("Document contains no template " + name + '.');
PdfReader reader = pdfStamper.stamper.reader;
// contRef: reference to the content stream of the spawned page;
// it only inserts the template XObject
PRIndirectReference contRef = 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 content
PdfDictionary xobjDict = new PdfDictionary();
xobjDict.put(new PdfName(name), reader.addPdfObject(getFormXObject(reader, template, pdfStamper.stamper.getCompressionLevel(), name)));
PdfDictionary resources = new PdfDictionary();
resources.put(PdfName.XOBJECT, xobjDict);
PRIndirectReference resRef = reader.addPdfObject(resources);
// page: dictionary of the spawned template page
PdfDictionary page = new PdfDictionary();
page.put(PdfName.TYPE, PdfName.PAGE); // not PdfName.TEMPLATE!
page.put(TEMPLATE_INSTANTIATED, new PdfName(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 treated
PRIndirectReference pref = reader.addPdfObject(page);
PdfDictionary parent;
PRIndirectReference parentRef;
if (pageNumber > reader.getNumberOfPages()) {
PdfDictionary lastPage = reader.getPageNRelease(reader.getNumberOfPages());
parentRef = (PRIndirectReference)lastPage.get(PdfName.PARENT);
parentRef = new PRIndirectReference(reader, parentRef.getNumber());
parent = (PdfDictionary)PdfReader.getPdfObject(parentRef);
PdfArray kids = (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;
PdfDictionary firstPage = reader.getPageN(pageNumber);
PRIndirectReference firstPageRef = reader.getPageOrigRef(pageNumber);
reader.releasePage(pageNumber);
parentRef = (PRIndirectReference)firstPage.get(PdfName.PARENT);
parentRef = new PRIndirectReference(reader, parentRef.getNumber());
parent = (PdfDictionary)PdfReader.getPdfObject(parentRef);
PdfArray kids = (PdfArray)PdfReader.getPdfObject(parent.get(PdfName.KIDS), parent);
ArrayList<PdfObject> ar = kids.getArrayList();
int len = ar.size();
int num = firstPageRef.getNumber();
for (int k = 0; k < len; ++k) {
PRIndirectReference cur = (PRIndirectReference)ar.get(k);
if (num == cur.getNumber()) {
ar.add(k, pref);
break;
}
}
if (len == ar.size())
throw new RuntimeException("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);
PdfNumber count = (PdfNumber)PdfReader.getPdfObjectRelease(parent.get(PdfName.COUNT));
parent.put(PdfName.COUNT, new PdfNumber(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.
*/
static boolean removePage(PdfStamper pdfStamper, PRIndirectReference pageTree, PRIndirectReference pageToRemove)
{
PdfDictionary pageDict = (PdfDictionary)PdfReader.getPdfObject(pageTree);
PdfArray kidsPR = (PdfArray)PdfReader.getPdfObject(pageDict.get(PdfName.KIDS));
if (kidsPR != null) {
ArrayList<PdfObject> kids = kidsPR.getArrayList();
boolean removed = false;
for (int k = 0; k < kids.size(); ++k){
PRIndirectReference obj = (PRIndirectReference)kids.get(k);
if (pageToRemove.getNumber() == obj.getNumber() && pageToRemove.getGeneration() == obj.getGeneration())
{
kids.remove(k);
pdfStamper.stamper.markUsed(pageTree);
removed = true;
break;
}
else if (removePage(pdfStamper, (PRIndirectReference)obj, pageToRemove))
{
removed = true;
break;
}
}
if (removed)
{
PdfNumber count = (PdfNumber) PdfReader.getPdfObjectRelease(pageDict.get(PdfName.COUNT));
pageDict.put(PdfName.COUNT, new PdfNumber(count.intValue() + 1));
pdfStamper.stamper.markUsed(pageTree);
return true;
}
}
return false;
}
/**
* This method returns the uncompressed bytes of a content PDF object.
*/
static byte[] pageContentsToArray(PdfReader reader, PdfObject contents, RandomAccessFileOrArray file) throws IOException{
if (contents == null)
return new byte[0];
if (file == null)
file = reader.getSafeFile();
ByteArrayOutputStream bout = null;
if (contents.isStream()) {
return PdfReader.getStreamBytes((PRStream)contents, file);
}
else if (contents.isArray()) {
PdfArray array = (PdfArray)contents;
ArrayList<PdfObject> list = array.getArrayList();
bout = new ByteArrayOutputStream();
for (int k = 0; k < list.size(); ++k) {
PdfObject item = 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();
}
else
return new byte[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 {
Rectangle pageSize = reader.getPageSize(page);
final PdfLiteral MATRIX = new PdfLiteral("[1 0 0 1 " + -getXOffset(pageSize) + " " + -getYOffset(pageSize) + "]");
PdfDictionary dic = new PdfDictionary();
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, new PdfName(name));
PdfStream stream;
PdfObject contents = PdfReader.getPdfObjectRelease(page.get(PdfName.CONTENTS));
byte bout[] = null;
if (contents != null)
bout = pageContentsToArray(reader, contents, reader.getSafeFile());
else
bout = new byte[0];
byte[] embedded = new byte[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 = new PdfStream(embedded);
stream.putAll(dic);
stream.flateCompress(compressionLevel);
PdfObject filter = stream.get(PdfName.FILTER);
if (filter != null && !(filter instanceof PdfArray))
stream.put(PdfName.FILTER, new PdfArray(filter));
return stream;
}
/**
* This method returns the content stream object for a spawned
* template.
*/
static PdfStream getTemplateStream(String name, Rectangle pageSize)
{
int x = getXOffset(pageSize);
int y = getYOffset(pageSize);
String content = "q 1 0 0 1 " + x + " " + y + " cm /" + name + " Do Q";
return new PdfStream(PdfEncodings.convertToBytes(content, null));
}
/**
* This method returns the center x offset for the given page rectangle.
*/
static int getXOffset(Rectangle pageSize)
{
return Math.round((pageSize.getLeft() + pageSize.getRight()) / 2);
}
/**
* This method returns the center y offset for the given page rectangle.
*/
static int getYOffset(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)
{
PdfDictionary catalog = pdfStamper.stamper.reader.getCatalog();
PdfDictionary names = (PdfDictionary)PdfReader.getPdfObject(catalog.get(PdfName.NAMES), catalog);
if (names == null) {
names = new PdfDictionary();
catalog.put(PdfName.NAMES, names);
pdfStamper.stamper.markUsed(catalog);
}
return names;
}
final static Map<PdfStamper, HashMap<String, PdfObject>> namedPagesByStamper = new HashMap<>();
static HashMap<String, PdfObject> getNamedPages(PdfStamper pdfStamper) throws DocumentException
{
if (namedPagesByStamper.containsKey(pdfStamper))
return namedPagesByStamper.get(pdfStamper);
final PdfDictionary nameDictionary = getNameDictionary(pdfStamper);
PdfObject pagesObject = PdfReader.getPdfObjectRelease(nameDictionary.get(PdfName.PAGES));
if (pagesObject != null && !(pagesObject instanceof PdfDictionary))
throw new DocumentException("Pages name dictionary is neither a PdfDictionary nor null");
HashMap<String, PdfObject> namesMap = PdfNameTree.readTree((PdfDictionary)pagesObject);
namedPagesByStamper.put(pdfStamper, namesMap);
return namesMap;
}
static void storeNamedPages(PdfStamper pdfStamper) throws IOException
{
if (namedPagesByStamper.containsKey(pdfStamper))
{
final HashMap<String, PdfObject> pages = namedPagesByStamper.get(pdfStamper);
final PdfDictionary nameDictionary = getNameDictionary(pdfStamper);
pdfStamper.stamper.markUsed(nameDictionary);
if (pages.isEmpty())
nameDictionary.remove(PdfName.PAGES);
else {
final PdfDictionary tree = PdfNameTree.writeTree(pages, pdfStamper.stamper);
nameDictionary.put(PdfName.PAGES, pdfStamper.stamper.addToBody(tree).getIndirectReference());
}
}
}
final static Map<PdfStamper, HashMap<String, PdfObject>> namedTemplatesByStamper = new HashMap<>();
static HashMap<String, PdfObject> getNamedTemplates(PdfStamper pdfStamper) throws DocumentException
{
if (namedTemplatesByStamper.containsKey(pdfStamper))
return namedTemplatesByStamper.get(pdfStamper);
final PdfDictionary nameDictionary = getNameDictionary(pdfStamper);
PdfObject templatesObject = PdfReader.getPdfObjectRelease(nameDictionary.get(TEMPLATES));
if (templatesObject != null && !(templatesObject instanceof PdfDictionary))
throw new DocumentException("Templates name dictionary is neither a PdfDictionary nor null");
HashMap<String, PdfObject> templatesMap = PdfNameTree.readTree((PdfDictionary)templatesObject);
namedTemplatesByStamper.put(pdfStamper, templatesMap);
return templatesMap;
}
static void storeNamedTemplates(PdfStamper pdfStamper) throws IOException
{
if (namedTemplatesByStamper.containsKey(pdfStamper))
{
final HashMap<String, PdfObject> templates = namedTemplatesByStamper.get(pdfStamper);
final PdfDictionary nameDictionary = getNameDictionary(pdfStamper);
pdfStamper.stamper.markUsed(nameDictionary);
if (templates.isEmpty())
nameDictionary.remove(TEMPLATES);
else {
final PdfDictionary tree = PdfNameTree.writeTree(templates, pdfStamper.stamper);
nameDictionary.put(TEMPLATES, pdfStamper.stamper.addToBody(tree).getIndirectReference());
}
}
}
}
Использование вспомогательного класса
Вспомогательный класс предполагает, что у вас уже есть PDF, и вы хотите сделать какую-то страницу в нем именованным шаблоном страницы или создать экземпляр существующего именованного шаблона.
Вы можете сделать существующую страницу именованным шаблоном следующим образом:
PdfReader pdfReader = new PdfReader(resource);
PdfStamper pdfStamper = new PdfStamper(pdfReader, target, '\0', true);
PdfStamperHelper.createTemplate(pdfStamper, "template", 1);
pdfStamper.close();
( BasicTemplating.java test testNameTest
)
Страница остается видимой. Если вы не хотите этого, спрячьте это, используя PdfStamperHelper.hideTemplate
после именования
Вы можете создать существующий шаблон следующим образом:
pdfReader = new PdfReader(...);
pdfStamper = new PdfStamper(pdfReader, target, '\0', true);
PdfStamperHelper.spawnTemplate(pdfStamper, "template", 1);
pdfStamper.close();
( BasicTemplating.java test testNameSpawnTest
)
Выпуск согласован с Adobe Reader
Я взял PDF, создал именованный шаблон страницы и подписал этот PDF.
Затем я породил названный шаблон, используя код выше, ср. BasicTemplating.java test testSpawnPdfaNamedSigned
, а потом проверил результат в Adobe Acrobat Reader DC, я к сожалению увидел
Мой старый Acrobat Pro 9.5 после нажатия "Compute Modification List" даже знает, что был создан только шаблон страницы, но все равно вызывает подпись INVALID:
Эксперименты показали, что Adobe Acrobat Reader выполняет один тест, который не имеет никакого смысла в свете спецификации PDF: он ожидает, что шаблон страницы xobject будет иметь такое же значение записи размера после сжатия (!!), как если бы оно было сжато Читатель сам. Поскольку разные реализации сжатия с раздувом могут привести к разным размерам потока (реализация iText в данном случае создала поток на 3 байта короче), я пока не знаю, как в общем пройти этот тест.
После исправления записи Размер определенного потока в PDF, сгенерированной выше со 159 по 162, Adobe Acrobat Reader показывает:
(Срок действия неизвестен, поскольку информация об отзыве не была добавлена вовремя.)