iText 5 itextpdf.jar Source Code

itextpdf.jar is a component in iText 5 Java library to provide core functionalities. iText Java library allows you to generate and manage PDF documents.

The Source Code files are provided at iText GitHub site.

You can compile it to generate your JAR file, using pom.xml as the build configuration file.

The source code of itextpdf-5.5.14.jar is provided below:

✍: FYIcenter.com

com/itextpdf/text/pdf/PdfReader.java

/*
 *
 * This file is part of the iText (R) project.
    Copyright (c) 1998-2020 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: sales@itextpdf.com
 */
package com.itextpdf.text.pdf;

import com.itextpdf.text.Document;
import com.itextpdf.text.ExceptionConverter;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.error_messages.MessageLocalization;
import com.itextpdf.text.exceptions.BadPasswordException;
import com.itextpdf.text.exceptions.InvalidPdfException;
import com.itextpdf.text.exceptions.UnsupportedPdfException;
import com.itextpdf.text.io.RandomAccessSource;
import com.itextpdf.text.io.RandomAccessSourceFactory;
import com.itextpdf.text.io.WindowRandomAccessSource;
import com.itextpdf.text.log.Counter;
import com.itextpdf.text.log.CounterFactory;
import com.itextpdf.text.log.Level;
import com.itextpdf.text.log.Logger;
import com.itextpdf.text.log.LoggerFactory;
import com.itextpdf.text.pdf.PRTokeniser.TokenType;
import com.itextpdf.text.pdf.interfaces.PdfViewerPreferences;
import com.itextpdf.text.pdf.internal.PdfViewerPreferencesImp;
import com.itextpdf.text.pdf.security.ExternalDecryptionProcess;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSEnvelopedData;
import org.bouncycastle.cms.RecipientInformation;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.Key;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.zip.InflaterInputStream;

/**
 * Reads a PDF document.
 * @author Paulo Soares
 * @author Kazuya Ujihara
 */
public class PdfReader implements PdfViewerPreferences {

	/**
	 * The iText developers are not responsible if you decide to change the
	 * value of this static parameter.
	 * @since 5.0.2
	 */
	public static boolean unethicalreading = false;
	
	public static boolean debugmode = false;
	private static final Logger LOGGER = LoggerFactory.getLogger(PdfReader.class);
	
    static final PdfName pageInhCandidates[] = {
        PdfName.MEDIABOX, PdfName.ROTATE, PdfName.RESOURCES, PdfName.CROPBOX
    };

    static final byte endstream[] = PdfEncodings.convertToBytes("endstream", null);
    static final byte endobj[] = PdfEncodings.convertToBytes("endobj", null);
    protected PRTokeniser tokens;
    // Each xref pair is a position
    // type 0 -> -1, 0
    // type 1 -> offset, 0
    // type 2 -> index, obj num
    protected long xref[];
    protected HashMap<Integer, IntHashtable> objStmMark;
    protected LongHashtable objStmToOffset;
    protected boolean newXrefType;
    protected ArrayList<PdfObject> xrefObj;
    PdfDictionary rootPages;
    protected PdfDictionary trailer;
    protected PdfDictionary catalog;
    protected PageRefs pageRefs;
    protected PRAcroForm acroForm = null;
    protected boolean acroFormParsed = false;
    protected boolean encrypted = false;
    protected boolean rebuilt = false;
    protected int freeXref;
    protected boolean tampered = false;
    protected long lastXref;
    protected long eofPos;
    protected char pdfVersion;
    protected PdfEncryption decrypt;
    protected byte password[] = null; //added by ujihara for decryption
    protected Key certificateKey = null; //added by Aiken Sam for certificate decryption
    protected Certificate certificate = null; //added by Aiken Sam for certificate decryption
    protected String certificateKeyProvider = null; //added by Aiken Sam for certificate decryption
    protected ExternalDecryptionProcess externalDecryptionProcess = null;
    private boolean ownerPasswordUsed;
    protected ArrayList<PdfString> strings = new ArrayList<PdfString>();
    protected boolean sharedStreams = true;
    protected boolean consolidateNamedDestinations = false;
    protected boolean remoteToLocalNamedDestinations = false;
    protected int rValue;
    protected long pValue;
    private int objNum;
    private int objGen;
    private long fileLength;
    private boolean hybridXref;
    private int lastXrefPartial = -1;
    private boolean partial;

    private PRIndirectReference cryptoRef;
	private final PdfViewerPreferencesImp viewerPreferences = new PdfViewerPreferencesImp();
    private boolean encryptionError;

    /**
     * Handler which will be used for decompression of pdf streams.
     */
    MemoryLimitsAwareHandler memoryLimitsAwareHandler = null;

    /**
     * Holds value of property appendable.
     */
    private boolean appendable;

	protected static Counter COUNTER = CounterFactory.getCounter(PdfReader.class);
	protected Counter getCounter() {
		return COUNTER;
	}
	
    /**
     * Constructs a new PdfReader.  This is the master constructor.
     * @param byteSource source of bytes for the reader
     * @param partialRead if true, the reader is opened in partial mode (PDF is parsed on demand), if false, the entire PDF is parsed into memory as the reader opens
     * @param ownerPassword the password or null if no password is required
     * @param certificate the certificate or null if no certificate is required
     * @param certificateKey the key or null if no certificate key is required
     * @param certificateKeyProvider the name of the key provider, or null if no key is required
     * @param externalDecryptionProcess
     * @param closeSourceOnConstructorError if true, the byteSource will be closed if there is an error during construction of this reader
     */
    private PdfReader(RandomAccessSource byteSource, boolean partialRead, byte ownerPassword[], Certificate certificate, Key certificateKey, String certificateKeyProvider, ExternalDecryptionProcess externalDecryptionProcess, boolean closeSourceOnConstructorError) throws IOException {
        this(byteSource, new ReaderProperties().setCertificate(certificate).setCertificateKey(certificateKey).setCertificateKeyProvider(certificateKeyProvider).setExternalDecryptionProcess(externalDecryptionProcess)
        .setOwnerPassword(ownerPassword).setPartialRead(partialRead).setCloseSourceOnconstructorError(closeSourceOnConstructorError));
    }


    /**
     * Constructs a new PdfReader.  This is the master constructor.
     * @param byteSource source of bytes for the reader
     * @param properties the properties which will be used to create the reader
     */
    private PdfReader(RandomAccessSource byteSource, ReaderProperties properties) throws IOException {
        this.certificate = properties.certificate;
        this.certificateKey = properties.certificateKey;
        this.certificateKeyProvider = properties.certificateKeyProvider;
        this.externalDecryptionProcess = properties.externalDecryptionProcess;
        this.password = properties.ownerPassword;
        this.partial = properties.partialRead;
        this.memoryLimitsAwareHandler = properties.memoryLimitsAwareHandler;
        try{

            tokens = getOffsetTokeniser(byteSource);

            if (partial){
                readPdfPartial();
            } else {
                readPdf();
            }
        } catch (IOException e){
            if (properties.closeSourceOnconstructorError)
                byteSource.close();
            throw e;
        }
        getCounter().read(fileLength);
    }

    /**
     * Reads and parses a PDF document.
     * @param filename the file name of the document
     * @throws IOException on error
     */
    public PdfReader(final String filename) throws IOException {
        this(filename, (byte[]) null);
    }

    /**
     * Reads and parses a PDF document.
     * @param properties the properties which will be used to create the reader
     * @param filename the file name of the document
     * @throws IOException on error
     */
    public PdfReader(ReaderProperties properties, final String filename) throws IOException {
        this(new RandomAccessSourceFactory()
                        .setForceRead(false)
                        .setUsePlainRandomAccess(Document.plainRandomAccess)
                        .createBestSource(filename),
                properties);
    }

    /**
     * Reads and parses a PDF document.
     * @param filename the file name of the document
     * @param ownerPassword the password to read the document
     * @throws IOException on error
     */
    public PdfReader(final String filename, final byte ownerPassword[]) throws IOException {
        this(new ReaderProperties().setOwnerPassword(ownerPassword), filename);
    }


    /**
     * Reads and parses a PDF document.
     * @param filename the file name of the document
     * @param ownerPassword the password to read the document
     * @param partial indicates if the reader needs to read the document only partially
     * @throws IOException on error
     */
    public PdfReader(final String filename, final byte ownerPassword[], boolean partial) throws IOException {
        this(new RandomAccessSourceFactory()
                .setForceRead(false)
                .setUsePlainRandomAccess(Document.plainRandomAccess)
                .createBestSource(filename), new ReaderProperties().setOwnerPassword(ownerPassword).setPartialRead(partial));
    }

    /**
     * Reads and parses a PDF document.
     * @param pdfIn the byte array with the document
     * @throws IOException on error
     */
    public PdfReader(final byte pdfIn[]) throws IOException {
        this(new RandomAccessSourceFactory().createSource(pdfIn), new ReaderProperties());
    }

    /**
     * Reads and parses a PDF document.
     * @param pdfIn the byte array with the document
     * @param ownerPassword the password to read the document
     * @throws IOException on error
     */
    public PdfReader(final byte pdfIn[], final byte ownerPassword[]) throws IOException {
        this(new RandomAccessSourceFactory().createSource(pdfIn), new ReaderProperties().setOwnerPassword(ownerPassword));
    }

    /**
     * Reads and parses a PDF document.
     * @param filename the file name of the document
     * @param certificate the certificate to read the document
     * @param certificateKey the private key of the certificate
     * @param certificateKeyProvider the security provider for certificateKey
     * @throws IOException on error
     */
    public PdfReader(final String filename, final Certificate certificate, final Key certificateKey, final String certificateKeyProvider) throws IOException {
        this(new RandomAccessSourceFactory()
    			.setForceRead(false)
    			.setUsePlainRandomAccess(Document.plainRandomAccess)
    			.createBestSource(filename),
    			new ReaderProperties().setCertificate(certificate).setCertificateKey(certificateKey).setCertificateKeyProvider(certificateKeyProvider));
    }


    /**
     * Reads and parses a PDF document.
     * @param filename the file name of the document
     * @param certificate
     * @param externalDecryptionProcess
     * @throws IOException on error
     */
    public PdfReader(final String filename, Certificate certificate, final ExternalDecryptionProcess externalDecryptionProcess) throws IOException {
        this(new RandomAccessSourceFactory()
                        .setForceRead(false)
                        .setUsePlainRandomAccess(Document.plainRandomAccess)
                        .createBestSource(filename),
                new ReaderProperties().setCertificate(certificate).setExternalDecryptionProcess(externalDecryptionProcess));
    }

    /**
     * Reads and parses a PDF document.
     *
     * @param pdfIn the document as a byte array
     * @param certificate
     * @param externalDecryptionProcess
     * @throws IOException on error
     */
    public PdfReader(final byte[] pdfIn, Certificate certificate, final ExternalDecryptionProcess externalDecryptionProcess) throws IOException {
        this(new RandomAccessSourceFactory()
                        .setForceRead(false)
                        .setUsePlainRandomAccess(Document.plainRandomAccess)
                        .createSource(pdfIn),
                new ReaderProperties().setCertificate(certificate).setExternalDecryptionProcess(externalDecryptionProcess));
    }

    /**
     * Reads and parses a PDF document.
     *
     * @param inputStream the PDF file
     * @param certificate
     * @param externalDecryptionProcess
     * @throws IOException on error
     */
    public PdfReader(final InputStream inputStream, final Certificate certificate, final ExternalDecryptionProcess externalDecryptionProcess) throws IOException {
        this(new RandomAccessSourceFactory().setForceRead(false).setUsePlainRandomAccess(Document.plainRandomAccess).createSource(inputStream),
                new ReaderProperties().setCertificate(certificate).setExternalDecryptionProcess(externalDecryptionProcess));
    }

    /**
     * Reads and parses a PDF document.
     * @param url the URL of the document
     * @throws IOException on error
     */
    public PdfReader(final URL url) throws IOException {
        this(new RandomAccessSourceFactory().createSource(url), new ReaderProperties());
    }

    /**
     * Reads and parses a PDF document.
     * @param url the URL of the document
     * @param ownerPassword the password to read the document
     * @throws IOException on error
     */
    public PdfReader(final URL url, final byte ownerPassword[]) throws IOException {
        this(new RandomAccessSourceFactory().createSource(url),
    			new ReaderProperties().setOwnerPassword(ownerPassword));
    }

    /**
     * Reads and parses a PDF document.
     * @param is the <CODE>InputStream</CODE> containing the document. The stream is read to the
     * end but is not closed
     * @param ownerPassword the password to read the document
     * @throws IOException on error
     */
    public PdfReader(final InputStream is, final byte ownerPassword[]) throws IOException {
        this(new RandomAccessSourceFactory().createSource(is),
    			new ReaderProperties().setOwnerPassword(ownerPassword).setCloseSourceOnconstructorError(false));
    	
    }

    /**
     * Reads and parses a PDF document.
     * @param is the <CODE>InputStream</CODE> containing the document. The stream is read to the
     * end but is not closed
     * @throws IOException on error
     */
    public PdfReader(final InputStream is) throws IOException {
        this(new RandomAccessSourceFactory().createSource(is), new ReaderProperties().setCloseSourceOnconstructorError(false));
    }

    /**
     * Reads and parses a PDF document.
     * @param properties the properties which will be used to create the reader
     * @param is the <CODE>InputStream</CODE> containing the document. The stream is read to the
     * end but is not closed
     * @throws IOException on error
     */
    public PdfReader(ReaderProperties properties, final InputStream is) throws IOException {
        this(new RandomAccessSourceFactory().createSource(is), properties);
    }

    /**
     * Reads and parses a PDF document.
     * @param properties the properties which will be used to create the reader
     * @param raf the document location
     * @throws IOException on error
     */
    public PdfReader(ReaderProperties properties, final RandomAccessFileOrArray raf) throws IOException {
        this(raf.getByteSource(), properties);
    }


    /**
     * Reads and parses a pdf document. Contrary to the other constructors only the xref is read
     * into memory. The reader is said to be working in "partial" mode as only parts of the pdf
     * are read as needed.
     * @param raf the document location
     * @param ownerPassword the password or <CODE>null</CODE> for no password
     * @throws IOException on error
     */
    public PdfReader(final RandomAccessFileOrArray raf, final byte ownerPassword[]) throws IOException {
        this(new ReaderProperties().setOwnerPassword(ownerPassword).setPartialRead(true).setCloseSourceOnconstructorError(false), raf);
    }

    /**
     * Reads and parses a pdf document.
     * @param raf the document location
     * @param ownerPassword the password or <CODE>null</CODE> for no password
     * @param partial indicates if the reader needs to read the document only partially. See {@link PdfReader#PdfReader(RandomAccessFileOrArray, byte[])}
     * @throws IOException on error
     */
    public PdfReader(final RandomAccessFileOrArray raf, final byte ownerPassword[], boolean partial) throws IOException {
        this(raf.getByteSource(), new ReaderProperties().setPartialRead(partial).setOwnerPassword(ownerPassword).setCloseSourceOnconstructorError(false));
    }

    /** Creates an independent duplicate.
     * @param reader the <CODE>PdfReader</CODE> to duplicate
     */
    public PdfReader(final PdfReader reader) {
        this.appendable = reader.appendable;
        this.consolidateNamedDestinations = reader.consolidateNamedDestinations;
        this.encrypted = reader.encrypted;
        this.rebuilt = reader.rebuilt;
        this.sharedStreams = reader.sharedStreams;
        this.tampered = reader.tampered;
        this.password = reader.password;
        this.pdfVersion = reader.pdfVersion;
        this.eofPos = reader.eofPos;
        this.freeXref = reader.freeXref;
        this.lastXref = reader.lastXref;
        this.newXrefType = reader.newXrefType;
        this.tokens = new PRTokeniser(reader.tokens.getSafeFile()); 
        if (reader.decrypt != null)
            this.decrypt = new PdfEncryption(reader.decrypt);
        this.pValue = reader.pValue;
        this.rValue = reader.rValue;
        this.xrefObj = new ArrayList<PdfObject>(reader.xrefObj);
        for (int k = 0; k < reader.xrefObj.size(); ++k) {
            this.xrefObj.set(k, duplicatePdfObject(reader.xrefObj.get(k), this));
        }
        this.pageRefs = new PageRefs(reader.pageRefs, this);
        this.trailer = (PdfDictionary)duplicatePdfObject(reader.trailer, this);
        this.catalog = trailer.getAsDict(PdfName.ROOT);
        this.rootPages = catalog.getAsDict(PdfName.PAGES);
        this.fileLength = reader.fileLength;
        this.partial = reader.partial;
        this.hybridXref = reader.hybridXref;
        this.objStmToOffset = reader.objStmToOffset;
        this.xref = reader.xref;
        this.cryptoRef = (PRIndirectReference)duplicatePdfObject(reader.cryptoRef, this);
        this.ownerPasswordUsed = reader.ownerPasswordUsed;
    }

    /**
     * Utility method that checks the provided byte source to see if it has junk bytes at the beginning.  If junk bytes
     * are found, construct a tokeniser that ignores the junk.  Otherwise, construct a tokeniser for the byte source as it is
     * @param byteSource the source to check
     * @return a tokeniser that is guaranteed to start at the PDF header
     * @throws IOException if there is a problem reading the byte source
     */
    private static PRTokeniser getOffsetTokeniser(RandomAccessSource byteSource) throws IOException{
    	PRTokeniser tok = new PRTokeniser(new RandomAccessFileOrArray(byteSource));
    	int offset = tok.getHeaderOffset();
    	if (offset != 0){
    		RandomAccessSource offsetSource = new WindowRandomAccessSource(byteSource, offset);
    		tok = new PRTokeniser(new RandomAccessFileOrArray(offsetSource));
    	}
    	return tok;
    }
    
    /** Gets a new file instance of the original PDF
     * document.
     * @return a new file instance of the original PDF document
     */
    public RandomAccessFileOrArray getSafeFile() {
        return tokens.getSafeFile();
    }

    protected PdfReaderInstance getPdfReaderInstance(final PdfWriter writer) {
        return new PdfReaderInstance(this, writer);
    }

    /** Gets the number of pages in the document.
     * Partial mode: return the value stored in the COUNT field of the pageref
     * Full mode: return the total number of pages found while loading in the entire document.
     * @return the number of pages in the document
     */
    public int getNumberOfPages() {
        return pageRefs.size();
    }

    /**
     * Returns the document's catalog. This dictionary is not a copy,
     * any changes will be reflected in the catalog.
     * @return the document's catalog
     */
    public PdfDictionary getCatalog() {
        return catalog;
    }

    /**
     * Returns the document's acroform, if it has one.
     * @return the document's acroform
     */
    public PRAcroForm getAcroForm() {
        if (!acroFormParsed) {
            acroFormParsed = true;
            PdfObject form = catalog.get(PdfName.ACROFORM);
            if (form != null) {
                try {
                    acroForm = new PRAcroForm(this);
                    acroForm.readAcroForm((PdfDictionary)getPdfObject(form));
                }
                catch (Exception e) {
                    acroForm = null;
                }
            }
        }
        return acroForm;
    }

    MemoryLimitsAwareHandler getMemoryLimitsAwareHandler() {
        return memoryLimitsAwareHandler;
    }

    /**
     * Gets the page rotation. This value can be 0, 90, 180 or 270.
     * @param index the page number. The first page is 1
     * @return the page rotation
     */
    public int getPageRotation(final int index) {
        return getPageRotation(pageRefs.getPageNRelease(index));
    }

    int getPageRotation(final PdfDictionary page) {
        PdfNumber rotate = page.getAsNumber(PdfName.ROTATE);
        if (rotate == null)
            return 0;
        else {
            int n = rotate.intValue();
            n %= 360;
            return n < 0 ? n + 360 : n;
        }
    }
    /** Gets the page size, taking rotation into account. This
     * is a <CODE>Rectangle</CODE> with the value of the /MediaBox and the /Rotate key.
     * @param index the page number. The first page is 1
     * @return a <CODE>Rectangle</CODE>
     */
    public Rectangle getPageSizeWithRotation(final int index) {
        return getPageSizeWithRotation(pageRefs.getPageNRelease(index));
    }

    /**
     * Gets the rotated page from a page dictionary.
     * @param page the page dictionary
     * @return the rotated page
     */
    public Rectangle getPageSizeWithRotation(final PdfDictionary page) {
        Rectangle rect = getPageSize(page);
        int rotation = getPageRotation(page);
        while (rotation > 0) {
            rect = rect.rotate();
            rotation -= 90;
        }
        return rect;
    }

    /** Gets the page size without taking rotation into account. This
     * is the value of the /MediaBox key.
     * @param index the page number. The first page is 1
     * @return the page size
     */
    public Rectangle getPageSize(final int index) {
        return getPageSize(pageRefs.getPageNRelease(index));
    }

    /**
     * Gets the page from a page dictionary
     * @param page the page dictionary
     * @return the page
     */
    public Rectangle getPageSize(final PdfDictionary page) {
        PdfArray mediaBox = page.getAsArray(PdfName.MEDIABOX);
        return getNormalizedRectangle(mediaBox);
    }

    /** Gets the crop box without taking rotation into account. This
     * is the value of the /CropBox key. The crop box is the part
     * of the document to be displayed or printed. It usually is the same
     * as the media box but may be smaller. If the page doesn't have a crop
     * box the page size will be returned.
     * @param index the page number. The first page is 1
     * @return the crop box
     */
    public Rectangle getCropBox(final int index) {
        PdfDictionary page = pageRefs.getPageNRelease(index);
        PdfArray cropBox = (PdfArray)getPdfObjectRelease(page.get(PdfName.CROPBOX));
        if (cropBox == null)
            return getPageSize(page);
        return getNormalizedRectangle(cropBox);
    }

    /** Gets the box size. Allowed names are: "crop", "trim", "art", "bleed" and "media".
     * @param index the page number. The first page is 1
     * @param boxName the box name
     * @return the box rectangle or null
     */
    public Rectangle getBoxSize(final int index, final String boxName) {
        PdfDictionary page = pageRefs.getPageNRelease(index);
        PdfArray box = null;
        if (boxName.equals("trim"))
            box = (PdfArray)getPdfObjectRelease(page.get(PdfName.TRIMBOX));
        else if (boxName.equals("art"))
            box = (PdfArray)getPdfObjectRelease(page.get(PdfName.ARTBOX));
        else if (boxName.equals("bleed"))
            box = (PdfArray)getPdfObjectRelease(page.get(PdfName.BLEEDBOX));
        else if (boxName.equals("crop"))
            box = (PdfArray)getPdfObjectRelease(page.get(PdfName.CROPBOX));
        else if (boxName.equals("media"))
            box = (PdfArray)getPdfObjectRelease(page.get(PdfName.MEDIABOX));
        if (box == null)
            return null;
        return getNormalizedRectangle(box);
    }

    /**
     * Returns the content of the document information dictionary as a <CODE>HashMap</CODE>
     * of <CODE>String</CODE>.
     * @return content of the document information dictionary
     */
    public HashMap<String, String> getInfo() {
        HashMap<String, String> map = new HashMap<String, String>();
        PdfDictionary info = trailer.getAsDict(PdfName.INFO);
        if (info == null)
            return map;
        for (Object element : info.getKeys()) {
            PdfName key = (PdfName)element;
            PdfObject obj = getPdfObject(info.get(key));
            if (obj == null)
                continue;
            String value = obj.toString();
            switch (obj.type()) {
                case PdfObject.STRING: {
                    value = ((PdfString)obj).toUnicodeString();
                    break;
                }
                case PdfObject.NAME: {
                    value = PdfName.decodeName(value);
                    break;
                }
            }
            map.put(PdfName.decodeName(key.toString()), value);
        }
        return map;
    }

    /** Normalizes a <CODE>Rectangle</CODE> so that llx and lly are smaller than urx and ury.
     * @param box the original rectangle
     * @return a normalized <CODE>Rectangle</CODE>
     */
    public static Rectangle getNormalizedRectangle(final PdfArray box) {
        float llx = ((PdfNumber)getPdfObjectRelease(box.getPdfObject(0))).floatValue();
        float lly = ((PdfNumber)getPdfObjectRelease(box.getPdfObject(1))).floatValue();
        float urx = ((PdfNumber)getPdfObjectRelease(box.getPdfObject(2))).floatValue();
        float ury = ((PdfNumber)getPdfObjectRelease(box.getPdfObject(3))).floatValue();
        return new Rectangle(Math.min(llx, urx), Math.min(lly, ury),
        Math.max(llx, urx), Math.max(lly, ury));
    }
    
    /**
     * Checks if the PDF is a tagged PDF.
     */
    public boolean isTagged() {
    	PdfDictionary markInfo = catalog.getAsDict(PdfName.MARKINFO);
    	if (markInfo == null)
    		return false;
    	if ( PdfBoolean.PDFTRUE.equals(markInfo.getAsBoolean(PdfName.MARKED))) {
            return catalog.getAsDict(PdfName.STRUCTTREEROOT) != null;
        } else {
            return false;
        }
    }

    /**
     * Parses the entire PDF
     */
    protected void readPdf() throws IOException {
        fileLength = tokens.getFile().length();
        pdfVersion = tokens.checkPdfHeader();
        if (null == memoryLimitsAwareHandler) {
            memoryLimitsAwareHandler = new MemoryLimitsAwareHandler(fileLength);
        }
        try {
            readXref();
        }
        catch (Exception e) {
            try {
                rebuilt = true;
                rebuildXref();
                lastXref = -1;
            }
            catch (Exception ne) {
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("rebuild.failed.1.original.message.2", ne.getMessage(), e.getMessage()));
            }
        }
        try {
            readDocObj();
        }
        catch (Exception e) {
        	if (e instanceof BadPasswordException)
        		throw new BadPasswordException(e.getMessage());
            if (rebuilt || encryptionError)
                throw new InvalidPdfException(e.getMessage());
            rebuilt = true;
            encrypted = false;
            try{
                rebuildXref();
                lastXref = -1;
                readDocObj();
            } catch (Exception ne){
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("rebuild.failed.1.original.message.2", ne.getMessage(), e.getMessage()));
            }
        }
        strings.clear();
        readPages();
        //eliminateSharedStreams();
        removeUnusedObjects();

    }
    /**
     * Partially parses the pdf
     *
     * */

    protected void readPdfPartial() throws IOException {
        fileLength = tokens.getFile().length();
        pdfVersion = tokens.checkPdfHeader();
        if (null == memoryLimitsAwareHandler) {
            memoryLimitsAwareHandler = new MemoryLimitsAwareHandler(fileLength);
        }
        try {
            readXref();
        }
        catch (Exception e) {
			try {
				rebuilt = true;
				rebuildXref();
				lastXref = -1;
			} catch (Exception ne) {
				throw new InvalidPdfException(
						MessageLocalization.getComposedMessage(
								"rebuild.failed.1.original.message.2",
								ne.getMessage(), e.getMessage()), ne);
			}
		}
		readDocObjPartial();
		readPages();
    }

    private boolean equalsArray(final byte ar1[], final byte ar2[], final int size) {
        for (int k = 0; k < size; ++k) {
            if (ar1[k] != ar2[k])
                return false;
        }
        return true;
    }

    /**
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    private void readDecryptedDocObj() throws IOException {
        if (encrypted)
            return;
        PdfObject encDic = trailer.get(PdfName.ENCRYPT);
        if (encDic == null || encDic.toString().equals("null"))
            return;
        encryptionError = true;
        byte[] encryptionKey = null;
        encrypted = true;
        PdfDictionary enc = (PdfDictionary)getPdfObject(encDic);
        //This string of condidions is to determine whether or not the authevent for this PDF is EFOPEN
        //If it is, we return since the attachments of the PDF are what are encrypted, not the PDF itself.  
        //Without this check we run into a bad password exception when trying to open documents that have an
        //auth event type of EFOPEN.  
        PdfDictionary cfDict = enc.getAsDict(PdfName.CF);
        if(cfDict != null){
        	PdfDictionary stdCFDict = cfDict.getAsDict(PdfName.STDCF);
        	if(stdCFDict != null){
        		PdfName authEvent = stdCFDict.getAsName(PdfName.AUTHEVENT);
        		if(authEvent != null){
        			//Return only if the event is EFOPEN and there is no password so that 
        			//attachments that are encrypted can still be opened.
        			if(authEvent.compareTo(PdfName.EFOPEN) == 0 && !this.ownerPasswordUsed)
        				return;
        		}
        	}
        }
        String s;
        PdfObject o;

        PdfArray documentIDs = trailer.getAsArray(PdfName.ID);
        byte documentID[] = null;
        if (documentIDs != null) {
            o = documentIDs.getPdfObject(0);
            strings.remove(o);
            s = o.toString();
            documentID = com.itextpdf.text.DocWriter.getISOBytes(s);
            if (documentIDs.size() > 1)
                strings.remove(documentIDs.getPdfObject(1));
        }
        // just in case we have a broken producer
        if (documentID == null)
            documentID = new byte[0];
        byte uValue[] = null;
        byte oValue[] = null;
        int cryptoMode = PdfWriter.STANDARD_ENCRYPTION_40;
        int lengthValue = 0;

        PdfObject filter = getPdfObjectRelease(enc.get(PdfName.FILTER));

        if (filter.equals(PdfName.STANDARD)) {
            s = enc.get(PdfName.U).toString();
            strings.remove(enc.get(PdfName.U));
            uValue = com.itextpdf.text.DocWriter.getISOBytes(s);
            s = enc.get(PdfName.O).toString();
            strings.remove(enc.get(PdfName.O));
            oValue = com.itextpdf.text.DocWriter.getISOBytes(s);
            if (enc.contains(PdfName.OE))
                strings.remove(enc.get(PdfName.OE));
            if (enc.contains(PdfName.UE))
                strings.remove(enc.get(PdfName.UE));
            if (enc.contains(PdfName.PERMS))
                strings.remove(enc.get(PdfName.PERMS));

            o = enc.get(PdfName.P);
            if (!o.isNumber())
            	throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.p.value"));
            pValue = ((PdfNumber)o).longValue();

            o = enc.get(PdfName.R);
            if (!o.isNumber())
            	throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.r.value"));
            rValue = ((PdfNumber)o).intValue();

            switch (rValue) {
            case 2:
            	cryptoMode = PdfWriter.STANDARD_ENCRYPTION_40;
            	break;
            case 3:
                o = enc.get(PdfName.LENGTH);
                if (!o.isNumber())
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.length.value"));
                lengthValue = ( (PdfNumber) o).intValue();
                if (lengthValue > 128 || lengthValue < 40 || lengthValue % 8 != 0)
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.length.value"));
                cryptoMode = PdfWriter.STANDARD_ENCRYPTION_128;
                break;
            case 4:
                PdfDictionary dic = (PdfDictionary)enc.get(PdfName.CF);
                if (dic == null)
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("cf.not.found.encryption"));
                dic = (PdfDictionary)dic.get(PdfName.STDCF);
                if (dic == null)
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("stdcf.not.found.encryption"));
                if (PdfName.V2.equals(dic.get(PdfName.CFM)))
                    cryptoMode = PdfWriter.STANDARD_ENCRYPTION_128;
                else if (PdfName.AESV2.equals(dic.get(PdfName.CFM)))
                    cryptoMode = PdfWriter.ENCRYPTION_AES_128;
                else
                    throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("no.compatible.encryption.found"));
                PdfObject em = enc.get(PdfName.ENCRYPTMETADATA);
                if (em != null && em.toString().equals("false"))
                    cryptoMode |= PdfWriter.DO_NOT_ENCRYPT_METADATA;
                break;
            case 5:
                cryptoMode = PdfWriter.ENCRYPTION_AES_256;
                PdfObject em5 = enc.get(PdfName.ENCRYPTMETADATA);
                if (em5 != null && em5.toString().equals("false"))
                    cryptoMode |= PdfWriter.DO_NOT_ENCRYPT_METADATA;
                break;
            default:
            	throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("unknown.encryption.type.r.eq.1", rValue));
            }
        }
        else if (filter.equals(PdfName.PUBSEC)) {
            boolean foundRecipient = false;
            byte[] envelopedData = null;
            PdfArray recipients = null;

            o = enc.get(PdfName.V);
            if (!o.isNumber())
            	throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.v.value"));
            int vValue = ((PdfNumber)o).intValue();
            switch(vValue) {
            case 1:
                cryptoMode = PdfWriter.STANDARD_ENCRYPTION_40;
                lengthValue = 40;
                recipients = (PdfArray)enc.get(PdfName.RECIPIENTS);
            	break;
            case 2:
                o = enc.get(PdfName.LENGTH);
                if (!o.isNumber())
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.length.value"));
                lengthValue = ( (PdfNumber) o).intValue();
                if (lengthValue > 128 || lengthValue < 40 || lengthValue % 8 != 0)
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.length.value"));
                cryptoMode = PdfWriter.STANDARD_ENCRYPTION_128;
                recipients = (PdfArray)enc.get(PdfName.RECIPIENTS);
                break;
            case 4:
            case 5:
                PdfDictionary dic = (PdfDictionary)enc.get(PdfName.CF);
                if (dic == null)
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("cf.not.found.encryption"));
                dic = (PdfDictionary)dic.get(PdfName.DEFAULTCRYPTFILTER);
                if (dic == null)
                    throw new InvalidPdfException(MessageLocalization.getComposedMessage("defaultcryptfilter.not.found.encryption"));
                if (PdfName.V2.equals(dic.get(PdfName.CFM))) {
                    cryptoMode = PdfWriter.STANDARD_ENCRYPTION_128;
                    lengthValue = 128;
                }
                else if (PdfName.AESV2.equals(dic.get(PdfName.CFM))) {
                    cryptoMode = PdfWriter.ENCRYPTION_AES_128;
                    lengthValue = 128;
                }
                else if (PdfName.AESV3.equals(dic.get(PdfName.CFM))) {
                    cryptoMode = PdfWriter.ENCRYPTION_AES_256;
                    lengthValue = 256;
                }
                else
                    throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("no.compatible.encryption.found"));
                PdfObject em = dic.get(PdfName.ENCRYPTMETADATA);
                if (em != null && em.toString().equals("false"))
                    cryptoMode |= PdfWriter.DO_NOT_ENCRYPT_METADATA;

                recipients = (PdfArray)dic.get(PdfName.RECIPIENTS);
                break;
            default:
            	throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("unknown.encryption.type.v.eq.1", vValue));
            }
            X509CertificateHolder certHolder;
            try {
                certHolder = new X509CertificateHolder(certificate.getEncoded());
            }
            catch (Exception f) {
                throw new ExceptionConverter(f);
            }
            if (externalDecryptionProcess == null) {
                for (int i = 0; i < recipients.size(); i++) {
                    PdfObject recipient = recipients.getPdfObject(i);
                    strings.remove(recipient);

                    CMSEnvelopedData data = null;
                    try {
                        data = new CMSEnvelopedData(recipient.getBytes());

                        Iterator<RecipientInformation> recipientCertificatesIt = data.getRecipientInfos().getRecipients().iterator();

                        while (recipientCertificatesIt.hasNext()) {
                            RecipientInformation recipientInfo = recipientCertificatesIt.next();

                            if (recipientInfo.getRID().match(certHolder) && !foundRecipient) {
                                envelopedData = PdfEncryptor.getContent(recipientInfo, (PrivateKey) certificateKey, certificateKeyProvider);
                                foundRecipient = true;
                            }
                        }

                    } catch (Exception f) {
                        throw new ExceptionConverter(f);
                    }
                }
            } else {
                for (int i = 0; i < recipients.size(); i++) {
                    PdfObject recipient = recipients.getPdfObject(i);
                    strings.remove(recipient);

                    CMSEnvelopedData data = null;
                    try {
                        data = new CMSEnvelopedData(recipient.getBytes());

                        RecipientInformation recipientInfo =
                                data.getRecipientInfos().get(externalDecryptionProcess.getCmsRecipientId());

                        if (recipientInfo != null) {
                            envelopedData =
                                    recipientInfo.getContent(externalDecryptionProcess.getCmsRecipient());
                            foundRecipient = true;
                        }
                    } catch (Exception f) {
                        throw new ExceptionConverter(f);
                    }
                }
            }

            if(!foundRecipient || envelopedData == null) {
                throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("bad.certificate.and.key"));
            }

            MessageDigest md = null;

            try {
                if ((cryptoMode & PdfWriter.ENCRYPTION_MASK)  == PdfWriter.ENCRYPTION_AES_256)
                    md = MessageDigest.getInstance("SHA-256");
                else
                    md = MessageDigest.getInstance("SHA-1");
                md.update(envelopedData, 0, 20);
                for (int i = 0; i<recipients.size(); i++) {
                  byte[] encodedRecipient = recipients.getPdfObject(i).getBytes();
                  md.update(encodedRecipient);
                }
                if ((cryptoMode & PdfWriter.DO_NOT_ENCRYPT_METADATA) != 0)
                    md.update(new byte[]{(byte)255, (byte)255, (byte)255, (byte)255});
                encryptionKey = md.digest();
            }
            catch (Exception f) {
                throw new ExceptionConverter(f);
            }
        }


        decrypt = new PdfEncryption();
        decrypt.setCryptoMode(cryptoMode, lengthValue);

        if (filter.equals(PdfName.STANDARD)) {
            if (rValue == 5) {
                ownerPasswordUsed = decrypt.readKey(enc, password);
                decrypt.documentID = documentID;
                pValue = decrypt.getPermissions();
            }
            else {
                //check by owner password
                decrypt.setupByOwnerPassword(documentID, password, uValue, oValue, pValue);
                if (!equalsArray(uValue, decrypt.userKey, rValue == 3 || rValue == 4 ? 16 : 32)) {
                    //check by user password
                    decrypt.setupByUserPassword(documentID, password, oValue, pValue);
                    if (!equalsArray(uValue, decrypt.userKey, rValue == 3 || rValue == 4 ? 16 : 32)) {
                        throw new BadPasswordException(MessageLocalization.getComposedMessage("bad.user.password"));
                    }
                }
                else
                    ownerPasswordUsed = true;
            }
        }
        else if (filter.equals(PdfName.PUBSEC)) {
            decrypt.documentID = documentID;
            if ((cryptoMode & PdfWriter.ENCRYPTION_MASK) == PdfWriter.ENCRYPTION_AES_256)
                decrypt.setKey(encryptionKey);
            else
                decrypt.setupByEncryptionKey(encryptionKey, lengthValue);
            ownerPasswordUsed = true;
        }

        for (int k = 0; k < strings.size(); ++k) {
            PdfString str = strings.get(k);
            str.decrypt(this);
        }

        if (encDic.isIndirect()) {
            cryptoRef = (PRIndirectReference)encDic;
            xrefObj.set(cryptoRef.getNumber(), null);
        }
        encryptionError = false;
    }

    /**
     * @param obj
     * @return a PdfObject
     */
    public static PdfObject getPdfObjectRelease(final PdfObject obj) {
        PdfObject obj2 = getPdfObject(obj);
        releaseLastXrefPartial(obj);
        return obj2;
    }


    /**
     * Reads a <CODE>PdfObject</CODE> resolving an indirect reference
     * if needed.
     * @param obj the <CODE>PdfObject</CODE> to read
     * @return the resolved <CODE>PdfObject</CODE>
     */
    public static PdfObject getPdfObject(PdfObject obj) {
        if (obj == null)
            return null;
        if (!obj.isIndirect())
            return obj;
        try {
            PRIndirectReference ref = (PRIndirectReference)obj;
            int idx = ref.getNumber();
            boolean appendable = ref.getReader().appendable;
            obj = ref.getReader().getPdfObject(idx);
            if (obj == null) {
                return null;
            }
            else {
                if (appendable) {
                    switch (obj.type()) {
                        case PdfObject.NULL:
                            obj = new PdfNull();
                            break;
                        case PdfObject.BOOLEAN:
                            obj = new PdfBoolean(((PdfBoolean)obj).booleanValue());
                            break;
                        case PdfObject.NAME:
                            obj = new PdfName(obj.getBytes());
                            break;
                    }
                    obj.setIndRef(ref);
                }
                return obj;
            }
        }
        catch (Exception e) {
            throw new ExceptionConverter(e);
        }
    }

    /**
     * Reads a <CODE>PdfObject</CODE> resolving an indirect reference
     * if needed. If the reader was opened in partial mode the object will be released
     * to save memory.
     * @param obj the <CODE>PdfObject</CODE> to read
     * @param parent
     * @return a PdfObject
     */
    public static PdfObject getPdfObjectRelease(final PdfObject obj, final PdfObject parent) {
        PdfObject obj2 = getPdfObject(obj, parent);
        releaseLastXrefPartial(obj);
        return obj2;
    }

    /**
     * @param obj
     * @param parent
     * @return a PdfObject
     */
    public static PdfObject getPdfObject(PdfObject obj, final PdfObject parent) {
        if (obj == null)
            return null;
        if (!obj.isIndirect()) {
            PRIndirectReference ref = null;
            if (parent != null && (ref = parent.getIndRef()) != null && ref.getReader().isAppendable()) {
                switch (obj.type()) {
                    case PdfObject.NULL:
                        obj = new PdfNull();
                        break;
                    case PdfObject.BOOLEAN:
                        obj = new PdfBoolean(((PdfBoolean)obj).booleanValue());
                        break;
                    case PdfObject.NAME:
                        obj = new PdfName(obj.getBytes());
                        break;
                }
                obj.setIndRef(ref);
            }
            return obj;
        }
        return getPdfObject(obj);
    }

    /**
     * @param idx
     * @return a PdfObject
     */
    public PdfObject getPdfObjectRelease(final int idx) {
        PdfObject obj = getPdfObject(idx);
        releaseLastXrefPartial();
        return obj;
    }

    /**
     * @param idx
     * @return aPdfObject
     */
    public PdfObject getPdfObject(final int idx) {
        try {
            lastXrefPartial = -1;
            if (idx < 0 || idx >= xrefObj.size())
                return null;
            PdfObject obj = xrefObj.get(idx);
            if (!partial || obj != null)
                return obj;
            if (idx * 2 >= xref.length)
                return null;
            obj = readSingleObject(idx);
            lastXrefPartial = -1;
            if (obj != null)
                lastXrefPartial = idx;
            return obj;
        }
        catch (Exception e) {
            throw new ExceptionConverter(e);
        }
    }

    /**
     *
     */
    public void resetLastXrefPartial() {
        lastXrefPartial = -1;
    }

    /**
     *
     */
    public void releaseLastXrefPartial() {
        if (partial && lastXrefPartial != -1) {
            xrefObj.set(lastXrefPartial, null);
            lastXrefPartial = -1;
        }
    }

    /**
     * @param obj
     */
    public static void releaseLastXrefPartial(final PdfObject obj) {
        if (obj == null)
            return;
        if (!obj.isIndirect())
            return;
        if (!(obj instanceof PRIndirectReference))
            return;

        PRIndirectReference ref = (PRIndirectReference)obj;
        PdfReader reader = ref.getReader();
        if (reader.partial && reader.lastXrefPartial != -1 && reader.lastXrefPartial == ref.getNumber()) {
            reader.xrefObj.set(reader.lastXrefPartial, null);
        }
        reader.lastXrefPartial = -1;
    }

    private void setXrefPartialObject(final int idx, final PdfObject obj) {
        if (!partial || idx < 0)
            return;
        xrefObj.set(idx, obj);
    }

    /**
     * @param obj
     * @return an indirect reference
     */
    public PRIndirectReference addPdfObject(final PdfObject obj) {
        xrefObj.add(obj);
        return new PRIndirectReference(this, xrefObj.size() - 1);
    }

    protected void readPages() throws IOException {
        catalog = trailer.getAsDict(PdfName.ROOT);
        if (catalog == null) {
            throw new InvalidPdfException(MessageLocalization.getComposedMessage("the.document.has.no.catalog.object"));
        }
        rootPages = catalog.getAsDict(PdfName.PAGES);
        if (rootPages == null || (!PdfName.PAGES.equals(rootPages.get(PdfName.TYPE)) && !PdfName.PAGES.equals(rootPages.get(new PdfName("Types"))))) {
            if (debugmode) {
                if ( LOGGER.isLogging(Level.ERROR) ) {
                    LOGGER.error(MessageLocalization.getComposedMessage("the.document.has.no.page.root"));
                }
            }
            else {
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("the.document.has.no.page.root"));
            }
        }
        pageRefs = new PageRefs(this);
    }

    protected void readDocObjPartial() throws IOException {
        xrefObj = new ArrayList<PdfObject>(xref.length / 2);
        xrefObj.addAll(Collections.<PdfObject>nCopies(xref.length / 2, null));
        readDecryptedDocObj();
        if (objStmToOffset != null) {
            long keys[] = objStmToOffset.getKeys();
            for (int k = 0; k < keys.length; ++k) {
                long n = keys[k];
                objStmToOffset.put(n, xref[(int)(n * 2)]);
                xref[(int)(n * 2)] = -1;
            }
        }
    }

    protected PdfObject readSingleObject(final int k) throws IOException {
        strings.clear();
        int k2 = k * 2;
        long pos = xref[k2];
        if (pos < 0)
            return null;
        if (xref[k2 + 1] > 0)
            pos = objStmToOffset.get(xref[k2 + 1]);
        if (pos == 0)
            return null;
        tokens.seek(pos);
        tokens.nextValidToken();
        if (tokens.getTokenType() != TokenType.NUMBER)
            tokens.throwError(MessageLocalization.getComposedMessage("invalid.object.number"));
        objNum = tokens.intValue();
        tokens.nextValidToken();
        if (tokens.getTokenType() != TokenType.NUMBER)
            tokens.throwError(MessageLocalization.getComposedMessage("invalid.generation.number"));
        objGen = tokens.intValue();
        tokens.nextValidToken();
        if (!tokens.getStringValue().equals("obj"))
            tokens.throwError(MessageLocalization.getComposedMessage("token.obj.expected"));
        PdfObject obj;
        try {
            obj = readPRObject();
            for (int j = 0; j < strings.size(); ++j) {
                PdfString str = strings.get(j);
                str.decrypt(this);
            }
            if (obj.isStream()) {
                checkPRStreamLength((PRStream)obj);
            }
        }
        catch (IOException e) {
        	if (debugmode) {
                if (LOGGER.isLogging(Level.ERROR))
                    LOGGER.error(e.getMessage(), e);
        		obj = null;
        	}
        	else
        		throw e;
        }
        if (xref[k2 + 1] > 0) {
            obj = readOneObjStm((PRStream)obj, (int)xref[k2]);
        }
        xrefObj.set(k, obj);
        return obj;
    }

    protected PdfObject readOneObjStm(final PRStream stream, int idx) throws IOException {
        int first = stream.getAsNumber(PdfName.FIRST).intValue();
        byte b[] = getStreamBytes(stream, tokens.getFile());
        PRTokeniser saveTokens = tokens;
        tokens = new PRTokeniser(new RandomAccessFileOrArray(new RandomAccessSourceFactory().createSource(b)));
        try {
            int address = 0;
            boolean ok = true;
            ++idx;
            for (int k = 0; k < idx; ++k) {
                ok = tokens.nextToken();
                if (!ok)
                    break;
                if (tokens.getTokenType() != TokenType.NUMBER) {
                    ok = false;
                    break;
                }
                ok = tokens.nextToken();
                if (!ok)
                    break;
                if (tokens.getTokenType() != TokenType.NUMBER) {
                    ok = false;
                    break;
                }
                address = tokens.intValue() + first;
            }
            if (!ok)
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("error.reading.objstm"));
            tokens.seek(address);
            tokens.nextToken();
            PdfObject obj;
            if (tokens.getTokenType() == PRTokeniser.TokenType.NUMBER) {
                obj = new PdfNumber(tokens.getStringValue());
            }
            else {
                tokens.seek(address);
                obj = readPRObject();
            }
            return obj;
            //return readPRObject();
        }
        finally {
            tokens = saveTokens;
        }
    }

    /**
     * @return the percentage of the cross reference table that has been read
     */
    public double dumpPerc() {
        int total = 0;
        for (int k = 0; k < xrefObj.size(); ++k) {
            if (xrefObj.get(k) != null)
                ++total;
        }
        return total * 100.0 / xrefObj.size();
    }

    protected void readDocObj() throws IOException {
        ArrayList<PRStream> streams = new ArrayList<PRStream>();
        xrefObj = new ArrayList<PdfObject>(xref.length / 2);
        xrefObj.addAll(Collections.<PdfObject>nCopies(xref.length / 2, null));
        for (int k = 2; k < xref.length; k += 2) {
            long pos = xref[k];
            if (pos <= 0 || xref[k + 1] > 0)
                continue;
            tokens.seek(pos);
            tokens.nextValidToken();
            if (tokens.getTokenType() != TokenType.NUMBER)
                tokens.throwError(MessageLocalization.getComposedMessage("invalid.object.number"));
            objNum = tokens.intValue();
            tokens.nextValidToken();
            if (tokens.getTokenType() != TokenType.NUMBER)
                tokens.throwError(MessageLocalization.getComposedMessage("invalid.generation.number"));
            objGen = tokens.intValue();
            tokens.nextValidToken();
            if (!tokens.getStringValue().equals("obj"))
                tokens.throwError(MessageLocalization.getComposedMessage("token.obj.expected"));
            PdfObject obj;
            try {
                obj = readPRObject();
                if (obj.isStream()) {
                    streams.add((PRStream)obj);
                }
            }
            catch (IOException e) {
            	if (debugmode) {
                    if (LOGGER.isLogging(Level.ERROR))
                        LOGGER.error(e.getMessage(), e);
            		obj = null;
            	}
            	else
            		throw e;
            }
            xrefObj.set(k / 2, obj);
        }
        for (int k = 0; k < streams.size(); ++k) {
            checkPRStreamLength(streams.get(k));
        }
        readDecryptedDocObj();
        if (objStmMark != null) {
            for (Map.Entry<Integer, IntHashtable>entry: objStmMark.entrySet()) {
                int n = entry.getKey().intValue();
                IntHashtable h = entry.getValue();
                readObjStm((PRStream)xrefObj.get(n), h);
                xrefObj.set(n, null);
            }
            objStmMark = null;
        }
        xref = null;
    }

    private void checkPRStreamLength(final PRStream stream) throws IOException {
        long fileLength = tokens.length();
        long start = stream.getOffset();
        boolean calc = false;
        long streamLength = 0;
        PdfObject obj = getPdfObjectRelease(stream.get(PdfName.LENGTH));
        if (obj != null && obj.type() == PdfObject.NUMBER) {
            streamLength = ((PdfNumber)obj).intValue();
            if (streamLength + start > fileLength - 20)
                calc = true;
            else {
                tokens.seek(start + streamLength);
                String line = tokens.readString(20);
                if (!line.startsWith("\nendstream") &&
                !line.startsWith("\r\nendstream") &&
                !line.startsWith("\rendstream") &&
                !line.startsWith("endstream"))
                    calc = true;
            }
        }
        else
            calc = true;
        if (calc) {
            byte tline[] = new byte[16];
            tokens.seek(start);
            long pos;
            while (true) {
                pos = tokens.getFilePointer();
                if (!tokens.readLineSegment(tline, false)) // added boolean because of mailing list issue (17 Feb. 2014)
                    break;
                if (equalsn(tline, endstream)) {
                    streamLength = pos - start;
                    break;
                }
                if (equalsn(tline, endobj)) {
                    tokens.seek(pos - 16);
                    String s = tokens.readString(16);
                    int index = s.indexOf("endstream");
                    if (index >= 0)
                        pos = pos - 16 + index;
                    streamLength = pos - start;
                    break;
                }
            }
            tokens.seek(pos - 2);
            if (tokens.read() == 13)
            	streamLength--;
            tokens.seek(pos - 1);
            if (tokens.read() == 10)
            	streamLength--;

            if ( streamLength < 0 ) {
                streamLength = 0;
            }
        }
        stream.setLength((int)streamLength);
    }

    protected void readObjStm(final PRStream stream, final IntHashtable map) throws IOException {
        if (stream == null) return;
        int first = stream.getAsNumber(PdfName.FIRST).intValue();
        int n = stream.getAsNumber(PdfName.N).intValue();
        byte b[] = getStreamBytes(stream, tokens.getFile());
        PRTokeniser saveTokens = tokens;
        tokens = new PRTokeniser(new RandomAccessFileOrArray(new RandomAccessSourceFactory().createSource(b)));
        try {
            int address[] = new int[n];
            int objNumber[] = new int[n];
            boolean ok = true;
            for (int k = 0; k < n; ++k) {
                ok = tokens.nextToken();
                if (!ok)
                    break;
                if (tokens.getTokenType() != TokenType.NUMBER) {
                    ok = false;
                    break;
                }
                objNumber[k] = tokens.intValue();
                ok = tokens.nextToken();
                if (!ok)
                    break;
                if (tokens.getTokenType() != TokenType.NUMBER) {
                    ok = false;
                    break;
                }
                address[k] = tokens.intValue() + first;
            }
            if (!ok)
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("error.reading.objstm"));
            for (int k = 0; k < n; ++k) {
                if (map.containsKey(k)) {
                    tokens.seek(address[k]);
                    tokens.nextToken();
                    PdfObject obj;
                    if (tokens.getTokenType() == PRTokeniser.TokenType.NUMBER) {
                    	obj = new PdfNumber(tokens.getStringValue());
                    }
                    else {
                    	tokens.seek(address[k]);
                    	obj = readPRObject();
                    }
                    xrefObj.set(objNumber[k], obj);
                }
            }
        }
        finally {
            tokens = saveTokens;
        }
    }

    /**
     * Eliminates the reference to the object freeing the memory used by it and clearing
     * the xref entry.
     * @param obj the object. If it's an indirect reference it will be eliminated
     * @return the object or the already erased dereferenced object
     */
    public static PdfObject killIndirect(final PdfObject obj) {
        if (obj == null || obj.isNull())
            return null;
        PdfObject ret = getPdfObjectRelease(obj);
        if (obj.isIndirect()) {
            PRIndirectReference ref = (PRIndirectReference)obj;
            PdfReader reader = ref.getReader();
            int n = ref.getNumber();
            reader.xrefObj.set(n, null);
            if (reader.partial)
                reader.xref[n * 2] = -1;
        }
        return ret;
    }

    private void ensureXrefSize(final int size) {
        if (size == 0)
            return;
        if (xref == null)
            xref = new long[size];
        else {
            if (xref.length < size) {
                long xref2[] = new long[size];
                System.arraycopy(xref, 0, xref2, 0, xref.length);
                xref = xref2;
            }
        }
    }

    protected void readXref() throws IOException {
        hybridXref = false;
        newXrefType = false;
        tokens.seek(tokens.getStartxref());
        tokens.nextToken();
        if (!tokens.getStringValue().equals("startxref"))
            throw new InvalidPdfException(MessageLocalization.getComposedMessage("startxref.not.found"));
        tokens.nextToken();
        if (tokens.getTokenType() != TokenType.NUMBER)
            throw new InvalidPdfException(MessageLocalization.getComposedMessage("startxref.is.not.followed.by.a.number"));
        long startxref = tokens.longValue();
        lastXref = startxref;
        eofPos = tokens.getFilePointer();
        try {
            if (readXRefStream(startxref)) {
                newXrefType = true;
                return;
            }
        }
        catch (Exception e) {}
        xref = null;
        tokens.seek(startxref);
        trailer = readXrefSection();
        PdfDictionary trailer2 = trailer;
        while (true) {
            PdfNumber prev = (PdfNumber)trailer2.get(PdfName.PREV);
            if (prev == null)
                break;
            if (prev.longValue() == startxref)
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("trailer.prev.entry.points.to.its.own.cross.reference.section"));
            startxref = prev.longValue();
            tokens.seek(startxref);
            trailer2 = readXrefSection();
        }
    }

    protected PdfDictionary readXrefSection() throws IOException {
        tokens.nextValidToken();
        if (!tokens.getStringValue().equals("xref"))
            tokens.throwError(MessageLocalization.getComposedMessage("xref.subsection.not.found"));
        int start = 0;
        int end = 0;
        long pos = 0;
        int gen = 0;
        while (true) {
            tokens.nextValidToken();
            if (tokens.getStringValue().equals("trailer"))
                break;
            if (tokens.getTokenType() != TokenType.NUMBER)
                tokens.throwError(MessageLocalization.getComposedMessage("object.number.of.the.first.object.in.this.xref.subsection.not.found"));
            start = tokens.intValue();
            tokens.nextValidToken();
            if (tokens.getTokenType() != TokenType.NUMBER)
                tokens.throwError(MessageLocalization.getComposedMessage("number.of.entries.in.this.xref.subsection.not.found"));
            end = tokens.intValue() + start;
            if (start == 1) { // fix incorrect start number
                long back = tokens.getFilePointer();
                tokens.nextValidToken();
                pos = tokens.longValue();
                tokens.nextValidToken();
                gen = tokens.intValue();
                if (pos == 0 && gen == PdfWriter.GENERATION_MAX) {
                    --start;
                    --end;
                }
                tokens.seek(back);
            }
            ensureXrefSize(end * 2);
            for (int k = start; k < end; ++k) {
                tokens.nextValidToken();
                pos = tokens.longValue();
                tokens.nextValidToken();
                gen = tokens.intValue();
                tokens.nextValidToken();
                int p = k * 2;
                if (tokens.getStringValue().equals("n")) {
                    if (xref[p] == 0 && xref[p + 1] == 0) {
//                        if (pos == 0)
//                            tokens.throwError(MessageLocalization.getComposedMessage("file.position.0.cross.reference.entry.in.this.xref.subsection"));
                        xref[p] = pos;
                    }
                }
                else if (tokens.getStringValue().equals("f")) {
                    if (xref[p] == 0 && xref[p + 1] == 0)
                        xref[p] = -1;
                }
                else
                    tokens.throwError(MessageLocalization.getComposedMessage("invalid.cross.reference.entry.in.this.xref.subsection"));
            }
        }
        PdfDictionary trailer = (PdfDictionary)readPRObject();
        PdfNumber xrefSize = (PdfNumber)trailer.get(PdfName.SIZE);
        ensureXrefSize(xrefSize.intValue() * 2);
        PdfObject xrs = trailer.get(PdfName.XREFSTM);
        if (xrs != null && xrs.isNumber()) {
            int loc = ((PdfNumber)xrs).intValue();
            try {
                readXRefStream(loc);
                newXrefType = true;
                hybridXref = true;
            }
            catch (IOException e) {
                xref = null;
                throw e;
            }
        }
        return trailer;
    }

    protected boolean readXRefStream(final long ptr) throws IOException {
        tokens.seek(ptr);
        int thisStream = 0;
        if (!tokens.nextToken())
            return false;
        if (tokens.getTokenType() != TokenType.NUMBER)
            return false;
        thisStream = tokens.intValue();
        if (!tokens.nextToken() || tokens.getTokenType() != TokenType.NUMBER)
            return false;
        if (!tokens.nextToken() || !tokens.getStringValue().equals("obj"))
            return false;
        PdfObject object = readPRObject();
        PRStream stm = null;
        if (object.isStream()) {
            stm = (PRStream)object;
            if (!PdfName.XREF.equals(stm.get(PdfName.TYPE)))
                return false;
        }
        else
            return false;
        if (trailer == null) {
            trailer = new PdfDictionary();
            trailer.putAll(stm);
        }
        stm.setLength(((PdfNumber)stm.get(PdfName.LENGTH)).intValue());
        int size = ((PdfNumber)stm.get(PdfName.SIZE)).intValue();
        PdfArray index;
        PdfObject obj = stm.get(PdfName.INDEX);
        if (obj == null) {
            index = new PdfArray();
            index.add(new int[]{0, size});
        }
        else
            index = (PdfArray)obj;
        PdfArray w = (PdfArray)stm.get(PdfName.W);
        long prev = -1;
        obj = stm.get(PdfName.PREV);
        if (obj != null)
            prev = ((PdfNumber)obj).longValue();
        // Each xref pair is a position
        // type 0 -> -1, 0
        // type 1 -> offset, 0
        // type 2 -> index, obj num
        ensureXrefSize(size * 2);
        if (objStmMark == null && !partial)
            objStmMark = new HashMap<Integer, IntHashtable>();
        if (objStmToOffset == null && partial)
            objStmToOffset = new LongHashtable();
        byte b[] = getStreamBytes(stm, tokens.getFile());
        int bptr = 0;
        int wc[] = new int[3];
        for (int k = 0; k < 3; ++k)
            wc[k] = w.getAsNumber(k).intValue();
        for (int idx = 0; idx < index.size(); idx += 2) {
            int start = index.getAsNumber(idx).intValue();
            int length = index.getAsNumber(idx + 1).intValue();
            ensureXrefSize((start + length) * 2);
            while (length-- > 0) {
                int type = 1;
                if (wc[0] > 0) {
                    type = 0;
                    for (int k = 0; k < wc[0]; ++k)
                        type = (type << 8) + (b[bptr++] & 0xff);
                }
                long field2 = 0;
                for (int k = 0; k < wc[1]; ++k)
                    field2 = (field2 << 8) + (b[bptr++] & 0xff);
                int field3 = 0;
                for (int k = 0; k < wc[2]; ++k)
                    field3 = (field3 << 8) + (b[bptr++] & 0xff);
                int base = start * 2;
                if (xref[base] == 0 && xref[base + 1] == 0) {
                    switch (type) {
                        case 0:
                            xref[base] = -1;
                            break;
                        case 1:
                            xref[base] = field2;
                            break;
                        case 2:
                            xref[base] = field3;
                            xref[base + 1] = field2;
                            if (partial) {
                                objStmToOffset.put(field2, 0);
                            }
                            else {
                                Integer on = Integer.valueOf((int)field2);
                                IntHashtable seq = objStmMark.get(on);
                                if (seq == null) {
                                    seq = new IntHashtable();
                                    seq.put(field3, 1);
                                    objStmMark.put(on, seq);
                                }
                                else
                                    seq.put(field3, 1);
                            }
                            break;
                    }
                }
                ++start;
            }
        }
        thisStream *= 2;
        if (thisStream + 1 < xref.length && xref[thisStream] == 0 && xref[thisStream + 1] == 0)
            xref[thisStream] = -1;

        if (prev == -1)
            return true;
        return readXRefStream(prev);
    }

    protected void rebuildXref() throws IOException {
        hybridXref = false;
        newXrefType = false;
        tokens.seek(0);
        long xr[][] = new long[1024][];
        long top = 0;
        trailer = null;
        byte line[] = new byte[64];
        for (;;) {
            long pos = tokens.getFilePointer();
            if (!tokens.readLineSegment(line, true)) // added boolean because of mailing list issue (17 Feb. 2014)
                break;
            if (line[0] == 't') {
                if (!PdfEncodings.convertToString(line, null).startsWith("trailer"))
                    continue;
                tokens.seek(pos);
                tokens.nextToken();
                pos = tokens.getFilePointer();
                try {
                    PdfDictionary dic = (PdfDictionary)readPRObject();
                    if (dic.get(PdfName.ROOT) != null)
                        trailer = dic;
                    else
                        tokens.seek(pos);
                }
                catch (Exception e) {
                    tokens.seek(pos);
                }
            }
            else if (line[0] >= '0' && line[0] <= '9') {
                long obj[] = PRTokeniser.checkObjectStart(line);
                if (obj == null)
                    continue;
                long num = obj[0];
                long gen = obj[1];
                if (num >= xr.length) {
                    long newLength = num * 2;
                    long xr2[][] = new long[(int)newLength][];
                    System.arraycopy(xr, 0, xr2, 0, (int)top);
                    xr = xr2;
                }
                if (num >= top)
                    top = num + 1;
                if (xr[(int)num] == null || gen >= xr[(int)num][1]) {
                    obj[0] = pos;
                    xr[(int)num] = obj;
                }
            }
        }
        if (trailer == null)
            throw new InvalidPdfException(MessageLocalization.getComposedMessage("trailer.not.found"));
        xref = new long[(int)(top * 2)];
        for (int k = 0; k < top; ++k) {
            long obj[] = xr[k];
            if (obj != null)
                xref[k * 2] = obj[0];
        }
    }

    protected PdfDictionary readDictionary() throws IOException {
        PdfDictionary dic = new PdfDictionary();
        while (true) {
            tokens.nextValidToken();
            if (tokens.getTokenType() == TokenType.END_DIC)
                break;
            if (tokens.getTokenType() != TokenType.NAME)
                tokens.throwError(MessageLocalization.getComposedMessage("dictionary.key.1.is.not.a.name", tokens.getStringValue()));
            PdfName name = new PdfName(tokens.getStringValue(), false);
            PdfObject obj = readPRObject();
            int type = obj.type();
            if (-type == TokenType.END_DIC.ordinal())
                tokens.throwError(MessageLocalization.getComposedMessage("unexpected.gt.gt"));
            if (-type == TokenType.END_ARRAY.ordinal())
                tokens.throwError(MessageLocalization.getComposedMessage("unexpected.close.bracket"));
            dic.put(name, obj);
        }
        return dic;
    }

    protected PdfArray readArray() throws IOException {
        PdfArray array = new PdfArray();
        while (true) {
            PdfObject obj = readPRObject();
            int type = obj.type();
            if (-type == TokenType.END_ARRAY.ordinal())
                break;
            if (-type == TokenType.END_DIC.ordinal())
                tokens.throwError(MessageLocalization.getComposedMessage("unexpected.gt.gt"));
            array.add(obj);
        }
        return array;
    }

    // Track how deeply nested the current object is, so
    // we know when to return an individual null or boolean, or
    // reuse one of the static ones.
    private int readDepth = 0;

    protected PdfObject readPRObject() throws IOException {
        tokens.nextValidToken();
        TokenType type = tokens.getTokenType();
        switch (type) {
            case START_DIC: {
                ++readDepth;
                PdfDictionary dic = readDictionary();
                --readDepth;
                long pos = tokens.getFilePointer();
                // be careful in the trailer. May not be a "next" token.
                boolean hasNext;
                do {
                    hasNext = tokens.nextToken();
                } while (hasNext && tokens.getTokenType() == TokenType.COMMENT);

                if (hasNext && tokens.getStringValue().equals("stream")) {
                    //skip whitespaces
                    int ch;
                    do {
                        ch = tokens.read();
                    } while (ch == 32 || ch == 9 || ch == 0 || ch == 12);
                    if (ch != '\n')
                        ch = tokens.read();
                    if (ch != '\n')
                        tokens.backOnePosition(ch);
                    PRStream stream = new PRStream(this, tokens.getFilePointer());
                    stream.putAll(dic);
                    // crypto handling
                    stream.setObjNum(objNum, objGen);

                    return stream;
                }
                else {
                    tokens.seek(pos);
                    return dic;
                }
            }
            case START_ARRAY: {
                ++readDepth;
                PdfArray arr = readArray();
                --readDepth;
                return arr;
            }
            case NUMBER:
                return new PdfNumber(tokens.getStringValue());
            case STRING:
                PdfString str = new PdfString(tokens.getStringValue(), null).setHexWriting(tokens.isHexString());
                // crypto handling
                str.setObjNum(objNum, objGen);
                if (strings != null)
                    strings.add(str);

                return str;
            case NAME: {
                PdfName cachedName = PdfName.staticNames.get( tokens.getStringValue() );
                if (readDepth > 0 && cachedName != null) {
                    return cachedName;
                } else {
                    // an indirect name (how odd...), or a non-standard one
                    return new PdfName(tokens.getStringValue(), false);
                }
            }
            case REF: {
                int num = tokens.getReference();
                if (num >= 0) {
                    return new PRIndirectReference(this, num, tokens.getGeneration());
                } else {
                    if (LOGGER.isLogging(Level.ERROR)) {
                        LOGGER.error(MessageLocalization.getComposedMessage("invalid.reference.number.skip"));
                    }
                    return PdfNull.PDFNULL;
                }
            }
            case ENDOFFILE:
                throw new IOException(MessageLocalization.getComposedMessage("unexpected.end.of.file"));
            default:
                String sv = tokens.getStringValue();
                if ("null".equals(sv)) {
                    if (readDepth == 0) {
                        return new PdfNull();
                    } //else
                    return PdfNull.PDFNULL;
                }
                else if ("true".equals(sv)) {
                    if (readDepth == 0) {
                        return new PdfBoolean( true );
                    } //else
                    return PdfBoolean.PDFTRUE;
                }
                else if ("false".equals(sv)) {
                    if (readDepth == 0) {
                        return new PdfBoolean( false );
                    } //else
                    return PdfBoolean.PDFFALSE;
                }
                return new PdfLiteral(-type.ordinal(), tokens.getStringValue());
        }
    }

    /** Decodes a stream that has the FlateDecode filter.
     * @param in the input data
     * @return the decoded data
     */
    public static byte[] FlateDecode(final byte in[]) {
        byte b[] = FlateDecode(in, true);
        if (b == null)
            return FlateDecode(in, false);
        return b;
    }

    /** Decodes a stream that has the FlateDecode filter.
     * @param in the input data
     * @return the decoded data
     */
    static byte[] FlateDecode(final byte in[], ByteArrayOutputStream out) {
        byte b[] = FlateDecode(in, true, out);
        if (b == null)
            return FlateDecode(in, false, out);
        return b;
    }

    /**
     * @param in
     * @param dicPar
     * @return a byte array
     */
    public static byte[] decodePredictor(final byte in[], final PdfObject dicPar) {
        if (dicPar == null || !dicPar.isDictionary())
            return in;
        PdfDictionary dic = (PdfDictionary)dicPar;
        PdfObject obj = getPdfObject(dic.get(PdfName.PREDICTOR));
        if (obj == null || !obj.isNumber())
            return in;
        int predictor = ((PdfNumber)obj).intValue();
        if (predictor < 10 && predictor != 2)
            return in;
        int width = 1;
        obj = getPdfObject(dic.get(PdfName.COLUMNS));
        if (obj != null && obj.isNumber())
            width = ((PdfNumber)obj).intValue();
        int colors = 1;
        obj = getPdfObject(dic.get(PdfName.COLORS));
        if (obj != null && obj.isNumber())
            colors = ((PdfNumber)obj).intValue();
        int bpc = 8;
        obj = getPdfObject(dic.get(PdfName.BITSPERCOMPONENT));
        if (obj != null && obj.isNumber())
            bpc = ((PdfNumber)obj).intValue();
        DataInputStream dataStream = new DataInputStream(new ByteArrayInputStream(in));
        ByteArrayOutputStream fout = new ByteArrayOutputStream(in.length);
        int bytesPerPixel = colors * bpc / 8;
        int bytesPerRow = (colors*width*bpc + 7)/8;
        byte[] curr = new byte[bytesPerRow];
        byte[] prior = new byte[bytesPerRow];
        if (predictor == 2) {
			if (bpc == 8) {
				int numRows = in.length / bytesPerRow;
				for (int row = 0; row < numRows; row++) {
					int rowStart = row * bytesPerRow;
					for (int col = 0 + bytesPerPixel; col < bytesPerRow; col++) {
						in[rowStart + col] = (byte)(in[rowStart + col] + in[rowStart + col - bytesPerPixel]);
					}
				}
			}
			return in;
		}
        // Decode the (sub)image row-by-row
        while (true) {
            // Read the filter type byte and a row of data
            int filter = 0;
            try {
                filter = dataStream.read();
                if (filter < 0) {
                    return fout.toByteArray();
                }
                dataStream.readFully(curr, 0, bytesPerRow);
            } catch (Exception e) {
                return fout.toByteArray();
            }

            switch (filter) {
                case 0: //PNG_FILTER_NONE
                    break;
                case 1: //PNG_FILTER_SUB
                    for (int i = bytesPerPixel; i < bytesPerRow; i++) {
                        curr[i] += curr[i - bytesPerPixel];
                    }
                    break;
                case 2: //PNG_FILTER_UP
                    for (int i = 0; i < bytesPerRow; i++) {
                        curr[i] += prior[i];
                    }
                    break;
                case 3: //PNG_FILTER_AVERAGE
                    for (int i = 0; i < bytesPerPixel; i++) {
                        curr[i] += prior[i] / 2;
                    }
                    for (int i = bytesPerPixel; i < bytesPerRow; i++) {
                        curr[i] += ((curr[i - bytesPerPixel] & 0xff) + (prior[i] & 0xff))/2;
                    }
                    break;
                case 4: //PNG_FILTER_PAETH
                    for (int i = 0; i < bytesPerPixel; i++) {
                        curr[i] += prior[i];
                    }

                    for (int i = bytesPerPixel; i < bytesPerRow; i++) {
                        int a = curr[i - bytesPerPixel] & 0xff;
                        int b = prior[i] & 0xff;
                        int c = prior[i - bytesPerPixel] & 0xff;

                        int p = a + b - c;
                        int pa = Math.abs(p - a);
                        int pb = Math.abs(p - b);
                        int pc = Math.abs(p - c);

                        int ret;

                        if (pa <= pb && pa <= pc) {
                            ret = a;
                        } else if (pb <= pc) {
                            ret = b;
                        } else {
                            ret = c;
                        }
                        curr[i] += (byte)ret;
                    }
                    break;
                default:
                    // Error -- unknown filter type
                    throw new RuntimeException(MessageLocalization.getComposedMessage("png.filter.unknown"));
            }
            try {
                fout.write(curr);
            }
            catch (IOException ioe) {
                // Never happens
            }

            // Swap curr and prior
            byte[] tmp = prior;
            prior = curr;
            curr = tmp;
        }
    }

    /** A helper to FlateDecode.
     * @param in the input data
     * @param strict <CODE>true</CODE> to read a correct stream. <CODE>false</CODE>
     * to try to read a corrupted stream
     * @return the decoded data
     */
    public static byte[] FlateDecode(final byte in[], final boolean strict) {
        return FlateDecode(in, strict, new ByteArrayOutputStream());
    }

    private static byte[] FlateDecode(final byte in[], final boolean strict, ByteArrayOutputStream out) {
        ByteArrayInputStream stream = new ByteArrayInputStream(in);
        InflaterInputStream zip = new InflaterInputStream(stream);
        byte b[] = new byte[strict ? 4092 : 1];
        try {
            int n;
            while ((n = zip.read(b)) >= 0) {
                out.write(b, 0, n);
            }
            zip.close();
            out.close();
            return out.toByteArray();
        } catch (MemoryLimitsAwareException e) {
            throw e;
        } catch (Exception e) {
            if (strict)
                return null;
            return out.toByteArray();
        }
        finally {
            try {
                zip.close();
            } catch (IOException ex) {
            }
            try {
                out.close();
            } catch (IOException ex) {
            }
        }
    }

    /** Decodes a stream that has the ASCIIHexDecode filter.
     * @param in the input data
     * @return the decoded data
     */
    public static byte[] ASCIIHexDecode(final byte in[]) {
        return ASCIIHexDecode(in, new ByteArrayOutputStream());
    }

    static byte[] ASCIIHexDecode(final byte in[], ByteArrayOutputStream out) {
        boolean first = true;
        int n1 = 0;
        for (int k = 0; k < in.length; ++k) {
            int ch = in[k] & 0xff;
            if (ch == '>')
                break;
            if (PRTokeniser.isWhitespace(ch))
                continue;
            int n = PRTokeniser.getHex(ch);
            if (n == -1)
                throw new RuntimeException(MessageLocalization.getComposedMessage("illegal.character.in.asciihexdecode"));
            if (first)
                n1 = n;
            else
                out.write((byte)((n1 << 4) + n));
            first = !first;
        }
        if (!first)
            out.write((byte)(n1 << 4));
        return out.toByteArray();
    }

    /** Decodes a stream that has the ASCII85Decode filter.
     * @param in the input data
     * @return the decoded data
     */
    public static byte[] ASCII85Decode(final byte in[]) {
        return ASCII85Decode(in, new ByteArrayOutputStream());
    }

    static byte[] ASCII85Decode(final byte in[], ByteArrayOutputStream out) {
        int state = 0;
        int chn[] = new int[5];
        for (int k = 0; k < in.length; ++k) {
            int ch = in[k] & 0xff;
            if (ch == '~')
                break;
            if (PRTokeniser.isWhitespace(ch))
                continue;
            if (ch == 'z' && state == 0) {
                out.write(0);
                out.write(0);
                out.write(0);
                out.write(0);
                continue;
            }
            if (ch < '!' || ch > 'u')
                throw new RuntimeException(MessageLocalization.getComposedMessage("illegal.character.in.ascii85decode"));
            chn[state] = ch - '!';
            ++state;
            if (state == 5) {
                state = 0;
                int r = 0;
                for (int j = 0; j < 5; ++j)
                    r = r * 85 + chn[j];
                out.write((byte)(r >> 24));
                out.write((byte)(r >> 16));
                out.write((byte)(r >> 8));
                out.write((byte)r);
            }
        }
        int r = 0;
        // We'll ignore the next two lines for the sake of perpetuating broken PDFs
//        if (state == 1)
//            throw new RuntimeException(MessageLocalization.getComposedMessage("illegal.length.in.ascii85decode"));
        if (state == 2) {
            r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + 85 * 85 * 85  + 85 * 85 + 85;
            out.write((byte)(r >> 24));
        }
        else if (state == 3) {
            r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85  + chn[2] * 85 * 85 + 85 * 85 + 85;
            out.write((byte)(r >> 24));
            out.write((byte)(r >> 16));
        }
        else if (state == 4) {
            r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85  + chn[2] * 85 * 85  + chn[3] * 85 + 85;
            out.write((byte)(r >> 24));
            out.write((byte)(r >> 16));
            out.write((byte)(r >> 8));
        }
        return out.toByteArray();
    }

    /** Decodes a stream that has the LZWDecode filter.
     * @param in the input data
     * @return the decoded data
     */
    public static byte[] LZWDecode(final byte in[]) {
        return LZWDecode(in, new ByteArrayOutputStream());
    }

    static byte[] LZWDecode(final byte in[], ByteArrayOutputStream out) {
        LZWDecoder lzw = new LZWDecoder();
        lzw.decode(in, out);
        return out.toByteArray();
    }

    /** Checks if the document had errors and was rebuilt.
     * @return true if rebuilt.
     *
     */
    public boolean isRebuilt() {
        return this.rebuilt;
    }

    /** Gets the dictionary that represents a page.
     * @param pageNum the page number. 1 is the first
     * @return the page dictionary
     */
    public PdfDictionary getPageN(final int pageNum) {
        PdfDictionary dic = pageRefs.getPageN(pageNum);
        if (dic == null)
            return null;
        if (appendable)
            dic.setIndRef(pageRefs.getPageOrigRef(pageNum));
        return dic;
    }

    /**
     * @param pageNum
     * @return a Dictionary object
     */
    public PdfDictionary getPageNRelease(final int pageNum) {
        PdfDictionary dic = getPageN(pageNum);
        pageRefs.releasePage(pageNum);
        return dic;
    }

    /**
     * @param pageNum
     */
    public void releasePage(final int pageNum) {
        pageRefs.releasePage(pageNum);
    }

    /**
     *
     */
    public void resetReleasePage() {
        pageRefs.resetReleasePage();
    }

    /** Gets the page reference to this page.
     * @param pageNum the page number. 1 is the first
     * @return the page reference
     */
    public PRIndirectReference getPageOrigRef(final int pageNum) {
        return pageRefs.getPageOrigRef(pageNum);
    }

    /** Gets the contents of the page.
     * @param pageNum the page number. 1 is the first
     * @param file the location of the PDF document
     * @throws IOException on error
     * @return the content
     */
    public byte[] getPageContent(final int pageNum, final RandomAccessFileOrArray file) throws IOException{
        PdfDictionary page = getPageNRelease(pageNum);
        if (page == null)
            return null;
        PdfObject contents = getPdfObjectRelease(page.get(PdfName.CONTENTS));
        if (contents == null)
            return new byte[0];
        MemoryLimitsAwareHandler handler = memoryLimitsAwareHandler;
        long usedMemory = null == handler ? -1 : handler.getAllMemoryUsedForDecompression();

        if (contents.isStream()) {
            return getStreamBytes((PRStream)contents, file);
        }
        else if (contents.isArray()) {
            PdfArray array = (PdfArray)contents;
            MemoryLimitsAwareOutputStream bout = new MemoryLimitsAwareOutputStream();
            for (int k = 0; k < array.size(); ++k) {
                PdfObject item = getPdfObjectRelease(array.getPdfObject(k));
                if (item == null || !item.isStream())
                    continue;
                byte[] b = getStreamBytes((PRStream)item, file);
                // usedMemory has changed, that means that some of currently processed pdf streams are suspicious
                if (null != handler && usedMemory < handler.getAllMemoryUsedForDecompression()) {
                    bout.setMaxStreamSize(handler.getMaxSizeOfSingleDecompressedPdfStream());
                }
                bout.write(b);
                if (k != array.size() - 1)
                    bout.write('\n');
            }
            return bout.toByteArray();
        }
        else
            return new byte[0];
    }

    /** Gets the content from the page dictionary.
     * @param page the page dictionary
     * @throws IOException on error
     * @return the content
     * @since 5.0.6
     */
    public static byte[] getPageContent(final PdfDictionary page) throws IOException{
        if (page == null)
            return null;
        RandomAccessFileOrArray rf = null;
        try {
            PdfObject contents = getPdfObjectRelease(page.get(PdfName.CONTENTS));
            if (contents == null)
                return new byte[0];
            if (contents.isStream()) {
                if (rf == null) {
                    rf = ((PRStream)contents).getReader().getSafeFile();
                    rf.reOpen();
                }
                return getStreamBytes((PRStream)contents, rf);
            }
            else if (contents.isArray()) {
                PdfArray array = (PdfArray)contents;
                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                for (int k = 0; k < array.size(); ++k) {
                    PdfObject item = getPdfObjectRelease(array.getPdfObject(k));
                    if (item == null || !item.isStream())
                        continue;
                    if (rf == null) {
                        rf = ((PRStream)item).getReader().getSafeFile();
                        rf.reOpen();
                    }
                    byte[] b = getStreamBytes((PRStream)item, rf);
                    bout.write(b);
                    if (k != array.size() - 1)
                        bout.write('\n');
                }
                return bout.toByteArray();
            }
            else
                return new byte[0];
        }
        finally {
            try {
                if (rf != null)
                    rf.close();
            }catch(Exception e){}
        }
    }

    /**
     * Retrieve the given page's resource dictionary
     * @param pageNum 1-based page number from which to retrieve the resource dictionary
     * @return The page's resources, or 'null' if the page has none.
     * @since 5.1
     */
    public PdfDictionary getPageResources(final int pageNum) {
        return getPageResources(getPageN(pageNum));
    }

    /**
     * Retrieve the given page's resource dictionary
     * @param pageDict the given page
     * @return The page's resources, or 'null' if the page has none.
     * @since 5.1
     */
    public PdfDictionary getPageResources(final PdfDictionary pageDict) {
    	return pageDict.getAsDict(PdfName.RESOURCES);
    }

    /** Gets the contents of the page.
     * @param pageNum the page number. 1 is the first
     * @throws IOException on error
     * @return the content
     */
    public byte[] getPageContent(final int pageNum) throws IOException{
        RandomAccessFileOrArray rf = getSafeFile();
        try {
            rf.reOpen();
            return getPageContent(pageNum, rf);
        }
        finally {
            try{rf.close();}catch(Exception e){}
        }
    }

    protected void killXref(PdfObject obj) {
        if (obj == null)
            return;
        if (obj instanceof PdfIndirectReference && !obj.isIndirect())
            return;
        switch (obj.type()) {
            case PdfObject.INDIRECT: {
                int xr = ((PRIndirectReference)obj).getNumber();
                obj = xrefObj.get(xr);
                xrefObj.set(xr, null);
                freeXref = xr;
                killXref(obj);
                break;
            }
            case PdfObject.ARRAY: {
                PdfArray t = (PdfArray)obj;
                for (int i = 0; i < t.size(); ++i)
                    killXref(t.getPdfObject(i));
                break;
            }
            case PdfObject.STREAM:
            case PdfObject.DICTIONARY: {
                PdfDictionary dic = (PdfDictionary)obj;
                for (Object element : dic.getKeys()) {
                    killXref(dic.get((PdfName)element));
                }
                break;
            }
        }
    }

    /** Sets the contents of the page.
     * @param content the new page content
     * @param pageNum the page number. 1 is the first
     */
    public void setPageContent(final int pageNum, final byte content[]) {
    	setPageContent(pageNum, content, PdfStream.DEFAULT_COMPRESSION);
    }

    /** Sets the contents of the page.
     * @param content the new page content
     * @param pageNum the page number. 1 is the first
     * @param compressionLevel the compressionLevel
     * @since	2.1.3	(the method already existed without param compressionLevel)
     */
    public void setPageContent(final int pageNum, final byte content[], final int compressionLevel) {
        setPageContent(pageNum, content, compressionLevel, false);
    }

    /** Sets the contents of the page.
     * @param content the new page content
     * @param pageNum the page number. 1 is the first
     * @param compressionLevel the compressionLevel
     * @param killOldXRefRecursively if true, old contents will be deeply removed from the pdf (i.e. if it was an array,
     *                               all its entries will also be removed). Use careful when a content stream may be reused.
     *                               If false, old contents will not be removed and will stay in the document if not manually deleted.
     * @since	5.5.7	(the method already existed without param killOldXRefRecursively)
     */
    public void setPageContent(final int pageNum, final byte content[], final int compressionLevel, final boolean killOldXRefRecursively) {
        PdfDictionary page = getPageN(pageNum);
        if (page == null)
            return;
        PdfObject contents = page.get(PdfName.CONTENTS);
        freeXref = -1;
        if (killOldXRefRecursively) {
            killXref(contents);
        }
        if (freeXref == -1) {
            xrefObj.add(null);
            freeXref = xrefObj.size() - 1;
        }
        page.put(PdfName.CONTENTS, new PRIndirectReference(this, freeXref));
        xrefObj.set(freeXref, new PRStream(this, content, compressionLevel));
    }
    
    /**
     * Decode a byte[] applying the filters specified in the provided dictionary using default filter handlers.
     * @param b the bytes to decode
     * @param streamDictionary the dictionary that contains filter information
     * @return the decoded bytes
     * @throws IOException if there are any problems decoding the bytes
     * @since 5.0.4
     */
    public static byte[] decodeBytes(byte[] b, final PdfDictionary streamDictionary) throws IOException {
        return decodeBytes(b, streamDictionary, FilterHandlers.getDefaultFilterHandlers());
    }
    
    /**
     * Decode a byte[] applying the filters specified in the provided dictionary using the provided filter handlers.
     * @param b the bytes to decode
     * @param streamDictionary the dictionary that contains filter information
     * @param filterHandlers the map used to look up a handler for each type of filter
     * @return the decoded bytes
     * @throws IOException if there are any problems decoding the bytes
     * @since 5.0.4
     */
    public static byte[] decodeBytes(byte[] b, final PdfDictionary streamDictionary, Map<PdfName, FilterHandlers.FilterHandler> filterHandlers) throws IOException {
        PdfObject filter = getPdfObjectRelease(streamDictionary.get(PdfName.FILTER));

        ArrayList<PdfObject> filters = new ArrayList<PdfObject>();
        if (filter != null) {
            if (filter.isName())
                filters.add(filter);
            else if (filter.isArray())
                filters = ((PdfArray)filter).getArrayList();
        }

        MemoryLimitsAwareHandler memoryLimitsAwareHandler = null;
        if (streamDictionary instanceof PRStream && null != ((PRStream) streamDictionary).getReader()) {
            memoryLimitsAwareHandler = ((PRStream) streamDictionary).getReader().getMemoryLimitsAwareHandler();
        }
        if (null != memoryLimitsAwareHandler) {
            HashSet<PdfName> filterSet = new HashSet<PdfName>();
            int index;
            for (index = 0; index < filters.size(); index++) {
                PdfName filterName = (PdfName) filters.get(index);
                if (!filterSet.add(filterName)) {
                    memoryLimitsAwareHandler.beginDecompressedPdfStreamProcessing();
                    break;
                }
            }
            if (index == filters.size()) { // The stream isn't suspicious. We shouldn't process it.
                memoryLimitsAwareHandler = null;
            }
        }

        ArrayList<PdfObject> dp = new ArrayList<PdfObject>();
        PdfObject dpo = getPdfObjectRelease(streamDictionary.get(PdfName.DECODEPARMS));
        if (dpo == null || !dpo.isDictionary() && !dpo.isArray())
            dpo = getPdfObjectRelease(streamDictionary.get(PdfName.DP));
        if (dpo != null) {
            if (dpo.isDictionary())
                dp.add(dpo);
            else if (dpo.isArray())
                dp = ((PdfArray)dpo).getArrayList();
        }
        for (int j = 0; j < filters.size(); ++j) {
            PdfName filterName = (PdfName)filters.get(j);
            FilterHandlers.FilterHandler filterHandler = filterHandlers.get(filterName);
            if (filterHandler == null)
                throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("the.filter.1.is.not.supported", filterName));
            
            PdfDictionary decodeParams;
            if (j < dp.size()){
                PdfObject dpEntry = getPdfObject(dp.get(j));
                if (dpEntry instanceof PdfDictionary){
                    decodeParams = (PdfDictionary)dpEntry;
                } else if (dpEntry == null || dpEntry instanceof PdfNull ||
                        (dpEntry instanceof PdfLiteral && Arrays.equals("null".getBytes(), ((PdfLiteral)dpEntry).getBytes()))) {
                    decodeParams = null;
                } else {
                    throw new UnsupportedPdfException(MessageLocalization.getComposedMessage("the.decode.parameter.type.1.is.not.supported", dpEntry.getClass().toString()));
                }
                
            } else {
                decodeParams = null;
            }
            b = filterHandler.decode(b, filterName, decodeParams, streamDictionary);
            if (null != memoryLimitsAwareHandler) {
                memoryLimitsAwareHandler.considerBytesOccupiedByDecompressedPdfStream(b.length);
            }
        }
        if (null != memoryLimitsAwareHandler) {
            memoryLimitsAwareHandler.endDecompressedPdfStreamProcessing();
        }
        return b;
    }

    /** Get the content from a stream applying the required filters.
     * @param stream the stream
     * @param file the location where the stream is
     * @throws IOException on error
     * @return the stream content
     */
    public static byte[] getStreamBytes(final PRStream stream, final RandomAccessFileOrArray file) throws IOException {
        byte[] b = getStreamBytesRaw(stream, file);
        return decodeBytes(b, stream);
    }

    /** Get the content from a stream applying the required filters.
     * @param stream the stream
     * @throws IOException on error
     * @return the stream content
     */
    public static byte[] getStreamBytes(final PRStream stream) throws IOException {
        RandomAccessFileOrArray rf = stream.getReader().getSafeFile();
        try {
            rf.reOpen();
            return getStreamBytes(stream, rf);
        }
        finally {
            try{rf.close();}catch(Exception e){}
        }
    }

    /** Get the content from a stream as it is without applying any filter.
     * @param stream the stream
     * @param file the location where the stream is
     * @throws IOException on error
     * @return the stream content
     */
    public static byte[] getStreamBytesRaw(final PRStream stream, final RandomAccessFileOrArray file) throws IOException {
        PdfReader reader = stream.getReader();
        byte b[];
        if (stream.getOffset() < 0)
            b = stream.getBytes();
        else {
            b = new byte[stream.getLength()];
            file.seek(stream.getOffset());
            file.readFully(b);
            PdfEncryption decrypt = reader.getDecrypt();
            if (decrypt != null) {
                PdfObject filter = getPdfObjectRelease(stream.get(PdfName.FILTER));
                ArrayList<PdfObject> filters = new ArrayList<PdfObject>();
                if (filter != null) {
                    if (filter.isName())
                        filters.add(filter);
                    else if (filter.isArray())
                        filters = ((PdfArray)filter).getArrayList();
                }
                boolean skip = false;
                for (int k = 0; k < filters.size(); ++k) {
                    PdfObject obj = getPdfObjectRelease(filters.get(k));
                    if (obj != null && obj.toString().equals("/Crypt")) {
                        skip = true;
                        break;
                    }
                }
                if (!skip) {
                    decrypt.setHashKey(stream.getObjNum(), stream.getObjGen());
                    b = decrypt.decryptByteArray(b);
                }
            }
        }
        return b;
    }

    /** Get the content from a stream as it is without applying any filter.
     * @param stream the stream
     * @throws IOException on error
     * @return the stream content
     */
    public static byte[] getStreamBytesRaw(final PRStream stream) throws IOException {
        RandomAccessFileOrArray rf = stream.getReader().getSafeFile();
        try {
            rf.reOpen();
            return getStreamBytesRaw(stream, rf);
        }
        finally {
            try{rf.close();}catch(Exception e){}
        }
    }

    /** Eliminates shared streams if they exist. */
    public void eliminateSharedStreams() {
        if (!sharedStreams)
            return;
        sharedStreams = false;
        if (pageRefs.size() == 1)
            return;
        ArrayList<PRIndirectReference> newRefs = new ArrayList<PRIndirectReference>();
        ArrayList<PRStream> newStreams = new ArrayList<PRStream>();
        IntHashtable visited = new IntHashtable();
        for (int k = 1; k <= pageRefs.size(); ++k) {
            PdfDictionary page = pageRefs.getPageN(k);
            if (page == null)
                continue;
            PdfObject contents = getPdfObject(page.get(PdfName.CONTENTS));
            if (contents == null)
                continue;
            if (contents.isStream()) {
                PRIndirectReference ref = (PRIndirectReference)page.get(PdfName.CONTENTS);
                if (visited.containsKey(ref.getNumber())) {
                    // need to duplicate
                    newRefs.add(ref);
                    newStreams.add(new PRStream((PRStream)contents, null));
                }
                else
                    visited.put(ref.getNumber(), 1);
            }
            else if (contents.isArray()) {
                PdfArray array = (PdfArray)contents;
                for (int j = 0; j < array.size(); ++j) {
                    PRIndirectReference ref = (PRIndirectReference)array.getPdfObject(j);
                    if (visited.containsKey(ref.getNumber())) {
                        // need to duplicate
                        newRefs.add(ref);
                        newStreams.add(new PRStream((PRStream)getPdfObject(ref), null));
                    }
                    else
                        visited.put(ref.getNumber(), 1);
                }
            }
        }
        if (newStreams.isEmpty())
            return;
        for (int k = 0; k < newStreams.size(); ++k) {
            xrefObj.add(newStreams.get(k));
            PRIndirectReference ref = newRefs.get(k);
            ref.setNumber(xrefObj.size() - 1, 0);
        }
    }

    /** Checks if the document was changed.
     * @return <CODE>true</CODE> if the document was changed,
     * <CODE>false</CODE> otherwise
     */
    public boolean isTampered() {
        return tampered;
    }

    /**
     * Sets the tampered state. A tampered PdfReader cannot be reused in PdfStamper.
     * @param tampered the tampered state
     */
    public void setTampered(final boolean tampered) {
        this.tampered = tampered;
        pageRefs.keepPages();
    }

    /** Gets the XML metadata.
     * @throws IOException on error
     * @return the XML metadata
     */
    public byte[] getMetadata() throws IOException {
        PdfObject obj = getPdfObject(catalog.get(PdfName.METADATA));
        if (!(obj instanceof PRStream))
            return null;
        RandomAccessFileOrArray rf = getSafeFile();
        byte b[] = null;
        try {
            rf.reOpen();
            b = getStreamBytes((PRStream)obj, rf);
        }
        finally {
            try {
                rf.close();
            }
            catch (Exception e) {
                // empty on purpose
            }
        }
        return b;
    }

    /**
     * Gets the byte address of the last xref table.
     * @return the byte address of the last xref table
     */
    public long getLastXref() {
        return lastXref;
    }

    /**
     * Gets the number of xref objects.
     * @return the number of xref objects
     */
    public int getXrefSize() {
        return xrefObj.size();
    }

    /**
     * Gets the byte address of the %%EOF marker.
     * @return the byte address of the %%EOF marker
     */
    public long getEofPos() {
        return eofPos;
    }

    /**
     * Gets the PDF version. Only the last version char is returned. For example
     * version 1.4 is returned as '4'.
     * @return the PDF version
     */
    public char getPdfVersion() {
        return pdfVersion;
    }

    /**
     * Returns <CODE>true</CODE> if the PDF is encrypted.
     * @return <CODE>true</CODE> if the PDF is encrypted
     */
    public boolean isEncrypted() {
        return encrypted;
    }

    /**
     * Gets the encryption permissions. It can be used directly in
     * <CODE>PdfWriter.setEncryption()</CODE>.
     * @return the encryption permissions
     */
    public long getPermissions() {
        return pValue;
    }

    /**
     * Returns <CODE>true</CODE> if the PDF has a 128 bit key encryption.
     * @return <CODE>true</CODE> if the PDF has a 128 bit key encryption
     */
    public boolean is128Key() {
        return rValue == 3;
    }

    /**
     * Gets the trailer dictionary
     * @return the trailer dictionary
     */
    public PdfDictionary getTrailer() {
        return trailer;
    }

    PdfEncryption getDecrypt() {
        return decrypt;
    }

    static boolean equalsn(final byte a1[], final byte a2[]) {
        int length = a2.length;
        for (int k = 0; k < length; ++k) {
            if (a1[k] != a2[k])
                return false;
        }
        return true;
    }

    static boolean existsName(final PdfDictionary dic, final PdfName key, final PdfName value) {
        PdfObject type = getPdfObjectRelease(dic.get(key));
        if (type == null || !type.isName())
            return false;
        PdfName name = (PdfName)type;
        return name.equals(value);
    }

    static String getFontName(final PdfDictionary dic) {
        if (dic == null)
            return null;
        PdfObject type = getPdfObjectRelease(dic.get(PdfName.BASEFONT));
        if (type == null || !type.isName())
            return null;
        return PdfName.decodeName(type.toString());
    }

    static String getSubsetPrefix(final PdfDictionary dic) {
        if (dic == null)
            return null;
        String s = getFontName(dic);
        if (s == null)
            return null;
        if (s.length() < 8 || s.charAt(6) != '+')
            return null;
        for (int k = 0; k < 6; ++k) {
            char c = s.charAt(k);
            if (c < 'A' || c > 'Z')
                return null;
        }
        return s;
    }

    /** Finds all the font subsets and changes the prefixes to some
     * random values.
     * @return the number of font subsets altered
     */
    public int shuffleSubsetNames() {
        int total = 0;
        for (int k = 1; k < xrefObj.size(); ++k) {
            PdfObject obj = getPdfObjectRelease(k);
            if (obj == null || !obj.isDictionary())
                continue;
            PdfDictionary dic = (PdfDictionary)obj;
            if (!existsName(dic, PdfName.TYPE, PdfName.FONT))
                continue;
            if (existsName(dic, PdfName.SUBTYPE, PdfName.TYPE1)
                || existsName(dic, PdfName.SUBTYPE, PdfName.MMTYPE1)
                || existsName(dic, PdfName.SUBTYPE, PdfName.TRUETYPE)) {
                String s = getSubsetPrefix(dic);
                if (s == null)
                    continue;
                String ns = BaseFont.createSubsetPrefix() + s.substring(7);
                PdfName newName = new PdfName(ns);
                dic.put(PdfName.BASEFONT, newName);
                setXrefPartialObject(k, dic);
                ++total;
                PdfDictionary fd = dic.getAsDict(PdfName.FONTDESCRIPTOR);
                if (fd == null)
                    continue;
                fd.put(PdfName.FONTNAME, newName);
            }
            else if (existsName(dic, PdfName.SUBTYPE, PdfName.TYPE0)) {
                String s = getSubsetPrefix(dic);
                PdfArray arr = dic.getAsArray(PdfName.DESCENDANTFONTS);
                if (arr == null)
                    continue;
                if (arr.isEmpty())
                    continue;
                PdfDictionary desc = arr.getAsDict(0);
                String sde = getSubsetPrefix(desc);
                if (sde == null)
                    continue;
                String ns = BaseFont.createSubsetPrefix();
                if (s != null)
                    dic.put(PdfName.BASEFONT, new PdfName(ns + s.substring(7)));
                setXrefPartialObject(k, dic);
                PdfName newName = new PdfName(ns + sde.substring(7));
                desc.put(PdfName.BASEFONT, newName);
                ++total;
                PdfDictionary fd = desc.getAsDict(PdfName.FONTDESCRIPTOR);
                if (fd == null)
                    continue;
                fd.put(PdfName.FONTNAME, newName);
            }
        }
        return total;
    }

    /** Finds all the fonts not subset but embedded and marks them as subset.
     * @return the number of fonts altered
     */
    public int createFakeFontSubsets() {
        int total = 0;
        for (int k = 1; k < xrefObj.size(); ++k) {
            PdfObject obj = getPdfObjectRelease(k);
            if (obj == null || !obj.isDictionary())
                continue;
            PdfDictionary dic = (PdfDictionary)obj;
            if (!existsName(dic, PdfName.TYPE, PdfName.FONT))
                continue;
            if (existsName(dic, PdfName.SUBTYPE, PdfName.TYPE1)
                || existsName(dic, PdfName.SUBTYPE, PdfName.MMTYPE1)
                || existsName(dic, PdfName.SUBTYPE, PdfName.TRUETYPE)) {
                String s = getSubsetPrefix(dic);
                if (s != null)
                    continue;
                s = getFontName(dic);
                if (s == null)
                    continue;
                String ns = BaseFont.createSubsetPrefix() + s;
                PdfDictionary fd = (PdfDictionary)getPdfObjectRelease(dic.get(PdfName.FONTDESCRIPTOR));
                if (fd == null)
                    continue;
                if (fd.get(PdfName.FONTFILE) == null && fd.get(PdfName.FONTFILE2) == null
                    && fd.get(PdfName.FONTFILE3) == null)
                    continue;
                fd = dic.getAsDict(PdfName.FONTDESCRIPTOR);
                PdfName newName = new PdfName(ns);
                dic.put(PdfName.BASEFONT, newName);
                fd.put(PdfName.FONTNAME, newName);
                setXrefPartialObject(k, dic);
                ++total;
            }
        }
        return total;
    }

    private static PdfArray getNameArray(PdfObject obj) {
        if (obj == null)
            return null;
        obj = getPdfObjectRelease(obj);
        if (obj == null)
            return null;
        if (obj.isArray())
            return (PdfArray)obj;
        else if (obj.isDictionary()) {
            PdfObject arr2 = getPdfObjectRelease(((PdfDictionary)obj).get(PdfName.D));
            if (arr2 != null && arr2.isArray())
                return (PdfArray)arr2;
        }
        return null;
    }

    /**
     * Gets all the named destinations as an <CODE>HashMap</CODE>. The key is the name
     * and the value is the destinations array.
     * @return gets all the named destinations
     */
    public HashMap<Object, PdfObject> getNamedDestination() {
    	return getNamedDestination(false);
    }

    /**
     * Gets all the named destinations as an <CODE>HashMap</CODE>. The key is the name
     * and the value is the destinations array.
     * @param	keepNames	true if you want the keys to be real PdfNames instead of Strings
     * @return gets all the named destinations
     * @since	2.1.6
     */
    public HashMap<Object, PdfObject> getNamedDestination(final boolean keepNames) {
        HashMap<Object, PdfObject> names = getNamedDestinationFromNames(keepNames);
        names.putAll(getNamedDestinationFromStrings());
        return names;
    }

    /**
     * Gets the named destinations from the /Dests key in the catalog as an <CODE>HashMap</CODE>. The key is the name
     * and the value is the destinations array.
     * @return gets the named destinations
     * @since 5.0.1 (generic type in signature)
     */
    @SuppressWarnings("unchecked")
    public HashMap<String, PdfObject> getNamedDestinationFromNames() {
    	return new HashMap(getNamedDestinationFromNames(false));
    }

    /**
     * Gets the named destinations from the /Dests key in the catalog as an <CODE>HashMap</CODE>. The key is the name
     * and the value is the destinations array.
     * @param	keepNames	true if you want the keys to be real PdfNames instead of Strings
     * @return gets the named destinations
     * @since	2.1.6
     */
    public HashMap<Object, PdfObject> getNamedDestinationFromNames(final boolean keepNames) {
        HashMap<Object, PdfObject> names = new HashMap<Object, PdfObject>();
        if (catalog.get(PdfName.DESTS) != null) {
            PdfDictionary dic = (PdfDictionary)getPdfObjectRelease(catalog.get(PdfName.DESTS));
            if (dic == null)
                return names;
            Set<PdfName> keys = dic.getKeys();
            for (PdfName key : keys) {
                PdfArray arr = getNameArray(dic.get(key));
                if (arr == null)
                	continue;
                if (keepNames) {
                	names.put(key, arr);
                }
                else {
                	String name = PdfName.decodeName(key.toString());
                	names.put(name, arr);
                }
            }
        }
        return names;
    }

    /**
     * Gets the named destinations from the /Names key in the catalog as an <CODE>HashMap</CODE>. The key is the name
     * and the value is the destinations array.
     * @return gets the named destinations
     */
    public HashMap<String, PdfObject> getNamedDestinationFromStrings() {
        if (catalog.get(PdfName.NAMES) != null) {
            PdfDictionary dic = (PdfDictionary)getPdfObjectRelease(catalog.get(PdfName.NAMES));
            if (dic != null) {
                dic = (PdfDictionary)getPdfObjectRelease(dic.get(PdfName.DESTS));
                if (dic != null) {
                    HashMap<String, PdfObject> names = PdfNameTree.readTree(dic);
                    for (Iterator<Map.Entry<String, PdfObject>> it = names.entrySet().iterator(); it.hasNext();) {
                        Map.Entry<String, PdfObject> entry = it.next();
                        PdfArray arr = getNameArray(entry.getValue());
                        if (arr != null)
                            entry.setValue(arr);
                        else
                            it.remove();
                    }
                    return names;
                }
            }
        }
        return new HashMap<String, PdfObject>();
    }

    /**
     * Removes all the fields from the document.
     */
    public void removeFields() {
        pageRefs.resetReleasePage();
        for (int k = 1; k <= pageRefs.size(); ++k) {
            PdfDictionary page = pageRefs.getPageN(k);
            PdfArray annots = page.getAsArray(PdfName.ANNOTS);
            if (annots == null) {
                pageRefs.releasePage(k);
                continue;
            }
            for (int j = 0; j < annots.size(); ++j) {
                PdfObject obj = getPdfObjectRelease(annots.getPdfObject(j));
                if (obj == null || !obj.isDictionary())
                    continue;
                PdfDictionary annot = (PdfDictionary)obj;
                if (PdfName.WIDGET.equals(annot.get(PdfName.SUBTYPE)))
                    annots.remove(j--);
            }
            if (annots.isEmpty())
                page.remove(PdfName.ANNOTS);
            else
                pageRefs.releasePage(k);
        }
        catalog.remove(PdfName.ACROFORM);
        pageRefs.resetReleasePage();
    }

    /**
     * Removes all the annotations and fields from the document.
     */
    public void removeAnnotations() {
        pageRefs.resetReleasePage();
        for (int k = 1; k <= pageRefs.size(); ++k) {
            PdfDictionary page = pageRefs.getPageN(k);
            if (page.get(PdfName.ANNOTS) == null)
                pageRefs.releasePage(k);
            else
                page.remove(PdfName.ANNOTS);
        }
        catalog.remove(PdfName.ACROFORM);
        pageRefs.resetReleasePage();
    }

    /**
     * Retrieves links for a certain page.
     * @param page the page to inspect
     * @return a list of links
     */
    public ArrayList<PdfAnnotation.PdfImportedLink> getLinks(final int page) {
        pageRefs.resetReleasePage();
        ArrayList<PdfAnnotation.PdfImportedLink> result = new ArrayList<PdfAnnotation.PdfImportedLink>();
        PdfDictionary pageDic = pageRefs.getPageN(page);
        if (pageDic.get(PdfName.ANNOTS) != null) {
            PdfArray annots = pageDic.getAsArray(PdfName.ANNOTS);
            for (int j = 0; j < annots.size(); ++j) {
                PdfDictionary annot = (PdfDictionary)getPdfObjectRelease(annots.getPdfObject(j));

                if (PdfName.LINK.equals(annot.get(PdfName.SUBTYPE))) {
                	result.add(new PdfAnnotation.PdfImportedLink(annot));
                }
            }
        }
    	pageRefs.releasePage(page);
        pageRefs.resetReleasePage();
        return result;
    }

    private void iterateBookmarks(PdfObject outlineRef, final HashMap<Object, PdfObject> names) {
        while (outlineRef != null) {
            replaceNamedDestination(outlineRef, names);
            PdfDictionary outline = (PdfDictionary)getPdfObjectRelease(outlineRef);
            PdfObject first = outline.get(PdfName.FIRST);
            if (first != null) {
                iterateBookmarks(first, names);
            }
            outlineRef = outline.get(PdfName.NEXT);
        }
    }

    /**
     * Replaces remote named links with local destinations that have the same name.
     * @since	5.0
     */
    public void makeRemoteNamedDestinationsLocal() {
        if (remoteToLocalNamedDestinations)
            return;
        remoteToLocalNamedDestinations = true;
        HashMap<Object, PdfObject> names = getNamedDestination(true);
        if (names.isEmpty())
            return;
        for (int k = 1; k <= pageRefs.size(); ++k) {
            PdfDictionary page = pageRefs.getPageN(k);
            PdfObject annotsRef;
            PdfArray annots = (PdfArray)getPdfObject(annotsRef = page.get(PdfName.ANNOTS));
            int annotIdx = lastXrefPartial;
            releaseLastXrefPartial();
            if (annots == null) {
                pageRefs.releasePage(k);
                continue;
            }
            boolean commitAnnots = false;
            for (int an = 0; an < annots.size(); ++an) {
                PdfObject objRef = annots.getPdfObject(an);
                if (convertNamedDestination(objRef, names) && !objRef.isIndirect())
                    commitAnnots = true;
            }
            if (commitAnnots)
                setXrefPartialObject(annotIdx,  annots);
            if (!commitAnnots || annotsRef.isIndirect())
                pageRefs.releasePage(k);
        }
    }

    /**
     * Converts a remote named destination GoToR with a local named destination
     * if there's a corresponding name.
     * @param	obj	an annotation that needs to be screened for links to external named destinations.
     * @param	names	a map with names of local named destinations
     * @since	iText 5.0
     */
    private boolean convertNamedDestination(PdfObject obj, final HashMap<Object, PdfObject> names) {
        obj = getPdfObject(obj);
        int objIdx = lastXrefPartial;
        releaseLastXrefPartial();
        if (obj != null && obj.isDictionary()) {
            PdfObject ob2 = getPdfObject(((PdfDictionary)obj).get(PdfName.A));
            if (ob2 != null) {
                int obj2Idx = lastXrefPartial;
                releaseLastXrefPartial();
                PdfDictionary dic = (PdfDictionary)ob2;
                PdfName type = (PdfName)getPdfObjectRelease(dic.get(PdfName.S));
                if (PdfName.GOTOR.equals(type)) {
                    PdfObject ob3 = getPdfObjectRelease(dic.get(PdfName.D));
                    Object name = null;
                    if (ob3 != null) {
                        if (ob3.isName())
                            name = ob3;
                        else if (ob3.isString())
                            name = ob3.toString();
                        PdfArray dest = (PdfArray)names.get(name);
                        if (dest != null) {
                        	dic.remove(PdfName.F);
                        	dic.remove(PdfName.NEWWINDOW);
                        	dic.put(PdfName.S, PdfName.GOTO);
                        	setXrefPartialObject(obj2Idx, ob2);
                        	setXrefPartialObject(objIdx, obj);
                        	return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    /** Replaces all the local named links with the actual destinations. */
    public void consolidateNamedDestinations() {
        if (consolidateNamedDestinations)
            return;
        consolidateNamedDestinations = true;
        HashMap<Object, PdfObject> names = getNamedDestination(true);
        if (names.isEmpty())
            return;
        for (int k = 1; k <= pageRefs.size(); ++k) {
            PdfDictionary page = pageRefs.getPageN(k);
            PdfObject annotsRef;
            PdfArray annots = (PdfArray)getPdfObject(annotsRef = page.get(PdfName.ANNOTS));
            int annotIdx = lastXrefPartial;
            releaseLastXrefPartial();
            if (annots == null) {
                pageRefs.releasePage(k);
                continue;
            }
            boolean commitAnnots = false;
            for (int an = 0; an < annots.size(); ++an) {
                PdfObject objRef = annots.getPdfObject(an);
                if (replaceNamedDestination(objRef, names) && !objRef.isIndirect())
                    commitAnnots = true;
            }
            if (commitAnnots)
                setXrefPartialObject(annotIdx,  annots);
            if (!commitAnnots || annotsRef.isIndirect())
                pageRefs.releasePage(k);
        }
        PdfDictionary outlines = (PdfDictionary)getPdfObjectRelease(catalog.get(PdfName.OUTLINES));
        if (outlines == null)
            return;
        iterateBookmarks(outlines.get(PdfName.FIRST), names);
    }

    private boolean replaceNamedDestination(PdfObject obj, final HashMap<Object, PdfObject> names) {
        obj = getPdfObject(obj);
        int objIdx = lastXrefPartial;
        releaseLastXrefPartial();
        if (obj != null && obj.isDictionary()) {
            PdfObject ob2 = getPdfObjectRelease(((PdfDictionary)obj).get(PdfName.DEST));
            Object name = null;
            if (ob2 != null) {
                if (ob2.isName())
                    name = ob2;
                else if (ob2.isString())
                    name = ob2.toString();
                PdfArray dest = (PdfArray)names.get(name);
                if (dest != null) {
                    ((PdfDictionary)obj).put(PdfName.DEST, dest);
                    setXrefPartialObject(objIdx, obj);
                    return true;
                }
            }
            else if ((ob2 = getPdfObject(((PdfDictionary)obj).get(PdfName.A))) != null) {
                int obj2Idx = lastXrefPartial;
                releaseLastXrefPartial();
                PdfDictionary dic = (PdfDictionary)ob2;
                PdfName type = (PdfName)getPdfObjectRelease(dic.get(PdfName.S));
                if (PdfName.GOTO.equals(type)) {
                    PdfObject ob3 = getPdfObjectRelease(dic.get(PdfName.D));
                    if (ob3 != null) {
                        if (ob3.isName())
                            name = ob3;
                        else if (ob3.isString())
                            name = ob3.toString();
                    }
                    PdfArray dest = (PdfArray)names.get(name);
                    if (dest != null) {
                        dic.put(PdfName.D, dest);
                        setXrefPartialObject(obj2Idx, ob2);
                        setXrefPartialObject(objIdx, obj);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    protected static PdfDictionary duplicatePdfDictionary(final PdfDictionary original, PdfDictionary copy, final PdfReader newReader) {
        if (copy == null)
            copy = new PdfDictionary(original.size());
        for (Object element : original.getKeys()) {
            PdfName key = (PdfName)element;
            copy.put(key, duplicatePdfObject(original.get(key), newReader));
        }
        return copy;
    }

    protected static PdfObject duplicatePdfObject(final PdfObject original, final PdfReader newReader) {
        if (original == null)
            return null;
        switch (original.type()) {
            case PdfObject.DICTIONARY: {
                return duplicatePdfDictionary((PdfDictionary)original, null, newReader);
            }
            case PdfObject.STREAM: {
                PRStream org = (PRStream)original;
                PRStream stream = new PRStream(org, null, newReader);
                duplicatePdfDictionary(org, stream, newReader);
                return stream;
            }
            case PdfObject.ARRAY: {
                PdfArray originalArray = (PdfArray) original;
                PdfArray arr = new PdfArray(originalArray.size());
                for (Iterator<PdfObject> it = originalArray.listIterator(); it.hasNext();) {
                    arr.add(duplicatePdfObject(it.next(), newReader));
                }
                return arr;
            }
            case PdfObject.INDIRECT: {
                PRIndirectReference org = (PRIndirectReference)original;
                return new PRIndirectReference(newReader, org.getNumber(), org.getGeneration());
            }
            default:
                return original;
        }
    }

    /**
     * Closes the reader, and any underlying stream or data source used to create the reader
     */
    public void close() {
        try {
            tokens.close();
        }
        catch (IOException e) {
            throw new ExceptionConverter(e);
        }
    }

    @SuppressWarnings("unchecked")
    protected void removeUnusedNode(PdfObject obj, final boolean hits[]) {
        Stack<Object> state = new Stack<Object>();
        state.push(obj);
        while (!state.empty()) {
            Object current = state.pop();
            if (current == null)
                continue;
            ArrayList<PdfObject> ar = null;
            PdfDictionary dic = null;
            PdfName[] keys = null;
            Object[] objs = null;
            int idx = 0;
            if (current instanceof PdfObject) {
                obj = (PdfObject)current;
                switch (obj.type()) {
                    case PdfObject.DICTIONARY:
                    case PdfObject.STREAM:
                        dic = (PdfDictionary)obj;
                        keys = new PdfName[dic.size()];
                        dic.getKeys().toArray(keys);
                        break;
                    case PdfObject.ARRAY:
                         ar = ((PdfArray)obj).getArrayList();
                         break;
                    case PdfObject.INDIRECT:
                        PRIndirectReference ref = (PRIndirectReference)obj;
                        int num = ref.getNumber();
                        if (!hits[num]) {
                            hits[num] = true;
                            state.push(getPdfObjectRelease(ref));
                        }
                        continue;
                    default:
                        continue;
                }
            }
            else {
                objs = (Object[])current;
                if (objs[0] instanceof ArrayList) {
                    ar = (ArrayList<PdfObject>)objs[0];
                    idx = ((Integer)objs[1]).intValue();
                }
                else {
                    keys = (PdfName[])objs[0];
                    dic = (PdfDictionary)objs[1];
                    idx = ((Integer)objs[2]).intValue();
                }
            }
            if (ar != null) {
                for (int k = idx; k < ar.size(); ++k) {
                    PdfObject v = ar.get(k);
                    if (v.isIndirect()) {
                        int num = ((PRIndirectReference)v).getNumber();
                        if (num >= xrefObj.size() || !partial && xrefObj.get(num) == null) {
                            ar.set(k, PdfNull.PDFNULL);
                            continue;
                        }
                    }
                    if (objs == null)
                        state.push(new Object[]{ar, Integer.valueOf(k + 1)});
                    else {
                        objs[1] = Integer.valueOf(k + 1);
                        state.push(objs);
                    }
                    state.push(v);
                    break;
                }
            }
            else {
                for (int k = idx; k < keys.length; ++k) {
                    PdfName key = keys[k];
                    PdfObject v = dic.get(key);
                    if (v.isIndirect()) {
                        int num = ((PRIndirectReference)v).getNumber();
                        if (num < 0 || num >= xrefObj.size() || !partial && xrefObj.get(num) == null) {
                            dic.put(key, PdfNull.PDFNULL);
                            continue;
                        }
                    }
                    if (objs == null)
                        state.push(new Object[]{keys, dic, Integer.valueOf(k + 1)});
                    else {
                        objs[2] = Integer.valueOf(k + 1);
                        state.push(objs);
                    }
                    state.push(v);
                    break;
                }
            }
        }
    }

    /**
     * Removes all the unreachable objects.
     * @return the number of indirect objects removed
     */
    public int removeUnusedObjects() {
        boolean hits[] = new boolean[xrefObj.size()];
        removeUnusedNode(trailer, hits);
        int total = 0;
        if (partial) {
            for (int k = 1; k < hits.length; ++k) {
                if (!hits[k]) {
                    xref[k * 2] = -1;
                    xref[k * 2 + 1] = 0;
                    xrefObj.set(k, null);
                    ++total;
                }
            }
        }
        else {
            for (int k = 1; k < hits.length; ++k) {
                if (!hits[k]) {
                    xrefObj.set(k, null);
                    ++total;
                }
            }
        }
        return total;
    }

    /** Gets a read-only version of <CODE>AcroFields</CODE>.
     * @return a read-only version of <CODE>AcroFields</CODE>
     */
    public AcroFields getAcroFields() {
        return new AcroFields(this, null);
    }

    /**
     * Gets the global document JavaScript.
     * @param file the document file
     * @throws IOException on error
     * @return the global document JavaScript
     */
    public String getJavaScript(final RandomAccessFileOrArray file) throws IOException {
        PdfDictionary names = (PdfDictionary)getPdfObjectRelease(catalog.get(PdfName.NAMES));
        if (names == null)
            return null;
        PdfDictionary js = (PdfDictionary)getPdfObjectRelease(names.get(PdfName.JAVASCRIPT));
        if (js == null)
            return null;
        HashMap<String, PdfObject> jscript = PdfNameTree.readTree(js);
        String sortedNames[] = new String[jscript.size()];
        sortedNames = jscript.keySet().toArray(sortedNames);
        Arrays.sort(sortedNames);
        StringBuffer buf = new StringBuffer();
        for (int k = 0; k < sortedNames.length; ++k) {
            PdfDictionary j = (PdfDictionary)getPdfObjectRelease(jscript.get(sortedNames[k]));
            if (j == null)
                continue;
            PdfObject obj = getPdfObjectRelease(j.get(PdfName.JS));
            if (obj != null) {
                if (obj.isString())
                    buf.append(((PdfString)obj).toUnicodeString()).append('\n');
                else if (obj.isStream()) {
                    byte bytes[] = getStreamBytes((PRStream)obj, file);
                    if (bytes.length >= 2 && bytes[0] == (byte)254 && bytes[1] == (byte)255)
                        buf.append(PdfEncodings.convertToString(bytes, PdfObject.TEXT_UNICODE));
                    else
                        buf.append(PdfEncodings.convertToString(bytes, PdfObject.TEXT_PDFDOCENCODING));
                    buf.append('\n');
                }
            }
        }
        return buf.toString();
    }

    /**
     * Gets the global document JavaScript.
     * @throws IOException on error
     * @return the global document JavaScript
     */
    public String getJavaScript() throws IOException {
        RandomAccessFileOrArray rf = getSafeFile();
        try {
            rf.reOpen();
            return getJavaScript(rf);
        }
        finally {
            try{rf.close();}catch(Exception e){}
        }
    }

    /**
     * Selects the pages to keep in the document. The pages are described as
     * ranges. The page ordering can be changed but
     * no page repetitions are allowed. Note that it may be very slow in partial mode.
     * @param ranges the comma separated ranges as described in {@link SequenceList}
     */
    public void selectPages(final String ranges) {
        selectPages(SequenceList.expand(ranges, getNumberOfPages()));
    }

    /**
     * Selects the pages to keep in the document. The pages are described as a
     * <CODE>List</CODE> of <CODE>Integer</CODE>. The page ordering can be changed but
     * no page repetitions are allowed. Note that it may be very slow in partial mode.
     * @param pagesToKeep the pages to keep in the document
     */
    public void selectPages(final List<Integer> pagesToKeep) {
        selectPages(pagesToKeep, true);
    }

    /**
     * Selects the pages to keep in the document. The pages are described as a
     * <CODE>List</CODE> of <CODE>Integer</CODE>. The page ordering can be changed but
     * no page repetitions are allowed. Note that it may be very slow in partial mode.
     * @param pagesToKeep the pages to keep in the document
     * @param removeUnused indicate if to remove unsed objects. @see removeUnusedObjects
     */
    protected void selectPages(final List<Integer> pagesToKeep, boolean removeUnused) {
        pageRefs.selectPages(pagesToKeep);
        if (removeUnused) removeUnusedObjects();
    }

    /** Sets the viewer preferences as the sum of several constants.
     * @param preferences the viewer preferences
     * @see PdfViewerPreferences#setViewerPreferences
     */
    public void setViewerPreferences(final int preferences) {
    	this.viewerPreferences.setViewerPreferences(preferences);
        setViewerPreferences(this.viewerPreferences);
    }

    /** Adds a viewer preference
     * @param key a key for a viewer preference
     * @param value	a value for the viewer preference
     * @see PdfViewerPreferences#addViewerPreference
     */
    public void addViewerPreference(final PdfName key, final PdfObject value) {
    	this.viewerPreferences.addViewerPreference(key, value);
        setViewerPreferences(this.viewerPreferences);
    }

    public void setViewerPreferences(final PdfViewerPreferencesImp vp) {
    	vp.addToCatalog(catalog);
    }

    /**
     * Returns a bitset representing the PageMode and PageLayout viewer preferences.
     * Doesn't return any information about the ViewerPreferences dictionary.
     * @return an int that contains the Viewer Preferences.
     */
    public int getSimpleViewerPreferences() {
    	return PdfViewerPreferencesImp.getViewerPreferences(catalog).getPageLayoutAndMode();
    }

    /**
     * Getter for property appendable.
     * @return Value of property appendable.
     */
    public boolean isAppendable() {
        return this.appendable;
    }

    /**
     * Setter for property appendable.
     * @param appendable New value of property appendable.
     */
    public void setAppendable(final boolean appendable) {
        this.appendable = appendable;
        if (appendable)
            getPdfObject(trailer.get(PdfName.ROOT));
    }

    /**
     * Getter for property newXrefType.
     * @return Value of property newXrefType.
     */
    public boolean isNewXrefType() {
        return newXrefType;
    }

    /**
     * Getter for property fileLength.
     * @return Value of property fileLength.
     */
    public long getFileLength() {
        return fileLength;
    }

    /**
     * Getter for property hybridXref.
     * @return Value of property hybridXref.
     */
    public boolean isHybridXref() {
        return hybridXref;
    }

    static class PageRefs {
        private final PdfReader reader;
        /** ArrayList with the indirect references to every page. Element 0 = page 1; 1 = page 2;... Not used for partial reading. */
        private ArrayList<PRIndirectReference> refsn;
        /** The number of pages, updated only in case of partial reading. */
        private int sizep;
        /** intHashtable that does the same thing as refsn in case of partial reading: major difference: not all the pages are read. */
        private IntHashtable refsp;
        /** Page number of the last page that was read (partial reading only) */
        private int lastPageRead = -1;
        /** stack to which pages dictionaries are pushed to keep track of the current page attributes */
        private ArrayList<PdfDictionary> pageInh;
        private boolean keepPages;
        /**
         * Keeps track of all pages nodes to avoid circular references.
         */
        private Set<PdfObject> pagesNodes = new HashSet<PdfObject>();

        private PageRefs(final PdfReader reader) throws IOException {
            this.reader = reader;
            if (reader.partial) {
                refsp = new IntHashtable();
                PdfNumber npages = (PdfNumber)PdfReader.getPdfObjectRelease(reader.rootPages.get(PdfName.COUNT));
                sizep = npages.intValue();
            }
            else {
                readPages();
            }
        }

        PageRefs(final PageRefs other, final PdfReader reader) {
            this.reader = reader;
            this.sizep = other.sizep;
            if (other.refsn != null) {
                refsn = new ArrayList<PRIndirectReference>(other.refsn);
                for (int k = 0; k < refsn.size(); ++k) {
                    refsn.set(k, (PRIndirectReference)duplicatePdfObject(refsn.get(k), reader));
                }
            }
            else
                this.refsp = (IntHashtable)other.refsp.clone();
        }

        int size() {
            if (refsn != null)
                return refsn.size();
            else
                return sizep;
        }

        void readPages() throws IOException {
            if (refsn != null)
                return;
            refsp = null;
            refsn = new ArrayList<PRIndirectReference>();
            pageInh = new ArrayList<PdfDictionary>();
            iteratePages((PRIndirectReference)reader.catalog.get(PdfName.PAGES));
            pageInh = null;
            reader.rootPages.put(PdfName.COUNT, new PdfNumber(refsn.size()));
        }

        void reReadPages() throws IOException {
            refsn = null;
            readPages();
        }

        /** Gets the dictionary that represents a page.
         * @param pageNum the page number. 1 is the first
         * @return the page dictionary
         */
        public PdfDictionary getPageN(final int pageNum) {
            PRIndirectReference ref = getPageOrigRef(pageNum);
            return (PdfDictionary)PdfReader.getPdfObject(ref);
        }

        /**
         * @param pageNum
         * @return a dictionary object
         */
        public PdfDictionary getPageNRelease(final int pageNum) {
            PdfDictionary page = getPageN(pageNum);
            releasePage(pageNum);
            return page;
        }

        /**
         * @param pageNum
         * @return an indirect reference
         */
        public PRIndirectReference getPageOrigRefRelease(final int pageNum) {
            PRIndirectReference ref = getPageOrigRef(pageNum);
            releasePage(pageNum);
            return ref;
        }

        /**
         * Gets the page reference to this page.
         * @param pageNum the page number. 1 is the first
         * @return the page reference
         */
        public PRIndirectReference getPageOrigRef(int pageNum) {
            try {
                --pageNum;
                if (pageNum < 0 || pageNum >= size())
                    return null;
                if (refsn != null)
                    return refsn.get(pageNum);
                else {
                    int n = refsp.get(pageNum);
                    if (n == 0) {
                        PRIndirectReference ref = getSinglePage(pageNum);
                        if (reader.lastXrefPartial == -1)
                            lastPageRead = -1;
                        else
                            lastPageRead = pageNum;
                        reader.lastXrefPartial = -1;
                        refsp.put(pageNum, ref.getNumber());
                        if (keepPages)
                            lastPageRead = -1;
                        return ref;
                    }
                    else {
                        if (lastPageRead != pageNum)
                            lastPageRead = -1;
                        if (keepPages)
                            lastPageRead = -1;
                        return new PRIndirectReference(reader, n);
                    }
                }
            }
            catch (Exception e) {
                throw new ExceptionConverter(e);
            }
        }

        void keepPages() {
            if (refsp == null || keepPages)
                return;
            keepPages = true;
            refsp.clear();
        }

        /**
         * @param pageNum
         */
        public void releasePage(int pageNum) {
            if (refsp == null)
                return;
            --pageNum;
            if (pageNum < 0 || pageNum >= size())
                return;
            if (pageNum != lastPageRead)
                return;
            lastPageRead = -1;
            reader.lastXrefPartial = refsp.get(pageNum);
            reader.releaseLastXrefPartial();
            refsp.remove(pageNum);
        }

        /**
         *
         */
        public void resetReleasePage() {
            if (refsp == null)
                return;
            lastPageRead = -1;
        }

        void insertPage(int pageNum, final PRIndirectReference ref) {
            --pageNum;
            if (refsn != null) {
                if (pageNum >= refsn.size())
                    refsn.add(ref);
                else
                    refsn.add(pageNum, ref);
            }
            else {
                ++sizep;
                lastPageRead = -1;
                if (pageNum >= size()) {
                    refsp.put(size(), ref.getNumber());
                }
                else {
                    IntHashtable refs2 = new IntHashtable((refsp.size() + 1) * 2);
                    for (Iterator<IntHashtable.Entry> it = refsp.getEntryIterator(); it.hasNext();) {
                        IntHashtable.Entry entry = it.next();
                        int p = entry.getKey();
                        refs2.put(p >= pageNum ? p + 1 : p, entry.getValue());
                    }
                    refs2.put(pageNum, ref.getNumber());
                    refsp = refs2;
                }
            }
        }

        /**
         * Adds a PdfDictionary to the pageInh stack to keep track of the page attributes.
         * @param nodePages	a Pages dictionary
         */
        private void pushPageAttributes(final PdfDictionary nodePages) {
            PdfDictionary dic = new PdfDictionary();
            if (!pageInh.isEmpty()) {
                dic.putAll(pageInh.get(pageInh.size() - 1));
            }
            for (int k = 0; k < pageInhCandidates.length; ++k) {
                PdfObject obj = nodePages.get(pageInhCandidates[k]);
                if (obj != null)
                    dic.put(pageInhCandidates[k], obj);
            }
            pageInh.add(dic);
        }

        /**
         * Removes the last PdfDictionary that was pushed to the pageInh stack.
         */
        private void popPageAttributes() {
            pageInh.remove(pageInh.size() - 1);
        }

        private void iteratePages(final PRIndirectReference rpage) throws IOException {
            PdfDictionary page = (PdfDictionary)getPdfObject(rpage);
            if (page == null)
                return;
            if (!pagesNodes.add(getPdfObject(rpage)))
                throw new InvalidPdfException(MessageLocalization.getComposedMessage("illegal.pages.tree"));
            PdfArray kidsPR = page.getAsArray(PdfName.KIDS);
            // reference to a leaf
            if (kidsPR == null) {
                page.put(PdfName.TYPE, PdfName.PAGE);
                PdfDictionary dic = pageInh.get(pageInh.size() - 1);
                PdfName key;
                for (Object element : dic.getKeys()) {
                    key = (PdfName) element;
                    if (page.get(key) == null)
                        page.put(key, dic.get(key));
                }
                if (page.get(PdfName.MEDIABOX) == null) {
                    PdfArray arr = new PdfArray(new float[]{0,0,PageSize.LETTER.getRight(),PageSize.LETTER.getTop()});
                    page.put(PdfName.MEDIABOX, arr);
                }
                refsn.add(rpage);
            }
            // reference to a branch
            else {
                page.put(PdfName.TYPE, PdfName.PAGES);
                pushPageAttributes(page);
                for (int k = 0; k < kidsPR.size(); ++k){
                    PdfObject obj = kidsPR.getPdfObject(k);
                    if (!obj.isIndirect()) {
                        while (k < kidsPR.size())
                            kidsPR.remove(k);
                        break;
                    }
                    iteratePages((PRIndirectReference)obj);
                }
                popPageAttributes();
            }
        }

        protected PRIndirectReference getSinglePage(final int n) {
            PdfDictionary acc = new PdfDictionary();
            PdfDictionary top = reader.rootPages;
            int base = 0;
            while (true) {
                for (int k = 0; k < pageInhCandidates.length; ++k) {
                    PdfObject obj = top.get(pageInhCandidates[k]);
                    if (obj != null)
                        acc.put(pageInhCandidates[k], obj);
                }
                PdfArray kids = (PdfArray)PdfReader.getPdfObjectRelease(top.get(PdfName.KIDS));
                for (Iterator<PdfObject> it = kids.listIterator(); it.hasNext();) {
                    PRIndirectReference ref = (PRIndirectReference)it.next();
                    PdfDictionary dic = (PdfDictionary)getPdfObject(ref);
                    int last = reader.lastXrefPartial;
                    PdfObject count = getPdfObjectRelease(dic.get(PdfName.COUNT));
                    reader.lastXrefPartial = last;
                    int acn = 1;
                    if (count != null && count.type() == PdfObject.NUMBER)
                        acn = ((PdfNumber)count).intValue();
                    if (n < base + acn) {
                        if (count == null) {
                            dic.mergeDifferent(acc);
                            return ref;
                        }
                        reader.releaseLastXrefPartial();
                        top = dic;
                        break;
                    }
                    reader.releaseLastXrefPartial();
                    base += acn;
                }
            }
        }

        private void selectPages(final List<Integer> pagesToKeep) {
            IntHashtable pg = new IntHashtable();
            ArrayList<Integer> finalPages = new ArrayList<Integer>();
            int psize = size();
            for (Integer pi : pagesToKeep) {
                int p = pi.intValue();
                if (p >= 1 && p <= psize && pg.put(p, 1) == 0)
                    finalPages.add(pi);
            }
            if (reader.partial) {
                for (int k = 1; k <= psize; ++k) {
                    getPageOrigRef(k);
                    resetReleasePage();
                }
            }
            PRIndirectReference parent = (PRIndirectReference)reader.catalog.get(PdfName.PAGES);
            PdfDictionary topPages = (PdfDictionary)PdfReader.getPdfObject(parent);
            ArrayList<PRIndirectReference> newPageRefs = new ArrayList<PRIndirectReference>(finalPages.size());
            PdfArray kids = new PdfArray();
            for (int k = 0; k < finalPages.size(); ++k) {
                int p = finalPages.get(k).intValue();
                PRIndirectReference pref = getPageOrigRef(p);
                resetReleasePage();
                kids.add(pref);
                newPageRefs.add(pref);
                getPageN(p).put(PdfName.PARENT, parent);
            }
            AcroFields af = reader.getAcroFields();
            boolean removeFields = af.getFields().size() > 0;
            for (int k = 1; k <= psize; ++k) {
                if (!pg.containsKey(k)) {
                    if (removeFields)
                        af.removeFieldsFromPage(k);
                    PRIndirectReference pref = getPageOrigRef(k);
                    int nref = pref.getNumber();
                    reader.xrefObj.set(nref, null);
                    if (reader.partial) {
                        reader.xref[nref * 2] = -1;
                        reader.xref[nref * 2 + 1] = 0;
                    }
                }
            }
            topPages.put(PdfName.COUNT, new PdfNumber(finalPages.size()));
            topPages.put(PdfName.KIDS, kids);
            refsp = null;
            refsn = newPageRefs;
        }
    }

    PdfIndirectReference getCryptoRef() {
        if (cryptoRef == null)
            return null;
        return new PdfIndirectReference(0, cryptoRef.getNumber(), cryptoRef.getGeneration());
    }
    
    /**
     * Checks if this PDF has usage rights enabled.
     * 
     * @return <code>true</code> if usage rights are present; <code>false</code> otherwise
     */
    public boolean hasUsageRights() {
        PdfDictionary perms = catalog.getAsDict(PdfName.PERMS);
        if (perms == null)
            return false;
        return perms.contains(PdfName.UR) || perms.contains(PdfName.UR3);
    }

    /**
     * Removes any usage rights that this PDF may have. Only Adobe can grant usage rights
     * and any PDF modification with iText will invalidate them. Invalidated usage rights may
     * confuse Acrobat and it's advisable to remove them altogether.
     */
    public void removeUsageRights() {
        PdfDictionary perms = catalog.getAsDict(PdfName.PERMS);
        if (perms == null)
            return;
        perms.remove(PdfName.UR);
        perms.remove(PdfName.UR3);
        if (perms.size() == 0)
            catalog.remove(PdfName.PERMS);
    }

    /**
     * Gets the certification level for this document. The return values can be <code>PdfSignatureAppearance.NOT_CERTIFIED</code>,
     * <code>PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED</code>,
     * <code>PdfSignatureAppearance.CERTIFIED_FORM_FILLING</code> and
     * <code>PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS</code>.
     * <p>
     * No signature validation is made, use the methods available for that in <CODE>AcroFields</CODE>.
     * </p>
     * @return gets the certification level for this document
     */
    public int getCertificationLevel() {
        PdfDictionary dic = catalog.getAsDict(PdfName.PERMS);
        if (dic == null)
            return PdfSignatureAppearance.NOT_CERTIFIED;
        dic = dic.getAsDict(PdfName.DOCMDP);
        if (dic == null)
            return PdfSignatureAppearance.NOT_CERTIFIED;
        PdfArray arr = dic.getAsArray(PdfName.REFERENCE);
        if (arr == null || arr.size() == 0)
            return PdfSignatureAppearance.NOT_CERTIFIED;
        dic = arr.getAsDict(0);
        if (dic == null)
            return PdfSignatureAppearance.NOT_CERTIFIED;
        dic = dic.getAsDict(PdfName.TRANSFORMPARAMS);
        if (dic == null)
            return PdfSignatureAppearance.NOT_CERTIFIED;
        PdfNumber p = dic.getAsNumber(PdfName.P);
        if (p == null)
            return PdfSignatureAppearance.NOT_CERTIFIED;
        return p.intValue();
    }

    /**
     * Checks if the document was opened with the owner password so that the end application
     * can decide what level of access restrictions to apply. If the document is not encrypted
     * it will return <CODE>true</CODE>.
     * @return <CODE>true</CODE> if the document was opened with the owner password or if it's not encrypted,
     * <CODE>false</CODE> if the document was opened with the user password
     */
    public final boolean isOpenedWithFullPermissions() {
        return !encrypted || ownerPasswordUsed || unethicalreading;
    }

    /**
     * @return the crypto mode, or -1 of none
     */
    public int getCryptoMode() {
    	if (decrypt == null)
    		return -1;
    	else
    		return decrypt.getCryptoMode();
    }

    /**
     * @return true if the metadata is encrypted.
     */
    public boolean isMetadataEncrypted() {
    	if (decrypt == null)
    		return false;
    	else
    		return decrypt.isMetadataEncrypted();
    }

    /**
     * Computes user password if standard encryption handler is used with Standard40, Standard128 or AES128 encryption algorithm.
     *
     * @return user password, or null if not a standard encryption handler was used,
     *         if standard encryption handler was used with AES256 encryption algorithm,
     *         or if ownerPasswordUsed wasn't use to open the document.
     */
    public byte[] computeUserPassword() {
    	if (!encrypted || !ownerPasswordUsed) return null;
    	return decrypt.computeUserPassword(password);
    }
}

com/itextpdf/text/pdf/PdfReader.java

 

⇒ iText-2.1.6.jar - iText, a JAVA-PDF library

⇐ iText layout.jar Source Code

⇑ Download and Install iText Java Library

⇑⇑ iText for PDF Generation

2021-07-03, 47868👍, 0💬