PdfWriter.java
/*
* $Id: PdfWriter.java 4095 2009-11-12 14:40:41Z blowagie $
*
* Copyright 1999, 2000, 2001, 2002 Bruno Lowagie
*
* The contents of this file are subject to the Mozilla Public License Version 1.1
* (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the License.
*
* The Original Code is 'iText, a free JAVA-PDF library'.
*
* The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
* the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
* All Rights Reserved.
* Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
* are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
*
* Contributor(s): all the names of the contributors are added in the source code
* where applicable.
*
* Alternatively, the contents of this file may be used under the terms of the
* LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
* provisions of LGPL are applicable instead of those above. If you wish to
* allow use of your version of this file only under the terms of the LGPL
* License and not to allow others to use your version of this file under
* the MPL, indicate your decision by deleting the provisions above and
* replace them with the notice and other provisions required by the LGPL.
* If you do not delete the provisions above, a recipient may use your version
* of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MPL as stated above or under the terms of the GNU
* Library General Public License as published by the Free Software Foundation;
* either version 2 of the License, or any later version.
*
* This library 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 Library general Public License for more
* details.
*
* If you didn't download this code from the following link, you should check if
* you aren't using an obsolete version:
* https://github.com/LibrePDF/OpenPDF
*/
package org.openpdf.text.pdf;
import org.openpdf.text.DocWriter;
import org.openpdf.text.Document;
import org.openpdf.text.DocumentException;
import org.openpdf.text.ExceptionConverter;
import org.openpdf.text.Image;
import org.openpdf.text.error_messages.MessageLocalization;
import org.openpdf.text.pdf.interfaces.PdfRunDirection;
import org.openpdf.text.pdf.interfaces.PdfVersion;
import org.openpdf.text.pdf.internal.PdfVersionImp;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeSet;
/**
* A <CODE>DocWriter</CODE> class for PDF.
* <p>
* When this <CODE>PdfWriter</CODE> is added to a certain <CODE>PdfDocument</CODE>, the PDF representation of every
* Element added to this Document will be written to the outputstream.</P>
*/
public class PdfWriter extends DocWriter implements
PdfVersion,
PdfRunDirection {
/**
* The highest generation number possible.
*
* @since iText 2.1.6
*/
public static final int GENERATION_MAX = 65535;
// INNER CLASSES
/**
* possible PDF version (header)
*/
public static final String VERSION_1_2 = "1.2";
/**
* possible PDF version (header)
*/
public static final String VERSION_1_3 = "1.3";
// ESSENTIALS
// Construct a PdfWriter instance
/**
* possible PDF version (header)
*/
public static final String VERSION_1_4 = "1.4";
/**
* possible PDF version (header)
*/
public static final String VERSION_1_5 = "1.5";
/**
* possible PDF version (header)
*/
public static final String VERSION_1_6 = "1.6";
/**
* possible PDF version (header)
*/
public static final String VERSION_1_7 = "1.7";
/**
* PDF version 2.0.
*/
public static final String VERSION_2_0 = "2.0";
// the PdfDocument instance
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_1_2 = new PdfName("1.2");
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_1_3 = new PdfName("1.3");
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_1_4 = new PdfName("1.4");
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_1_5 = new PdfName("1.5");
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_1_6 = new PdfName("1.6");
// the PdfDirectContentByte instances
/*
* You should see Direct Content as a canvas on which you can draw
* graphics and text. One canvas goes on top of the page (getDirectContent),
* the other goes underneath (getDirectContentUnder).
* You can always the same object throughout your document,
* even if you have moved to a new page. Whatever you add on
* the canvas will be displayed on top or under the current page.
*/
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_1_7 = new PdfName("1.7");
/**
* possible PDF version (catalog)
*/
public static final PdfName PDF_VERSION_2_0 = new PdfName("2.0");
// PDF body
// PDF Catalog
// PdfPages
// page events
// Open and Close methods + method that create the PDF
// Root data for the PDF document (used when composing the Catalog)
// [C3] PdfViewerPreferences interface
/**
* This is the list with all the images in the document.
*/
private final HashMap<Long, PdfName> images = new LinkedHashMap<>();
/**
* the pdfdocument object.
*/
protected PdfDocument pdf;
/**
* The direct content in this document.
*/
protected PdfContentByte directContent;
/**
* The direct content under in this document.
*/
protected PdfContentByte directContentUnder;
// page mode (section 13.1.2 of "iText in Action")
/**
* body of the PDF document
*/
protected PdfBody body;
/**
* Holds value of property extraCatalog this is used for Output Intents.
*/
protected PdfDictionary extraCatalog;
/**
* The root of the page tree.
*/
protected PdfPages root = new PdfPages(this);
/**
* The PdfIndirectReference to the pages.
*/
protected ArrayList<PdfIndirectReference> pageReferences = new ArrayList<>();
/**
* The current page number.
*/
protected int currentPageNumber = 1;
/**
* The value of the Tabs entry in the page dictionary.
*
* @since 2.1.5
*/
protected PdfName tabs = null;
// values for setting viewer preferences in iText versions older than 2.x
/**
* A number referring to the previous Cross-Reference Table.
*/
protected int prevxref = 0;
/**
* Stores the version information for the header and the catalog.
*/
protected PdfVersionImp pdf_version = new PdfVersionImp();
/**
* XMP Metadata for the document.
*/
protected byte[] xmpMetadata = null;
/**
* Contains the business logic for cryptography.
*/
/**
* Holds value of property fullCompression.
*/
protected boolean fullCompression = false;
/**
* The compression level of the content streams.
*
* @since 2.1.3
*/
protected int compressionLevel = PdfStream.DEFAULT_COMPRESSION;
/**
* When not null, the file identifier ({@code /ID}) of the trailer: the same bytes every time, instead of a
* random one. Used to produce reproducible files.
*/
private byte[] fixedFileId = null;
/**
* Makes the file identifier ({@code /ID}) of the trailer the given bytes (16 are used), instead of a random value,
* so that the same document gives the same file.
*
* @param fileId the identifier, or null for a random one (the default)
*/
public void setFixedFileId(byte[] fileId) {
this.fixedFileId = fileId;
}
/**
* The fonts of this document
*/
protected LinkedHashMap<BaseFont, FontDetails> documentFonts = new LinkedHashMap<>();
/**
* The font number counter for the fonts in the document.
*/
protected int fontNumber = 1;
/**
* The form XObjects in this document. The key is the xref and the value is Object[]{PdfName, template}.
*/
protected LinkedHashMap<PdfIndirectReference, Object[]> formXObjects = new LinkedHashMap<>();
/**
* The name counter for the form XObjects name.
*/
protected int formXObjectsCounter = 1;
/**
* The color number counter for the colors in the document.
*/
protected int colorNumber = 1;
// [C4] Page labels
/**
* The patterns of this document
*/
protected HashMap<PdfDictionary, PdfObject[]> documentExtGState = new LinkedHashMap<>();
protected HashMap<Object, PdfObject[]> documentProperties = new LinkedHashMap<>();
/**
* A group attributes dictionary specifying the attributes of the page's page group for use in the transparent
* imaging model
*/
protected PdfDictionary group;
protected float userunit = 0f;
protected PdfDictionary defaultColorspace = new PdfDictionary();
// [C8] AcroForm
/**
* Dictionary, containing all the images of the PDF document
*/
protected PdfDictionary imageDictionary = new PdfDictionary();
/**
* A HashSet with Stream objects containing JBIG2 Globals
*
* @since 2.1.5
*/
protected HashMap<PdfStream, PdfIndirectReference> JBIG2Globals = new LinkedHashMap<>();
/**
* Holds value of property RGBTranparency.
*/
private boolean rgbTransparencyBlending;
// [C9] Metadata
/**
* Constructs a <CODE>PdfWriter</CODE>.
* <p>
* Remark: a PdfWriter can only be constructed by calling the method
* <CODE>getInstance(Document document, OutputStream os)</CODE>.
*
* @param document The <CODE>PdfDocument</CODE> that has to be written
* @param os The <CODE>OutputStream</CODE> the writer has to write to.
*/
protected PdfWriter(PdfDocument document, OutputStream os) {
super(document, os);
pdf = document;
directContent = new PdfContentByte(this);
directContentUnder = new PdfContentByte(this);
}
/**
* Use this method to get an instance of the <CODE>PdfWriter</CODE>.
*
* @param document The <CODE>Document</CODE> that has to be written
* @param os The <CODE>OutputStream</CODE> the writer has to write to.
* @return a new <CODE>PdfWriter</CODE>
* @throws DocumentException on error
*/
public static PdfWriter getInstance(Document document, OutputStream os)
throws DocumentException {
PdfDocument pdf = new PdfDocument();
pdf.setTextRenderingOptions(document.getTextRenderingOptions());
document.addDocListener(pdf);
PdfWriter writer = new PdfWriter(pdf, os);
pdf.addWriter(writer);
return writer;
}
/**
* Use this method to get an instance of the <CODE>PdfWriter</CODE>.
*
* @param document The <CODE>Document</CODE> that has to be written
* @param os The <CODE>OutputStream</CODE> the writer has to write to.
* @param listener A <CODE>DocListener</CODE> to pass to the PdfDocument.
* @return a new <CODE>PdfWriter</CODE>
* @throws DocumentException on error
*/
// [C10] PDFX Conformance
/**
* Gets the <CODE>PdfDocument</CODE> associated with this writer.
*
* @return the <CODE>PdfDocument</CODE>
*/
PdfDocument getPdfDocument() {
return pdf;
}
/**
* Use this method to get the info dictionary if you want to change it directly (add keys and values to the info
* dictionary).
*
* @return the info dictionary
*/
public PdfDictionary getInfo() {
return pdf.getInfo();
}
/**
* Use this method to get the direct content for this document. There is only one direct content, multiple calls to
* this method will allways retrieve the same object.
*
* @return the direct content
*/
public PdfContentByte getDirectContent() {
if (!open) {
throw new RuntimeException(MessageLocalization.getComposedMessage("the.document.is.not.open"));
}
return directContent;
}
/**
* Use this method to get the direct content under for this document. There is only one direct content, multiple
* calls to this method will always retrieve the same object.
*
* @return the direct content
*/
public PdfContentByte getDirectContentUnder() {
if (!open) {
throw new RuntimeException(MessageLocalization.getComposedMessage("the.document.is.not.open"));
}
return directContentUnder;
}
/**
* Resets all the direct contents to empty. This happens when a new page is started.
*/
void resetContent() {
directContent.reset();
directContentUnder.reset();
}
/**
* Use this method to add a PDF object to the PDF body. Use this method only if you know what you're doing!
*
* @param object the PDF object to add
* @return a PdfIndirectObject
* @throws IOException thrown when an I/O operation fails
*/
public PdfIndirectObject addToBody(PdfObject object) throws IOException {
return body.add(object);
}
/**
* Use this method to add a PDF object to the PDF body. Use this method only if you know what you're doing!
*
* @param object the PDF object to add
* @param inObjStm a boolean
* @return a PdfIndirectObject
* @throws IOException thrown when an I/O operation fails
*/
public PdfIndirectObject addToBody(PdfObject object, boolean inObjStm) throws IOException {
return body.add(object, inObjStm);
}
// [C11] Output intents
/**
* Use this method to add a PDF object to the PDF body. Use this method only if you know what you're doing!
*
* @param object the object to add
* @param ref the IndirectReference of that object
* @return a PdfIndirectObject
* @throws IOException thrown when an I/O operation fails
*/
public PdfIndirectObject addToBody(PdfObject object, PdfIndirectReference ref) throws IOException {
return body.add(object, ref);
}
// PDF Objects that have an impact on the PDF body
// [F1] PdfEncryptionSettings interface
// types of encryption
/**
* Use this to get an <CODE>PdfIndirectReference</CODE> for an object that will be created in the future. Use this
* method only if you know what you're doing!
*
* @return the <CODE>PdfIndirectReference</CODE>
*/
public PdfIndirectReference getPdfIndirectReference() {
return body.getPdfIndirectReference();
}
/**
* Returns the outputStreamCounter.
*
* @return the outputStreamCounter
*/
OutputStreamCounter getOs() {
return os;
}
protected PdfDictionary getCatalog(PdfIndirectReference rootObj) {
PdfDictionary catalog = pdf.getCatalog(rootObj);
return catalog;
}
// ENCRYPTION_* = 3 reserved for implementation of the
// meanwhile deprecated proprietary AES256 variant by Adobe
/**
* Use this method to get a reference to a page existing or not. If the page does not exist yet the reference will
* be created in advance. If on closing the document, a page number greater than the total number of pages was
* requested, an exception is thrown.
*
* @param page the page number. The first page is 1
* @return the reference to the page
*/
public PdfIndirectReference getPageReference(int page) {
--page;
if (page < 0) {
throw new IndexOutOfBoundsException(
MessageLocalization.getComposedMessage("the.page.number.must.be.gt.eq.1"));
}
PdfIndirectReference ref;
if (page < pageReferences.size()) {
ref = pageReferences.get(page);
if (ref == null) {
ref = body.getPdfIndirectReference();
pageReferences.set(page, ref);
}
} else {
int empty = page - pageReferences.size();
for (int k = 0; k < empty; ++k) {
pageReferences.add(null);
}
ref = body.getPdfIndirectReference();
pageReferences.add(ref);
}
return ref;
}
// permissions
/**
* Retrieves a reference to the current page of the document.
* The current page is typically the last page that was modified or accessed.
*
* @return a {@link PdfIndirectReference} object pointing to the current page in the PDF document.
*/
public PdfIndirectReference getCurrentPage() {
return getPageReference(currentPageNumber);
}
/**
* Returns the value to be used for the Tabs entry in the page tree.
*
* @return a PdfName of the value of the Tabs entry in the page dictionnary
* @since 2.1.5
*/
public PdfName getTabs() {
return tabs;
}
/**
* Adds some <CODE>PdfContents</CODE> to this Writer.
* <p>
* The document has to be open before you can begin to add content to the body of the document.
*
* @param page the <CODE>PdfPage</CODE> to add
* @param contents the <CODE>PdfContents</CODE> of the page
* @return a <CODE>PdfIndirectReference</CODE>
* @throws PdfException on error
*/
PdfIndirectReference add(PdfPage page, PdfContents contents) throws PdfException {
if (!open) {
throw new PdfException(MessageLocalization.getComposedMessage("the.document.is.not.open"));
}
PdfIndirectObject object;
try {
object = addToBody(contents);
} catch (IOException ioe) {
throw new ExceptionConverter(ioe);
}
page.add(object.getIndirectReference());
// [U5]
if (group != null) {
page.put(PdfName.GROUP, group);
group = null;
} else if (rgbTransparencyBlending) {
PdfDictionary pp = new PdfDictionary();
pp.put(PdfName.TYPE, PdfName.GROUP);
pp.put(PdfName.S, PdfName.TRANSPARENCY);
pp.put(PdfName.CS, PdfName.DEVICERGB);
page.put(PdfName.GROUP, pp);
}
root.addPage(page);
currentPageNumber++;
return null;
}
/**
* Gets the <CODE>PdfPageEvent</CODE> for this document or <CODE>null</CODE> if none is set.
*
* @return the <CODE>PdfPageEvent</CODE> for this document or <CODE>null</CODE> if none is set
*/
/**
* Signals that the <CODE>Document</CODE> has been opened and that
* <CODE>Elements</CODE> can be added.
* <p>
* When this method is called, the PDF-document header is written to the outputstream.
*
* @see org.openpdf.text.DocWriter#open()
*/
@Override
public void open() {
super.open();
try {
pdf_version.writeHeader(os);
body = new PdfBody(this);
} catch (IOException ioe) {
throw new ExceptionConverter(ioe);
}
}
/**
* Signals that the <CODE>Document</CODE> was closed and that no other
* <CODE>Elements</CODE> will be added.
* <p>
* The pages-tree is built and written to the outputstream. A Catalog is constructed, as well as an Info-object, the
* reference table is composed and everything is written to the outputstream embedded in a Trailer.
*
* @see org.openpdf.text.DocWriter#close()
*/
@Override
public void close() {
if (open) {
if (pdf != null && pdf.isOpen()) {
throw new IllegalStateException(MessageLocalization
.getComposedMessage("you.should.call.document.close.instead"));
}
if ((currentPageNumber - 1) != pageReferences.size()) {
// 2019-04-26: If you get this error, it could be that you are using OpenPDF or
// another library such as flying-saucer's ITextRenderer in a non-threadsafe way.
// ITextRenderer is not thread safe. So if you get this problem here, create a new
// instance, rather than re-using it.
// See: https://github.com/LibrePDF/OpenPDF/issues/164
throw new RuntimeException("The page " + pageReferences.size() +
" was requested but the document has only " + (currentPageNumber - 1) + " pages.");
}
try {
addSharedObjectsToBody();
// add the root to the body
PdfIndirectReference rootRef = root.writePageTree();
// make the catalog-object and add it to the body
PdfDictionary catalog = getCatalog(rootRef);
// [C9] if there is XMP data to add: add it
if (xmpMetadata != null) {
PdfStream xmp = new PdfStream(xmpMetadata);
xmp.put(PdfName.TYPE, PdfName.METADATA);
xmp.put(PdfName.SUBTYPE, PdfName.XML);
catalog.put(PdfName.METADATA, body.add(xmp).getIndirectReference());
}
// [C11] Output Intents
if (extraCatalog != null) {
catalog.mergeDifferent(extraCatalog);
}
// add the Catalog to the body
PdfIndirectObject indirectCatalog = addToBody(catalog, false);
// add the info-object to the body
PdfIndirectObject infoObj = addToBody(getInfo(), false);
// [F1] encryption
PdfIndirectReference encryption = null;
PdfObject fileID;
body.flushObjStm();
if (getInfo().contains(PdfName.FILEID)) {
fileID = getInfo().get(PdfName.FILEID);
} else {
// the same documentId shall be provided to the first version
byte[] documentId = fixedFileId != null ? fixedFileId : createDocumentId();
fileID = createInfoId(documentId, documentId);
}
// write the cross-reference table of the body
body.writeCrossReferenceTable(os, indirectCatalog.getIndirectReference(),
infoObj.getIndirectReference(), encryption, fileID, prevxref);
os.write(getISOBytes("startxref\n"));
os.write(getISOBytes(String.valueOf(body.offset())));
os.write(getISOBytes("\n%%EOF\n"));
super.close();
} catch (IOException ioe) {
throw new ExceptionConverter(ioe);
}
}
}
protected void addSharedObjectsToBody() throws IOException {
// [F3] add the fonts
for (FontDetails details : documentFonts.values()) {
details.writeFont(this);
}
// [F4] add the form XObjects
for (Object[] objs : formXObjects.values()) {
PdfTemplate template = (PdfTemplate) objs[1];
if (template != null && template.getType() == PdfTemplate.TYPE_TEMPLATE) {
addToBody(template.getFormXObject(compressionLevel), template.getIndirectReference());
}
}
// [F10] add the extgstate
for (Map.Entry<PdfDictionary, PdfObject[]> entry : documentExtGState.entrySet()) {
PdfDictionary gstate = entry.getKey();
PdfObject[] obj = entry.getValue();
addToBody(gstate, (PdfIndirectReference) obj[1]);
}
// [F11] add the properties
for (Map.Entry<Object, PdfObject[]> entry : documentProperties.entrySet()) {
Object prop = entry.getKey();
PdfObject[] obj = entry.getValue();
if (prop instanceof PdfDictionary) {
addToBody((PdfDictionary) prop, (PdfIndirectReference) obj[1]);
}
}
}
/**
* Returns the version information.
*/
PdfVersionImp getPdfVersion() {
return pdf_version;
}
// [F3] adding fonts
public void addAnnotation(PdfAnnotation annot) {
pdf.addAnnotation(annot);
}
// [F9] document shadings
// [F11] adding properties (OCG, marked content)
// [F12] tagged PDF
private static byte[] createDocumentId() {
final byte[] id = new byte[16];
// Not secure on purpose: the /ID of a PDF only has to be (very likely) unique.
new java.util.Random(System.nanoTime() ^ System.currentTimeMillis()).nextBytes(id);
return id;
}
private static PdfObject createInfoId(byte[] idPartOne, byte[] idPartTwo) {
try (ByteBuffer buf = new ByteBuffer(90)) {
buf.append('[').append('<');
for (int k = 0; k < 16 && k < idPartOne.length; ++k) {
buf.appendHex(idPartOne[k]);
}
buf.append('>').append('<');
for (int k = 0; k < 16 && k < idPartTwo.length; ++k) {
buf.appendHex(idPartTwo[k]);
}
buf.append('>').append(']');
return new PdfLiteral(buf.toByteArray());
} catch (IOException e) {
throw new ExceptionConverter(e);
}
}
/**
* Use this method to find out if 1.5 compression is on.
*
* @return the 1.5 compression status
*/
public boolean isFullCompression() {
return this.fullCompression;
}
/**
* Returns the compression level used for streams written by this writer.
*
* @return the compression level (0 = best speed, 9 = best compression, -1 is default)
* @since 2.1.3
*/
public int getCompressionLevel() {
return compressionLevel;
}
/**
* Sets the compression level to be used for streams written by this writer.
*
* @param compressionLevel a value between 0 (best speed) and 9 (best compression)
* @since 2.1.3
*/
public void setCompressionLevel(int compressionLevel) {
if (compressionLevel < PdfStream.NO_COMPRESSION || compressionLevel > PdfStream.BEST_COMPRESSION) {
this.compressionLevel = PdfStream.DEFAULT_COMPRESSION;
} else {
this.compressionLevel = compressionLevel;
}
}
// User methods to change aspects of the page
// [U1] page size
PdfName getColorspaceName() {
return new PdfName("CS" + (colorNumber++));
}
// [U2] take care of empty pages
// [U3] page actions (open and close)
PdfObject[] addSimpleExtGState(PdfDictionary gstate) {
if (!documentExtGState.containsKey(gstate)) {
documentExtGState.put(gstate,
new PdfObject[]{new PdfName("GS" + (documentExtGState.size() + 1)), getPdfIndirectReference()});
}
return documentExtGState.get(gstate);
}
// [U4] Thumbnail image
// [U5] Transparency groups
// [U7] run direction (doesn't actually do anything)
// Miscellaneous topics
// [M1] Color settings
// [M2] spot patterns
/**
* Use this method to get the user unit. A user unit is a value that defines the default user space unit. The
* minimum UserUnit is 1 (1 unit = 1/72 inch). The maximum UserUnit is 75,000. Note that this userunit only works
* starting with PDF1.6!
*
* @return Returns the userunit.
*/
public float getUserunit() {
return userunit;
}
// [M3] Images
/**
* Use this method to get the default colorspaces.
*
* @return the default colorspaces
*/
public PdfDictionary getDefaultColorspace() {
return defaultColorspace;
}
/**
* Writes a <CODE>PdfImage</CODE> to the outputstream.
*
* @param pdfImage the image to be added
* @return a <CODE>PdfIndirectReference</CODE> to the encapsulated image
* @throws PdfException when a document isn't open yet, or has been closed
*/
PdfIndirectReference add(PdfImage pdfImage, PdfIndirectReference fixedRef) throws PdfException {
if (!imageDictionary.contains(pdfImage.name())) {
try {
if (fixedRef == null) {
fixedRef = addToBody(pdfImage).getIndirectReference();
} else {
addToBody(pdfImage, fixedRef);
}
} catch (IOException ioe) {
throw new ExceptionConverter(ioe);
}
imageDictionary.put(pdfImage.name(), fixedRef);
return fixedRef;
}
return (PdfIndirectReference) imageDictionary.get(pdfImage.name());
}
/**
* return the <CODE>PdfIndirectReference</CODE> to the image with a given name.
*
* @param name the name of the image
* @return a <CODE>PdfIndirectReference</CODE>
*/
PdfIndirectReference getImageReference(PdfName name) {
return (PdfIndirectReference) imageDictionary.get(name);
}
/**
* Gets an indirect reference to a JBIG2 Globals stream. Adds the stream if it hasn't already been added to the
* writer.
*
* @param content a byte array that may already been added to the writer inside a stream object.
* @return the PdfIndirectReference
* @since 2.1.5
*/
protected PdfIndirectReference getReferenceJBIG2Globals(byte[] content) {
if (content == null) {
return null;
}
PdfStream stream;
for (PdfStream pdfStream : JBIG2Globals.keySet()) {
stream = pdfStream;
if (Arrays.equals(content, stream.getBytes())) {
return JBIG2Globals.get(stream);
}
}
stream = new PdfStream(content);
PdfIndirectObject ref;
try {
ref = addToBody(stream);
} catch (IOException e) {
return null;
}
JBIG2Globals.put(stream, ref.getIndirectReference());
return ref.getIndirectReference();
}
// [F12] tagged PDF
/**
* Gets the transparency blending colorspace.
*
* @return <code>true</code> if the transparency blending colorspace is RGB, <code>false</code>
* if it is the default blending colorspace
* @since 2.1.0
*/
public boolean isRgbTransparencyBlending() {
return this.rgbTransparencyBlending;
}
/**
* This class generates the structure of a PDF document.
* <p>
* This class covers the third section of Chapter 5 in the 'Portable Document Format Reference Manual version 1.3'
* (page 55-60). It contains the body of a PDF document (section 5.14) and it can also generate a Cross-reference
* Table (section 5.15).
*
* @see PdfWriter
* @see PdfObject
* @see PdfIndirectObject
*/
public static class PdfBody {
private static final int OBJSINSTREAM = 200;
// membervariables
/**
* array containing the cross-reference table of the normal objects.
*/
private final TreeSet<PdfCrossReference> xrefs;
private final PdfWriter writer;
private int refnum;
/**
* the current byte position in the body.
*/
private long position;
private ByteBuffer index;
private ByteBuffer streamObjects;
private int currentObjNum;
private int numObj = 0;
// constructors
/**
* Constructs a new <CODE>PdfBody</CODE>.
*
* @param writer
*/
PdfBody(PdfWriter writer) {
xrefs = new TreeSet<>();
xrefs.add(new PdfCrossReference(0, 0, GENERATION_MAX));
position = writer.getOs().getCounter();
refnum = 1;
this.writer = writer;
}
// methods
private PdfWriter.PdfBody.PdfCrossReference addToObjStm(PdfObject obj, int nObj) throws IOException {
if (numObj >= OBJSINSTREAM) {
flushObjStm();
}
if (index == null) {
index = new ByteBuffer();
streamObjects = new ByteBuffer();
currentObjNum = getIndirectReferenceNumber();
numObj = 0;
}
int p = streamObjects.size();
int idx = numObj++;
obj.toPdf(writer, streamObjects);
streamObjects.append(' ');
index.append(nObj).append(' ').append(p).append(' ');
return new PdfWriter.PdfBody.PdfCrossReference(2, nObj, currentObjNum, idx);
}
private void flushObjStm() throws IOException {
if (numObj == 0) {
return;
}
int first = index.size();
index.append(streamObjects);
PdfStream stream = new PdfStream(index.toByteArray());
stream.flateCompress(writer.getCompressionLevel());
stream.put(PdfName.TYPE, PdfName.OBJSTM);
stream.put(PdfName.N, new PdfNumber(numObj));
stream.put(PdfName.FIRST, new PdfNumber(first));
add(stream, currentObjNum);
index = null;
streamObjects = null;
numObj = 0;
}
/**
* Adds a <CODE>PdfObject</CODE> to the body.
* <p>
* This methods creates a <CODE>PdfIndirectObject</CODE> with a certain number, containing the given
* <CODE>PdfObject</CODE>. It also adds a <CODE>PdfCrossReference</CODE> for this object to an
* <CODE>ArrayList</CODE> that will be used to build the Cross-reference Table.
*
* @param object a <CODE>PdfObject</CODE>
* @return a <CODE>PdfIndirectObject</CODE>
* @throws IOException
*/
PdfIndirectObject add(PdfObject object) throws IOException {
return add(object, getIndirectReferenceNumber());
}
PdfIndirectObject add(PdfObject object, boolean inObjStm) throws IOException {
return add(object, getIndirectReferenceNumber(), inObjStm);
}
/**
* Gets a PdfIndirectReference for an object that will be created in the future.
*
* @return a PdfIndirectReference
*/
PdfIndirectReference getPdfIndirectReference() {
return new PdfIndirectReference(0, getIndirectReferenceNumber());
}
int getIndirectReferenceNumber() {
int n = refnum++;
xrefs.add(new PdfCrossReference(n, 0, GENERATION_MAX));
return n;
}
/**
* Adds a <CODE>PdfObject</CODE> to the body given an already existing PdfIndirectReference.
* <p>
* This methods creates a <CODE>PdfIndirectObject</CODE> with the number given by
* <CODE>ref</CODE>, containing the given <CODE>PdfObject</CODE>.
* It also adds a <CODE>PdfCrossReference</CODE> for this object to an <CODE>ArrayList</CODE> that will be used
* to build the Cross-reference Table.
*
* @param object a <CODE>PdfObject</CODE>
* @param ref a <CODE>PdfIndirectReference</CODE>
* @return a <CODE>PdfIndirectObject</CODE>
* @throws IOException
*/
PdfIndirectObject add(PdfObject object, PdfIndirectReference ref) throws IOException {
return add(object, ref.getNumber());
}
PdfIndirectObject add(PdfObject object, int refNumber) throws IOException {
return add(object, refNumber, true); // to false
}
PdfIndirectObject add(PdfObject object, int refNumber, boolean inObjStm) throws IOException {
if (inObjStm && object.canBeInObjStm() && writer.isFullCompression()) {
PdfCrossReference pxref = addToObjStm(object, refNumber);
PdfIndirectObject indirect = new PdfIndirectObject(refNumber, object, writer);
if (!xrefs.add(pxref)) {
xrefs.remove(pxref);
xrefs.add(pxref);
}
return indirect;
} else {
PdfIndirectObject indirect = new PdfIndirectObject(refNumber, object, writer);
PdfCrossReference pxref = new PdfCrossReference(refNumber, position);
if (!xrefs.add(pxref)) {
xrefs.remove(pxref);
xrefs.add(pxref);
}
indirect.writeTo(writer.getOs());
position = writer.getOs().getCounter();
return indirect;
}
}
/**
* Returns the offset of the Cross-Reference table.
*
* @return an offset
*/
long offset() {
return position;
}
/**
* Returns the total number of objects contained in the CrossReferenceTable of this <CODE>Body</CODE>.
*
* @return a number of objects
*/
int size() {
return Math.max((xrefs.last()).getRefnum() + 1, refnum);
}
/**
* Returns the CrossReferenceTable of the <CODE>Body</CODE>.
*
* @param os
* @param root
* @param info
* @param encryption
* @param fileID
* @param prevxref
* @throws IOException
*/
void writeCrossReferenceTable(OutputStream os, PdfIndirectReference root, PdfIndirectReference info,
PdfIndirectReference encryption, PdfObject fileID, int prevxref) throws IOException {
int refNumber = 0;
// Old-style xref tables limit object offsets to 10 digits
boolean useNewXrefFormat = writer.isFullCompression() || position > 9_999_999_999L;
if (useNewXrefFormat) {
flushObjStm();
refNumber = getIndirectReferenceNumber();
xrefs.add(new PdfCrossReference(refNumber, position));
}
PdfCrossReference entry = xrefs.first();
int first = entry.getRefnum();
int len = 0;
ArrayList<Integer> sections = new ArrayList<>();
for (PdfCrossReference xref1 : xrefs) {
entry = xref1;
if (first + len == entry.getRefnum()) {
++len;
} else {
sections.add(first);
sections.add(len);
first = entry.getRefnum();
len = 1;
}
}
sections.add(first);
sections.add(len);
PdfTrailer trailer = new PdfTrailer(size(), root, info, encryption, fileID, prevxref);
if (useNewXrefFormat) {
int mid = 8 - (Long.numberOfLeadingZeros(position) >> 3);
ByteBuffer buf = new ByteBuffer();
for (PdfCrossReference xref : xrefs) {
entry = xref;
entry.toPdf(mid, buf);
}
PdfStream xr = new PdfStream(buf.toByteArray());
xr.flateCompress(writer.getCompressionLevel());
xr.putAll(trailer);
xr.put(PdfName.W, new PdfArray(new int[]{1, mid, 2}));
xr.put(PdfName.TYPE, PdfName.XREF);
PdfArray idx = new PdfArray();
for (Integer section : sections) {
idx.add(new PdfNumber(section));
}
xr.put(PdfName.INDEX, idx);
PdfIndirectObject indirect = new PdfIndirectObject(refNumber, xr, writer);
indirect.writeTo(writer.getOs());
} else {
os.write(getISOBytes("xref\n"));
Iterator<PdfCrossReference> i = xrefs.iterator();
for (int k = 0; k < sections.size(); k += 2) {
first = sections.get(k);
len = sections.get(k + 1);
os.write(getISOBytes(String.valueOf(first)));
os.write(getISOBytes(" "));
os.write(getISOBytes(String.valueOf(len)));
os.write('\n');
while (len-- > 0) {
entry = i.next();
entry.toPdf(os);
}
}
// make the trailer
trailer.toPdf(writer, os);
}
}
// inner classes
/**
* <CODE>PdfCrossReference</CODE> is an entry in the PDF Cross-Reference table.
*/
public static class PdfCrossReference implements Comparable<PdfCrossReference> {
/**
* String template for cross-reference entry PDF representation.
*
* @see Formatter
*/
private static final String CROSS_REFERENCE_ENTRY_FORMAT = "%010d %05d %c \n";
// membervariables
private final int type;
/**
* Byte offset in the PDF file.
*/
private final long offset;
private final int refnum;
/**
* generation of the object.
*/
private final int generation;
// constructors
/**
* Constructs a cross-reference element for a PdfIndirectObject.
*
* @param refnum the reference number
* @param offset byte offset of the object
* @param generation generation number of the object
*/
public PdfCrossReference(int refnum, long offset, int generation) {
type = 0;
this.offset = offset;
this.refnum = refnum;
this.generation = generation;
}
/**
* Constructs a cross-reference element for a PdfIndirectObject.
*
* @param refnum the reference number
* @param offset byte offset of the object
*/
public PdfCrossReference(int refnum, long offset) {
type = 1;
this.offset = offset;
this.refnum = refnum;
this.generation = 0;
}
public PdfCrossReference(int type, int refnum, long offset, int generation) {
this.type = type;
this.offset = offset;
this.refnum = refnum;
this.generation = generation;
}
int getRefnum() {
return refnum;
}
/**
* Writes PDF representation of cross-reference entry to passed output stream.
*
* @param os Output stream this entry to write to
* @throws IOException If any I/O error occurs
*/
public void toPdf(OutputStream os) throws IOException {
// TODO: are generation number and 'In use' keyword bound that way?
final char inUse = generation == GENERATION_MAX ? 'f' : 'n';
os.write(getISOBytes(String.format(CROSS_REFERENCE_ENTRY_FORMAT, offset, generation, inUse)));
}
/**
* Writes PDF syntax to the OutputStream
*
* @param midSize the mid size
* @param os the OutputStream
* @throws IOException on error
*/
public void toPdf(int midSize, OutputStream os) throws IOException {
os.write((byte) type);
int thisMidSize = midSize;
while (--thisMidSize >= 0) {
os.write((byte) ((offset >>> (8 * thisMidSize)) & 0xff));
}
os.write((byte) ((generation >>> 8) & 0xff));
os.write((byte) (generation & 0xff));
}
/**
* Compares current {@link PdfCrossReference entry} with passed {@code reference} by PDF object number.
*/
@Override
public int compareTo(final PdfCrossReference reference) {
return Integer.compare(refnum, reference.refnum);
}
/**
* Checks if two entries are equal if their PDF object numbers are equal.
*
* @param obj Another cross-reference entry
* @return If null, not of type {@link PdfCrossReference} or object numbers are not equal, returns false;
* true otherwise
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PdfCrossReference)) {
return false;
}
PdfCrossReference other = (PdfCrossReference) obj;
return refnum == other.refnum;
}
@Override
public int hashCode() {
return refnum;
}
}
}
/**
* <CODE>PdfTrailer</CODE> is the PDF Trailer object.
* <p>
* This object is described in the 'Portable Document Format Reference Manual version 1.3' section 5.16 (page
* 59-60).
*/
static class PdfTrailer extends PdfDictionary {
// constructors
/**
* Constructs a PDF-Trailer.
*
* @param size the number of entries in the <CODE>PdfCrossReferenceTable</CODE>
* @param root an indirect reference to the root of the PDF document
* @param info an indirect reference to the info object of the PDF document
* @param encryption
* @param fileID
* @param prevxref
*/
PdfTrailer(int size, PdfIndirectReference root, PdfIndirectReference info, PdfIndirectReference encryption,
PdfObject fileID, int prevxref) {
put(PdfName.SIZE, new PdfNumber(size));
put(PdfName.ROOT, root);
if (info != null) {
put(PdfName.INFO, info);
}
if (encryption != null) {
put(PdfName.ENCRYPT, encryption);
}
if (fileID != null) {
put(PdfName.ID, fileID);
}
if (prevxref > 0) {
put(PdfName.PREV, new PdfNumber(prevxref));
}
}
/**
* Returns the PDF representation of this <CODE>PdfObject</CODE>.
*
* @param writer
* @param os
* @throws IOException
*/
@Override
public void toPdf(PdfWriter writer, OutputStream os) throws IOException {
os.write(getISOBytes("trailer\n"));
super.toPdf(null, os);
os.write('\n');
}
}
/**
* Adds a <CODE>BaseFont</CODE> to the document but not to the page resources. It is used for templates.
*
* @param bf the <CODE>BaseFont</CODE> to add
* @return an <CODE>Object[]</CODE> where position 0 is a <CODE>PdfName</CODE> and position 1 is an
* <CODE>PdfIndirectReference</CODE>
*/
FontDetails addSimple(BaseFont bf) {
FontDetails ret = documentFonts.get(bf);
if (ret == null) {
ret = new FontDetails(new PdfName("F" + (fontNumber++)), body.getPdfIndirectReference(), bf);
documentFonts.put(bf, ret);
}
return ret;
}
/**
* Adds a template to the document but not to the page resources.
*
* @param template the template to add
* @param forcedName the template name, rather than a generated one. Can be null
* @return the <CODE>PdfName</CODE> for this template
*/
PdfName addDirectTemplateSimple(PdfTemplate template, PdfName forcedName) {
PdfIndirectReference ref = template.getIndirectReference();
Object[] obj = formXObjects.get(ref);
PdfName name;
try {
if (obj == null) {
if (forcedName == null) {
name = new PdfName("Xf" + formXObjectsCounter);
++formXObjectsCounter;
} else {
name = forcedName;
}
formXObjects.put(ref, new Object[]{name, template});
} else {
name = (PdfName) obj[0];
}
} catch (Exception e) {
throw new ExceptionConverter(e);
}
return name;
}
/**
* Use this method to adds an image to the document but not to the page resources. It is used with templates and
* <CODE>Document.add(Image)</CODE>. Use this method only if you know what you're doing!
*
* @param image the <CODE>Image</CODE> to add
* @return the name of the image added
* @throws PdfException on error
* @throws DocumentException on error
*/
public PdfName addDirectImageSimple(Image image) throws DocumentException {
return addDirectImageSimple(image, null);
}
/**
* Adds an image to the document but not to the page resources. It is used with templates and
* <CODE>Document.add(Image)</CODE>. Use this method only if you know what you're doing!
*
* @param image the <CODE>Image</CODE> to add
* @param fixedRef the reference to used. It may be <CODE>null</CODE>, a <CODE>PdfIndirectReference</CODE> or a
* <CODE>PRIndirectReference</CODE>.
* @return the name of the image added
* @throws PdfException on error
* @throws DocumentException on error
*/
public PdfName addDirectImageSimple(Image image, PdfIndirectReference fixedRef) throws DocumentException {
PdfName name;
// if the images is already added, just retrieve the name
if (images.containsKey(image.getMySerialId())) {
name = images.get(image.getMySerialId());
} else {
// if it's a new image, add it to the document
if (image.isImgTemplate()) {
name = new PdfName("img" + images.size());
} else {
PdfIndirectReference dref = image.getDirectReference();
if (dref != null) {
PdfName rname = new PdfName("img" + images.size());
images.put(image.getMySerialId(), rname);
imageDictionary.put(rname, dref);
return rname;
}
Image maskImage = image.getImageMask();
PdfIndirectReference maskRef = null;
if (maskImage != null) {
PdfName mname = images.get(maskImage.getMySerialId());
maskRef = getImageReference(mname);
}
PdfImage i = new PdfImage(image, "img" + images.size(), maskRef);
add(i, fixedRef);
name = i.name();
}
images.put(image.getMySerialId(), name);
}
return name;
}
}