com.itextpdf.kernel.pdf.PdfDocument Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2023 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
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 .
*/
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 PdfName[] PDF_NAMES_TO_REMOVE_FROM_ORIGINAL_TRAILER = new PdfName[] {
PdfName.Encrypt,
PdfName.Size,
PdfName.Prev,
PdfName.Root,
PdfName.Info,
PdfName.ID,
PdfName.XRefStm,
};
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