com.itextpdf.kernel.pdf.PdfDocument Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2023 iText Group NV
Authors: Bruno Lowagie, Paulo Soares, et al.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License version 3
as published by the Free Software Foundation with the addition of the
following permission added to Section 15 as permitted in Section 7(a):
FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
OF THIRD PARTY RIGHTS
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses or write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA, 02110-1301 USA, or download the license from the following URL:
http://itextpdf.com/terms-of-use/
The interactive user interfaces in modified source and object code versions
of this program must display Appropriate Legal Notices, as required under
Section 5 of the GNU Affero General Public License.
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the producer line in every PDF that is created
or manipulated using iText.
You can be released from the requirements of the license by purchasing
a commercial license. Buying such a license is mandatory as soon as you
develop commercial activities involving the iText software without
disclosing the source code of your own applications.
These activities include: offering paid services to customers as an ASP,
serving PDFs on the fly in a web application, shipping iText with a closed
source product.
For more information, please contact iText Software Corp. at this
address: [email protected]
*/
package com.itextpdf.kernel.pdf;
import com.itextpdf.commons.actions.EventManager;
import com.itextpdf.commons.actions.confirmations.ConfirmEvent;
import com.itextpdf.commons.actions.confirmations.EventConfirmationType;
import com.itextpdf.commons.actions.data.ProductData;
import com.itextpdf.commons.actions.sequence.SequenceId;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.ByteUtils;
import com.itextpdf.io.source.RandomAccessFileOrArray;
import com.itextpdf.kernel.actions.data.ITextCoreProductData;
import com.itextpdf.kernel.actions.events.FlushPdfDocumentEvent;
import com.itextpdf.kernel.actions.events.ITextCoreProductEvent;
import com.itextpdf.kernel.colors.Color;
import com.itextpdf.kernel.events.EventDispatcher;
import com.itextpdf.kernel.events.IEventDispatcher;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.exceptions.BadPasswordException;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.numbering.EnglishAlphabetNumbering;
import com.itextpdf.kernel.numbering.RomanNumbering;
import com.itextpdf.kernel.pdf.PdfReader.StrictnessLevel;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
import com.itextpdf.kernel.pdf.canvas.CanvasGraphicsState;
import com.itextpdf.kernel.pdf.collection.PdfCollection;
import com.itextpdf.kernel.pdf.filespec.PdfEncryptedPayloadFileSpecFactory;
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
import com.itextpdf.kernel.pdf.navigation.PdfDestination;
import com.itextpdf.kernel.pdf.statistics.NumberOfPagesStatisticsEvent;
import com.itextpdf.kernel.pdf.statistics.SizeOfPdfStatisticsEvent;
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
import com.itextpdf.kernel.xmp.PdfConst;
import com.itextpdf.kernel.xmp.XMPConst;
import com.itextpdf.kernel.xmp.XMPException;
import com.itextpdf.kernel.xmp.XMPMeta;
import com.itextpdf.kernel.xmp.XMPMetaFactory;
import com.itextpdf.kernel.xmp.options.PropertyOptions;
import com.itextpdf.kernel.xmp.options.SerializeOptions;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main enter point to work with PDF document.
*/
public class PdfDocument implements IEventDispatcher, Closeable {
private static final IPdfPageFactory pdfPageFactory = new PdfPageFactory();
protected final StampingProperties properties;
/**
* List of indirect objects used in the document.
*/
final PdfXrefTable xref = new PdfXrefTable();
private final Map documentFonts = new HashMap<>();
private final SequenceId documentId;
/**
* To be adjusted destinations.
* Key - originating page on the source document
* Value - a hashmap of Parent pdf objects and destinations to be updated
*/
private final List pendingDestinationMutations =
new ArrayList();
protected EventDispatcher eventDispatcher = new EventDispatcher();
/**
* PdfWriter associated with the document.
* Not null if document opened either in writing or stamping mode.
*/
protected PdfWriter writer = null;
/**
* PdfReader associated with the document.
* Not null if document is opened either in reading or stamping mode.
*/
protected PdfReader reader = null;
/**
* XMP Metadata for the document.
*/
protected byte[] xmpMetadata = null;
/**
* Document catalog.
*/
protected PdfCatalog catalog = null;
/**
* Document trailed.
*/
protected PdfDictionary trailer = null;
/**
* Document info.
*/
protected PdfDocumentInfo info = null;
/**
* Document version.
*/
protected PdfVersion pdfVersion = PdfVersion.PDF_1_7;
protected FingerPrint fingerPrint;
protected SerializeOptions serializeOptions = new SerializeOptions();
protected PdfStructTreeRoot structTreeRoot;
protected int structParentIndex = -1;
protected boolean closeReader = true;
protected boolean closeWriter = true;
protected boolean isClosing = false;
protected boolean closed = false;
/**
* flag determines whether to write unused objects to result document
*/
protected boolean flushUnusedObjects = false;
protected TagStructureContext tagStructureContext;
/**
* Cache of already serialized objects from this document for smart mode.
*/
Map serializedObjectsCache = new HashMap<>();
/**
* Handler which will be used for decompression of pdf streams.
*/
MemoryLimitsAwareHandler memoryLimitsAwareHandler = null;
/**
* Default page size.
* New page by default will be created with this size.
*/
private PageSize defaultPageSize = PageSize.DEFAULT;
/**
* The original (first) id when the document is read initially.
*/
private PdfString originalDocumentId;
/**
* The original modified (second) id when the document is read initially.
*/
private PdfString modifiedDocumentId;
private PdfFont defaultFont = null;
private EncryptedEmbeddedStreamsHandler encryptedEmbeddedStreamsHandler;
/**
* Open PDF document in reading mode.
*
* @param reader PDF reader.
*/
public PdfDocument(PdfReader reader) {
this(reader, new DocumentProperties());
}
/**
* Open PDF document in reading mode.
*
* @param reader PDF reader.
* @param properties document properties
*/
public PdfDocument(PdfReader reader, DocumentProperties properties) {
if (reader == null) {
throw new IllegalArgumentException("The reader in PdfDocument constructor can not be null.");
}
documentId = new SequenceId();
this.reader = reader;
// default values of the StampingProperties doesn't affect anything
this.properties = new StampingProperties();
this.properties.setEventCountingMetaInfo(properties.metaInfo);
open(null);
}
/**
* Open PDF document in writing mode.
* Document has no pages when initialized.
*
* @param writer PDF writer
*/
public PdfDocument(PdfWriter writer) {
this(writer, new DocumentProperties());
}
/**
* Open PDF document in writing mode.
* Document has no pages when initialized.
*
* @param writer PDF writer
* @param properties document properties
*/
public PdfDocument(PdfWriter writer, DocumentProperties properties) {
if (writer == null) {
throw new IllegalArgumentException("The writer in PdfDocument constructor can not be null.");
}
documentId = new SequenceId();
this.writer = writer;
// default values of the StampingProperties doesn't affect anything
this.properties = new StampingProperties();
this.properties.setEventCountingMetaInfo(properties.metaInfo);
open(writer.properties.pdfVersion);
}
/**
* Opens PDF document in the stamping mode.
*
*
* @param reader PDF reader.
* @param writer PDF writer.
*/
public PdfDocument(PdfReader reader, PdfWriter writer) {
this(reader, writer, new StampingProperties());
}
/**
* Open PDF document in stamping mode.
*
* @param reader PDF reader.
* @param writer PDF writer.
* @param properties properties of the stamping process
*/
public PdfDocument(PdfReader reader, PdfWriter writer, StampingProperties properties) {
if (reader == null) {
throw new IllegalArgumentException("The reader in PdfDocument constructor can not be null.");
}
if (writer == null) {
throw new IllegalArgumentException("The writer in PdfDocument constructor can not be null.");
}
documentId = new SequenceId();
this.reader = reader;
this.writer = writer;
this.properties = properties;
boolean writerHasEncryption = writerHasEncryption();
if (properties.appendMode && writerHasEncryption) {
Logger logger = LoggerFactory.getLogger(PdfDocument.class);
logger.warn(IoLogMessageConstant.WRITER_ENCRYPTION_IS_IGNORED_APPEND);
}
if (properties.preserveEncryption && writerHasEncryption) {
Logger logger = LoggerFactory.getLogger(PdfDocument.class);
logger.warn(IoLogMessageConstant.WRITER_ENCRYPTION_IS_IGNORED_PRESERVE);
}
open(writer.properties.pdfVersion);
}
/**
* Sets the XMP Metadata.
*
* @param xmpMeta the xmpMetadata to set
* @param serializeOptions serialization options
*
* @throws XMPException on serialization errors
*/
public void setXmpMetadata(XMPMeta xmpMeta, SerializeOptions serializeOptions) throws XMPException {
this.serializeOptions = serializeOptions;
setXmpMetadata(XMPMetaFactory.serializeToBuffer(xmpMeta, serializeOptions));
}
/**
* Use this method to set the XMP Metadata.
*
* @param xmpMetadata The xmpMetadata to set.
*/
protected void setXmpMetadata(byte[] xmpMetadata) {
this.xmpMetadata = xmpMetadata;
}
/**
* Sets the XMP Metadata.
*
* @param xmpMeta the xmpMetadata to set
*
* @throws XMPException on serialization errors
*/
public void setXmpMetadata(XMPMeta xmpMeta) throws XMPException {
serializeOptions.setPadding(2000);
setXmpMetadata(xmpMeta, serializeOptions);
}
/**
* Gets XMPMetadata.
*
* @return the XMPMetadata
*/
public byte[] getXmpMetadata() {
return getXmpMetadata(false);
}
/**
* Gets XMPMetadata or create a new one.
*
* @param createNew if true, create a new empty XMPMetadata if it did not present.
*
* @return existed or newly created XMPMetadata byte array.
*/
public byte[] getXmpMetadata(boolean createNew) {
if (xmpMetadata == null && createNew) {
XMPMeta xmpMeta = XMPMetaFactory.create();
xmpMeta.setObjectName(XMPConst.TAG_XMPMETA);
xmpMeta.setObjectName("");
addCustomMetadataExtensions(xmpMeta);
try {
xmpMeta.setProperty(XMPConst.NS_DC, PdfConst.Format, "application/pdf");
setXmpMetadata(xmpMeta);
} catch (XMPException ignored) {
}
}
return xmpMetadata;
}
/**
* Gets PdfObject by object number.
*
* @param objNum object number.
*
* @return {@link PdfObject} or {@code null}, if object not found.
*/
public PdfObject getPdfObject(int objNum) {
checkClosingStatus();
PdfIndirectReference reference = xref.get(objNum);
if (reference == null) {
return null;
} else {
return reference.getRefersTo();
}
}
/**
* Get number of indirect objects in the document.
*
* @return number of indirect objects.
*/
public int getNumberOfPdfObjects() {
return xref.size();
}
/**
* Gets the page by page number.
*
* @param pageNum page number.
*
* @return page by page number.
*
* @throws PdfException in case the page tree is broken
*/
public PdfPage getPage(int pageNum) {
checkClosingStatus();
return catalog.getPageTree().getPage(pageNum);
}
/**
* Gets the {@link PdfPage} instance by {@link PdfDictionary}.
*
* @param pageDictionary {@link PdfDictionary} that present page.
*
* @return page by {@link PdfDictionary}.
*/
public PdfPage getPage(PdfDictionary pageDictionary) {
checkClosingStatus();
return catalog.getPageTree().getPage(pageDictionary);
}
/**
* Get the first page of the document.
*
* @return first page of the document.
*/
public PdfPage getFirstPage() {
checkClosingStatus();
return getPage(1);
}
/**
* Gets the last page of the document.
*
* @return last page.
*/
public PdfPage getLastPage() {
return getPage(getNumberOfPages());
}
/**
* Marks {@link PdfStream} object as embedded file stream. Note that this method is for internal usage.
* To add an embedded file to the PDF document please use specialized API for file attachments.
* (e.g. {@link PdfDocument#addFileAttachment(String, PdfFileSpec)}, {@link PdfPage#addAnnotation(PdfAnnotation)})
*
* @param stream to be marked as embedded file stream
*/
public void markStreamAsEmbeddedFile(PdfStream stream) {
encryptedEmbeddedStreamsHandler.storeEmbeddedStream(stream);
}
/**
* Creates and adds new page to the end of document.
*
* @return added page
*/
public PdfPage addNewPage() {
return addNewPage(getDefaultPageSize());
}
/**
* Creates and adds new page with the specified page size.
*
* @param pageSize page size of the new page
*
* @return added page
*/
public PdfPage addNewPage(PageSize pageSize) {
checkClosingStatus();
PdfPage page = getPageFactory().createPdfPage(this, pageSize);
checkAndAddPage(page);
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.START_PAGE, page));
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
return page;
}
/**
* Creates and inserts new page to the document.
*
* @param index position to addPage page to
*
* @return inserted page
*
* @throws PdfException in case {@code page} is flushed
*/
public PdfPage addNewPage(int index) {
return addNewPage(index, getDefaultPageSize());
}
/**
* Creates and inserts new page to the document.
*
* @param index position to addPage page to
* @param pageSize page size of the new page
*
* @return inserted page
*
* @throws PdfException in case {@code page} is flushed
*/
public PdfPage addNewPage(int index, PageSize pageSize) {
checkClosingStatus();
PdfPage page = getPageFactory().createPdfPage(this, pageSize);
checkAndAddPage(index, page);
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.START_PAGE, page));
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
return page;
}
/**
* Adds page to the end of document.
*
* @param page page to add.
*
* @return added page.
*
* @throws PdfException in case {@code page} is flushed
*/
public PdfPage addPage(PdfPage page) {
checkClosingStatus();
checkAndAddPage(page);
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
return page;
}
/**
* Inserts page to the document.
*
* @param index position to addPage page to
* @param page page to addPage
*
* @return inserted page
*
* @throws PdfException in case {@code page} is flushed
*/
public PdfPage addPage(int index, PdfPage page) {
checkClosingStatus();
checkAndAddPage(index, page);
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
return page;
}
/**
* Gets number of pages of the document.
*
* @return number of pages.
*/
public int getNumberOfPages() {
checkClosingStatus();
return catalog.getPageTree().getNumberOfPages();
}
/**
* Gets page number by page.
*
* @param page the page.
*
* @return page number.
*/
public int getPageNumber(PdfPage page) {
checkClosingStatus();
return catalog.getPageTree().getPageNumber(page);
}
/**
* Gets page number by {@link PdfDictionary}.
*
* @param pageDictionary {@link PdfDictionary} that present page.
*
* @return page number by {@link PdfDictionary}.
*/
public int getPageNumber(PdfDictionary pageDictionary) {
return catalog.getPageTree().getPageNumber(pageDictionary);
}
/**
* Moves page to new place in same document with all it tag structure
*
* @param page page to be moved in document if present
* @param insertBefore indicates before which page new one will be inserted to
*
* @return true if this document contained the specified page
*/
public boolean movePage(PdfPage page, int insertBefore) {
checkClosingStatus();
int pageNum = getPageNumber(page);
if (pageNum > 0) {
movePage(pageNum, insertBefore);
return true;
}
return false;
}
/**
* Moves page to new place in same document with all it tag structure
*
* @param pageNumber number of Page that will be moved
* @param insertBefore indicates before which page new one will be inserted to
*/
public void movePage(int pageNumber, int insertBefore) {
checkClosingStatus();
if (insertBefore < 1 || insertBefore > getNumberOfPages() + 1) {
throw new IndexOutOfBoundsException(
MessageFormatUtil.format(KernelExceptionMessageConstant.REQUESTED_PAGE_NUMBER_IS_OUT_OF_BOUNDS,
insertBefore));
}
PdfPage page = getPage(pageNumber);
if (isTagged()) {
getStructTreeRoot().move(page, insertBefore);
getTagStructureContext().normalizeDocumentRootTag();
}
PdfPage removedPage = catalog.getPageTree().removePage(pageNumber);
if (insertBefore > pageNumber) {
--insertBefore;
}
catalog.getPageTree().addPage(insertBefore, removedPage);
}
/**
* Removes the first occurrence of the specified page from this document,
* if it is present. Returns true if this document
* contained the specified element (or equivalently, if this document
* changed as a result of the call).
*
* @param page page to be removed from this document, if present
*
* @return true if this document contained the specified page
*/
public boolean removePage(PdfPage page) {
checkClosingStatus();
int pageNum = getPageNumber(page);
if (pageNum >= 1) {
removePage(pageNum);
return true;
}
return false;
}
/**
* Removes page from the document by page number.
*
* @param pageNum the one-based index of the PdfPage to be removed
*/
public void removePage(int pageNum) {
checkClosingStatus();
PdfPage removedPage = getPage(pageNum);
if (removedPage != null && removedPage.isFlushed() && (isTagged() || hasAcroForm())) {
throw new PdfException(KernelExceptionMessageConstant.FLUSHED_PAGE_CANNOT_BE_REMOVED);
}
if (removedPage != null) {
catalog.removeOutlines(removedPage);
removeUnusedWidgetsFromFields(removedPage);
if (isTagged()) {
getTagStructureContext().removePageTags(removedPage);
}
if (!removedPage.isFlushed()) {
removedPage.getPdfObject().remove(PdfName.Parent);
removedPage.getPdfObject().getIndirectReference().setFree();
}
dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.REMOVE_PAGE, removedPage));
}
catalog.getPageTree().removePage(pageNum);
}
/**
* Gets document information dictionary.
* {@link PdfDocument#info} is lazy initialized. It will be initialized during the first call of this method.
*
* @return document information dictionary.
*/
public PdfDocumentInfo getDocumentInfo() {
checkClosingStatus();
if (info == null) {
PdfObject infoDict = trailer.get(PdfName.Info);
info = new PdfDocumentInfo(
infoDict instanceof PdfDictionary ? (PdfDictionary) infoDict : new PdfDictionary(), this);
XmpMetaInfoConverter.appendMetadataToInfo(xmpMetadata, info);
}
return info;
}
/**
* Gets original document id
*
* In order to set originalDocumentId {@link WriterProperties#setInitialDocumentId} should be used
*
* @return original dccument id
*/
public PdfString getOriginalDocumentId() {
return originalDocumentId;
}
/**
* Gets modified document id
*
* In order to set modifiedDocumentId {@link WriterProperties#setModifiedDocumentId} should be used
*
* @return modified document id
*/
public PdfString getModifiedDocumentId() {
return modifiedDocumentId;
}
/**
* Gets default page size.
* New pages by default are created with this size.
*
* @return default page size
*/
public PageSize getDefaultPageSize() {
return defaultPageSize;
}
/**
* Sets default page size.
* New pages by default will be created with this size.
*
* @param pageSize page size to be set as default
*/
public void setDefaultPageSize(PageSize pageSize) {
defaultPageSize = pageSize;
}
/**
* {@inheritDoc}
*/
@Override
public void addEventHandler(String type, com.itextpdf.kernel.events.IEventHandler handler) {
eventDispatcher.addEventHandler(type, handler);
}
/**
* {@inheritDoc}
*/
@Override
public void dispatchEvent(com.itextpdf.kernel.events.Event event) {
eventDispatcher.dispatchEvent(event);
}
/**
* {@inheritDoc}
*/
@Override
public void dispatchEvent(com.itextpdf.kernel.events.Event event, boolean delayed) {
eventDispatcher.dispatchEvent(event, delayed);
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasEventHandler(String type) {
return eventDispatcher.hasEventHandler(type);
}
/**
* {@inheritDoc}
*/
@Override
public void removeEventHandler(String type, com.itextpdf.kernel.events.IEventHandler handler) {
eventDispatcher.removeEventHandler(type, handler);
}
/**
* {@inheritDoc}
*/
@Override
public void removeAllHandlers() {
eventDispatcher.removeAllHandlers();
}
/**
* Gets {@code PdfWriter} associated with the document.
*
* @return PdfWriter associated with the document.
*/
public PdfWriter getWriter() {
checkClosingStatus();
return writer;
}
/**
* Gets {@code PdfReader} associated with the document.
*
* @return PdfReader associated with the document.
*/
public PdfReader getReader() {
checkClosingStatus();
return reader;
}
/**
* Returns {@code true} if the document is opened in append mode, and {@code false} otherwise.
*
* @return {@code true} if the document is opened in append mode, and {@code false} otherwise.
*/
public boolean isAppendMode() {
checkClosingStatus();
return properties.appendMode;
}
/**
* Creates next available indirect reference.
*
* @return created indirect reference.
*/
public PdfIndirectReference createNextIndirectReference() {
checkClosingStatus();
return xref.createNextIndirectReference(this);
}
/**
* Gets PDF version.
*
* @return PDF version.
*/
public PdfVersion getPdfVersion() {
return pdfVersion;
}
/**
* Gets PDF catalog.
*
* @return PDF catalog.
*/
public PdfCatalog getCatalog() {
checkClosingStatus();
return catalog;
}
/**
* Close PDF document.
*/
@Override
public void close() {
if (closed) {
return;
}
isClosing = true;
try {
if (writer != null) {
if (catalog.isFlushed()) {
throw new PdfException(
KernelExceptionMessageConstant.CANNOT_CLOSE_DOCUMENT_WITH_ALREADY_FLUSHED_PDF_CATALOG);
}
EventManager manager = EventManager.getInstance();
manager.onEvent(new NumberOfPagesStatisticsEvent(catalog.getPageTree().getNumberOfPages(),
ITextCoreProductData.getInstance()));
// The event will prepare document for flushing, i.e. will set an appropriate producer line
manager.onEvent(new FlushPdfDocumentEvent(this));
updateXmpMetadata();
// In PDF 2.0, all the values except CreationDate and ModDate are deprecated. Remove them now
if (pdfVersion.compareTo(PdfVersion.PDF_2_0) >= 0) {
for (PdfName deprecatedKey : PdfDocumentInfo.PDF20_DEPRECATED_KEYS) {
getDocumentInfo().getPdfObject().remove(deprecatedKey);
}
}
if (getXmpMetadata() != null) {
PdfStream xmp = catalog.getPdfObject().getAsStream(PdfName.Metadata);
if (isAppendMode() && xmp != null && !xmp.isFlushed() && xmp.getIndirectReference() != null) {
// Use existing object for append mode
xmp.setData(xmpMetadata);
xmp.setModified();
} else {
// Create new object
xmp = (PdfStream) new PdfStream().makeIndirect(this);
xmp.getOutputStream().write(xmpMetadata);
catalog.getPdfObject().put(PdfName.Metadata, xmp);
catalog.setModified();
}
xmp.put(PdfName.Type, PdfName.Metadata);
xmp.put(PdfName.Subtype, PdfName.XML);
if (writer.crypto != null && !writer.crypto.isMetadataEncrypted()) {
PdfArray ar = new PdfArray();
ar.add(PdfName.Crypt);
xmp.put(PdfName.Filter, ar);
}
}
checkIsoConformance();
if (getNumberOfPages() == 0) {
// Add new page here, not in PdfPagesTree#generateTree method, so that any page
// operations are available when handling the START_PAGE and INSERT_PAGE events
addNewPage();
}
PdfObject crypto = null;
final Set forbiddenToFlush = new HashSet<>();
if (properties.appendMode) {
if (structTreeRoot != null) {
tryFlushTagStructure(true);
}
if (catalog.isOCPropertiesMayHaveChanged() && catalog.getOCProperties(false).getPdfObject()
.isModified()) {
catalog.getOCProperties(false).flush();
}
if (catalog.pageLabels != null) {
catalog.put(PdfName.PageLabels, catalog.pageLabels.buildTree());
}
for (Map.Entry entry : catalog.nameTrees.entrySet()) {
PdfNameTree tree = entry.getValue();
if (tree.isModified()) {
ensureTreeRootAddedToNames(tree.buildTree().makeIndirect(this), entry.getKey());
}
}
PdfObject pageRoot = catalog.getPageTree().generateTree();
if (catalog.getPdfObject().isModified() || pageRoot.isModified()) {
catalog.put(PdfName.Pages, pageRoot);
catalog.getPdfObject().flush(false);
}
if (getDocumentInfo().getPdfObject().isModified()) {
getDocumentInfo().getPdfObject().flush(false);
}
flushFonts();
if (writer.crypto != null) {
assert reader.decrypt.getPdfObject()
== writer.crypto.getPdfObject() : "Conflict with source encryption";
crypto = reader.decrypt.getPdfObject();
if (crypto.getIndirectReference() != null) {
// Checking just for extra safety, encryption dictionary shall never be direct.
forbiddenToFlush.add(crypto.getIndirectReference());
}
}
writer.flushModifiedWaitingObjects(forbiddenToFlush);
for (int i = 0; i < xref.size(); i++) {
PdfIndirectReference indirectReference = xref.get(i);
if (indirectReference != null && !indirectReference.isFree() && indirectReference.checkState(
PdfObject.MODIFIED) && !indirectReference.checkState(PdfObject.FLUSHED)
&& !forbiddenToFlush.contains(indirectReference)) {
indirectReference.setFree();
}
}
} else {
if (catalog.isOCPropertiesMayHaveChanged()) {
catalog.getPdfObject().put(PdfName.OCProperties, catalog.getOCProperties(false).getPdfObject());
catalog.getOCProperties(false).flush();
}
if (catalog.pageLabels != null) {
catalog.put(PdfName.PageLabels, catalog.pageLabels.buildTree());
}
catalog.getPdfObject().put(PdfName.Pages, catalog.getPageTree().generateTree());
for (Map.Entry entry : catalog.nameTrees.entrySet()) {
PdfNameTree tree = entry.getValue();
if (tree.isModified()) {
ensureTreeRootAddedToNames(tree.buildTree().makeIndirect(this), entry.getKey());
}
}
for (int pageNum = 1; pageNum <= getNumberOfPages(); pageNum++) {
PdfPage page = getPage(pageNum);
if (page != null) {
page.flush();
}
}
if (structTreeRoot != null) {
tryFlushTagStructure(false);
}
catalog.getPdfObject().flush(false);
getDocumentInfo().getPdfObject().flush(false);
flushFonts();
if (writer.crypto != null) {
crypto = writer.crypto.getPdfObject();
crypto.makeIndirect(this);
forbiddenToFlush.add(crypto.getIndirectReference());
}
writer.flushWaitingObjects(forbiddenToFlush);
for (int i = 0; i < xref.size(); i++) {
PdfIndirectReference indirectReference = xref.get(i);
if (indirectReference != null && !indirectReference.isFree() && !indirectReference.checkState(
PdfObject.FLUSHED) && !forbiddenToFlush.contains(indirectReference)) {
PdfObject object;
if (isFlushUnusedObjects() && !indirectReference.checkState(
PdfObject.ORIGINAL_OBJECT_STREAM)
&& (object = indirectReference.getRefersTo(false)) != null) {
object.flush();
} else {
indirectReference.setFree();
}
}
}
}
// To avoid encryption of XrefStream and Encryption dictionary remove crypto.
// NOTE. No need in reverting, because it is the last operation with the document.
writer.crypto = null;
if (!properties.appendMode && crypto != null) {
// no need to flush crypto in append mode, it shall not have changed in this case
crypto.flush(false);
}
// The following two operators prevents the possible inconsistency between root and info
// entries existing in the trailer object and corresponding fields. This inconsistency
// may appear when user gets trailer and explicitly sets new root or info dictionaries.
trailer.put(PdfName.Root, catalog.getPdfObject());
trailer.put(PdfName.Info, getDocumentInfo().getPdfObject());
//By this time original and modified document ids should always be not null due to initializing in
// either writer properties, or in the writer init section on document open or from pdfreader. So we
// shouldn't worry about it being null next
PdfObject fileId = PdfEncryption.createInfoId(ByteUtils.getIsoBytes(originalDocumentId.getValue()),
ByteUtils.getIsoBytes(modifiedDocumentId.getValue()));
xref.writeXrefTableAndTrailer(this, fileId, crypto);
writer.flush();
if (writer.getOutputStream() instanceof CountOutputStream) {
long amountOfBytes = ((CountOutputStream) writer.getOutputStream()).getAmountOfWrittenBytes();
manager.onEvent(new SizeOfPdfStatisticsEvent(amountOfBytes, ITextCoreProductData.getInstance()));
}
}
catalog.getPageTree().clearPageRefs();
removeAllHandlers();
} catch (IOException e) {
throw new PdfException(KernelExceptionMessageConstant.CANNOT_CLOSE_DOCUMENT, e, this);
} finally {
if (writer != null && isCloseWriter()) {
try {
writer.close();
} catch (Exception e) {
Logger logger = LoggerFactory.getLogger(PdfDocument.class);
logger.error(IoLogMessageConstant.PDF_WRITER_CLOSING_FAILED, e);
}
}
if (reader != null && isCloseReader()) {
try {
reader.close();
} catch (Exception e) {
Logger logger = LoggerFactory.getLogger(PdfDocument.class);
logger.error(IoLogMessageConstant.PDF_READER_CLOSING_FAILED, e);
}
}
}
closed = true;
}
/**
* Gets close status of the document.
*
* @return true, if the document has already been closed, otherwise false.
*/
public boolean isClosed() {
return closed;
}
/**
* Gets tagged status of the document.
*
* @return true, if the document has tag structure, otherwise false.
*/
public boolean isTagged() {
return structTreeRoot != null;
}
/**
* Specifies that document shall contain tag structure.
* See ISO 32000-1, section 14.8 "Tagged PDF"
*
* @return this {@link PdfDocument} instance
*/
public PdfDocument setTagged() {
checkClosingStatus();
if (structTreeRoot == null) {
structTreeRoot = new PdfStructTreeRoot(this);
catalog.getPdfObject().put(PdfName.StructTreeRoot, structTreeRoot.getPdfObject());
updateValueInMarkInfoDict(PdfName.Marked, PdfBoolean.TRUE);
structParentIndex = 0;
}
return this;
}
/**
* Gets {@link PdfStructTreeRoot} of tagged document.
*
* @return {@link PdfStructTreeRoot} in case document is tagged, otherwise it returns null.
*
* @see #isTagged()
* @see #getNextStructParentIndex()
*/
public PdfStructTreeRoot getStructTreeRoot() {
return structTreeRoot;
}
/**
* Gets next parent index of tagged document.
*
* @return -1 if document is not tagged, or >= 0 if tagged.
*
* @see #isTagged()
* @see #getNextStructParentIndex()
*/
public int getNextStructParentIndex() {
return structParentIndex < 0 ? -1 : structParentIndex++;
}
/**
* Gets document {@code TagStructureContext}.
* The document must be tagged, otherwise an exception will be thrown.
*
* @return document {@code TagStructureContext}.
*/
public TagStructureContext getTagStructureContext() {
checkClosingStatus();
if (tagStructureContext == null) {
if (!isTagged()) {
throw new PdfException(KernelExceptionMessageConstant.MUST_BE_A_TAGGED_DOCUMENT);
}
initTagStructureContext();
}
return tagStructureContext;
}
/**
* Copies a range of pages from current document to {@code toDocument}.
* Use this method if you want to copy pages across tagged documents.
* This will keep resultant PDF structure consistent.
*
* If outlines destination names are the same in different documents, all
* such outlines will lead to a single location in the resultant document.
* In this case iText will log a warning. This can be avoided by renaming
* destinations names in the source document.
*
* @param pageFrom start of the range of pages to be copied.
* @param pageTo end of the range of pages to be copied.
* @param toDocument a document to copy pages to.
* @param insertBeforePage a position where to insert copied pages.
*
* @return list of copied pages
*/
public List copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument, int insertBeforePage) {
return copyPagesTo(pageFrom, pageTo, toDocument, insertBeforePage, null);
}
/**
* Copies a range of pages from current document to {@code toDocument}. This range is inclusive, both {@code page}
* and {@code pageTo} are included in list of copied pages.
* Use this method if you want to copy pages across tagged documents.
* This will keep resultant PDF structure consistent.
*
* If outlines destination names are the same in different documents, all
* such outlines will lead to a single location in the resultant document.
* In this case iText will log a warning. This can be avoided by renaming
* destinations names in the source document.
*
* @param pageFrom 1-based start of the range of pages to be copied.
* @param pageTo 1-based end (inclusive) of the range of pages to be copied. This page is included in list
* of copied pages.
* @param toDocument a document to copy pages to.
* @param insertBeforePage a position where to insert copied pages.
* @param copier a copier which bears a special copy logic. May be null.
* It is recommended to use the same instance of {@link IPdfPageExtraCopier}
* for the same output document.
*
* @return list of new copied pages
*/
public List copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument, int insertBeforePage,
IPdfPageExtraCopier copier) {
List pages = new ArrayList<>();
for (int i = pageFrom; i <= pageTo; i++) {
pages.add(i);
}
return copyPagesTo(pages, toDocument, insertBeforePage, copier);
}
/**
* Copies a range of pages from current document to {@code toDocument} appending copied pages to the end. This range
* is inclusive, both {@code page} and {@code pageTo} are included in list of copied pages.
* Use this method if you want to copy pages across tagged documents.
* This will keep resultant PDF structure consistent.
*
* If outlines destination names are the same in different documents, all
* such outlines will lead to a single location in the resultant document.
* In this case iText will log a warning. This can be avoided by renaming
* destinations names in the source document.
*
* @param pageFrom 1-based start of the range of pages to be copied.
* @param pageTo 1-based end (inclusive) of the range of pages to be copied. This page is included in list of
* copied pages.
* @param toDocument a document to copy pages to.
*
* @return list of new copied pages
*/
public List copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument) {
return copyPagesTo(pageFrom, pageTo, toDocument, null);
}
/**
* Copies a range of pages from current document to {@code toDocument} appending copied pages to the end. This range
* is inclusive, both {@code page} and {@code pageTo} are included in list of copied pages.
* Use this method if you want to copy pages across tagged documents.
* This will keep resultant PDF structure consistent.
*
* If outlines destination names are the same in different documents, all
* such outlines will lead to a single location in the resultant document.
* In this case iText will log a warning. This can be avoided by renaming
* destinations names in the source document.
*
* @param pageFrom 1-based start of the range of pages to be copied.
* @param pageTo 1-based end (inclusive) of the range of pages to be copied. This page is included in list of
* copied pages.
* @param toDocument a document to copy pages to.
* @param copier a copier which bears a special copy logic. May be null.
* It is recommended to use the same instance of {@link IPdfPageExtraCopier}
* for the same output document.
*
* @return list of new copied pages.
*/
public List copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument, IPdfPageExtraCopier copier) {
return copyPagesTo(pageFrom, pageTo, toDocument, toDocument.getNumberOfPages() + 1, copier);
}
/**
* Copies a range of pages from current document to {@code toDocument}.
* Use this method if you want to copy pages across tagged documents.
* This will keep resultant PDF structure consistent.
*
* If outlines destination names are the same in different documents, all
* such outlines will lead to a single location in the resultant document.
* In this case iText will log a warning. This can be avoided by renaming
* destinations names in the source document.
*
* @param pagesToCopy list of pages to be copied.
* @param toDocument a document to copy pages to.
* @param insertBeforePage a position where to insert copied pages.
*
* @return list of new copied pages
*/
public List copyPagesTo(List pagesToCopy, PdfDocument toDocument, int insertBeforePage) {
return copyPagesTo(pagesToCopy, toDocument, insertBeforePage, null);
}
/**
* Copies a range of pages from current document to {@code toDocument}.
* Use this method if you want to copy pages across tagged documents.
* This will keep resultant PDF structure consistent.
*
* If outlines destination names are the same in different documents, all
* such outlines will lead to a single location in the resultant document.
* In this case iText will log a warning. This can be avoided by renaming
* destinations names in the source document.
*
* @param pagesToCopy list of pages to be copied.
* @param toDocument a document to copy pages to.
* @param insertBeforePage a position where to insert copied pages.
* @param copier a copier which bears a special copy logic. May be null.
* It is recommended to use the same instance of {@link IPdfPageExtraCopier}
* for the same output document.
*
* @return list of new copied pages
*/
public List copyPagesTo(List pagesToCopy, PdfDocument toDocument, int insertBeforePage,
IPdfPageExtraCopier copier) {
if (pagesToCopy.isEmpty()) {
return Collections.emptyList();
}
pendingDestinationMutations.clear();
checkClosingStatus();
List copiedPages = new ArrayList<>();
Map page2page = new LinkedHashMap<>();
Set outlinesToCopy = new HashSet<>();
List