All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.pdfbox.pdfwriter.COSWriter Maven / Gradle / Ivy

Go to download

The Apache PDFBox library is an open source Java tool for working with PDF documents.

There is a newer version: 3.0.2
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.pdfbox.pdfwriter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSBoolean;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.cos.COSFloat;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNull;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSObjectKey;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.cos.COSUpdateInfo;
import org.apache.pdfbox.cos.ICOSVisitor;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.io.RandomAccessBuffer;
import org.apache.pdfbox.io.RandomAccessInputStream;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.pdfparser.PDFXRefStream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.encryption.SecurityHandler;
import org.apache.pdfbox.pdmodel.fdf.FDFDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.apache.pdfbox.util.Charsets;
import org.apache.pdfbox.util.Hex;

/**
 * This class acts on a in-memory representation of a PDF document.
 *
 * @author Michael Traut
 * @author Ben Litchfield
 */
public class COSWriter implements ICOSVisitor, Closeable
{
    private static final Log LOG = LogFactory.getLog(COSWriter.class);

    /**
     * The dictionary open token.
     */
    public static final byte[] DICT_OPEN = "<<".getBytes(Charsets.US_ASCII);
    /**
     * The dictionary close token.
     */
    public static final byte[] DICT_CLOSE = ">>".getBytes(Charsets.US_ASCII);
    /**
     * space character.
     */
    public static final byte[] SPACE = { ' ' };
    /**
     * The start to a PDF comment.
     */
    public static final byte[] COMMENT = { '%' };

    /**
     * The output version of the PDF.
     */
    public static final byte[] VERSION = "PDF-1.4".getBytes(Charsets.US_ASCII);
    /**
     * Garbage bytes used to create the PDF header.
     */
    public static final byte[] GARBAGE = new byte[] {(byte)0xf6, (byte)0xe4, (byte)0xfc, (byte)0xdf};
    /**
     * The EOF constant.
     */
    public static final byte[] EOF = "%%EOF".getBytes(Charsets.US_ASCII);
    // pdf tokens

    /**
     * The reference token.
     */
    public static final byte[] REFERENCE = "R".getBytes(Charsets.US_ASCII);
    /**
     * The XREF token.
     */
    public static final byte[] XREF = "xref".getBytes(Charsets.US_ASCII);
    /**
     * The xref free token.
     */
    public static final byte[] XREF_FREE = "f".getBytes(Charsets.US_ASCII);
    /**
     * The xref used token.
     */
    public static final byte[] XREF_USED = "n".getBytes(Charsets.US_ASCII);
    /**
     * The trailer token.
     */
    public static final byte[] TRAILER = "trailer".getBytes(Charsets.US_ASCII);
    /**
     * The start xref token.
     */
    public static final byte[] STARTXREF = "startxref".getBytes(Charsets.US_ASCII);
    /**
     * The starting object token.
     */
    public static final byte[] OBJ = "obj".getBytes(Charsets.US_ASCII);
    /**
     * The end object token.
     */
    public static final byte[] ENDOBJ = "endobj".getBytes(Charsets.US_ASCII);
    /**
     * The array open token.
     */
    public static final byte[] ARRAY_OPEN = "[".getBytes(Charsets.US_ASCII);
    /**
     * The array close token.
     */
    public static final byte[] ARRAY_CLOSE = "]".getBytes(Charsets.US_ASCII);
    /**
     * The open stream token.
     */
    public static final byte[] STREAM = "stream".getBytes(Charsets.US_ASCII);
    /**
     * The close stream token.
     */
    public static final byte[] ENDSTREAM = "endstream".getBytes(Charsets.US_ASCII);
    
    private final NumberFormat formatXrefOffset = new DecimalFormat("0000000000");

    // the decimal format for the xref object generation number data
    private final NumberFormat formatXrefGeneration = new DecimalFormat("00000");

    private final NumberFormat formatDecimal = NumberFormat.getNumberInstance( Locale.US );

    // the stream where we create the pdf output
    private OutputStream output;

    // the stream used to write standard cos data
    private COSStandardOutputStream standardOutput;

    // the start position of the x ref section
    private long startxref = 0;

    // the current object number
    private long number = 0;

    // maps the object to the keys generated in the writer
    // these are used for indirect references in other objects
    //A hashtable is used on purpose over a hashmap
    //so that null entries will not get added.
    private final Map objectKeys = new Hashtable();
    private final Map keyObject = new Hashtable();

    // the list of x ref entries to be made so far
    private final List xRefEntries = new ArrayList();
    private final Set objectsToWriteSet = new HashSet();

    //A list of objects to write.
    private final Deque objectsToWrite = new LinkedList();

    //a list of objects already written
    private final Set writtenObjects = new HashSet();

    //An 'actual' is any COSBase that is not a COSObject.
    //need to keep a list of the actuals that are added
    //as well as the objects because there is a problem
    //when adding a COSObject and then later adding
    //the actual for that object, so we will track
    //actuals separately.
    private final Set actualsAdded = new HashSet();

    private COSObjectKey currentObjectKey = null;
    private PDDocument pdDocument = null;
    private FDFDocument fdfDocument = null;
    private boolean willEncrypt = false;

    // signing
    private boolean incrementalUpdate = false;
    private boolean reachedSignature = false;
    private long signatureOffset, signatureLength;
    private long byteRangeOffset, byteRangeLength;
    private RandomAccessRead incrementalInput;
    private RandomAccessRead tempIncInput;
    private OutputStream incrementalOutput;
    private SignatureInterface signatureInterface;

    /**
     * COSWriter constructor comment.
     *
     * @param os The wrapped output stream.
     */
    public COSWriter(OutputStream os)
    {
        super();
        setOutput(os);
        setStandardOutput(new COSStandardOutputStream(output));
        formatDecimal.setMaximumFractionDigits( 10 );
        formatDecimal.setGroupingUsed( false );
    }
    
    /**
     * COSWriter constructor for incremental updates. 
     *
     * @param outputStream output stream where the new PDF data will be written
     * @param inputStream input stream containing source PDF data
     * 
     * @throws IOException if something went wrong
     * @deprecated Use {@link #COSWriter(OutputStream, RandomAccessRead)} instead
     */
    public COSWriter(OutputStream outputStream, InputStream inputStream) throws IOException
    {
        super();
        tempIncInput = new RandomAccessBuffer(inputStream);
        initWriter(outputStream, tempIncInput);
    }

    /**
     * COSWriter constructor for incremental updates. 
     *
     * @param outputStream output stream where the new PDF data will be written
     * @param inputData random access read containing source PDF data
     * 
     * @throws IOException if something went wrong
     */
    public COSWriter(OutputStream outputStream, RandomAccessRead inputData) throws IOException
    {
        super();
        initWriter(outputStream, inputData);
    }

    private void initWriter(OutputStream outputStream, RandomAccessRead inputData) throws IOException
    {
        // write to buffer instead of output
        setOutput(new ByteArrayOutputStream());
        setStandardOutput(new COSStandardOutputStream(output, (int)inputData.length()));

        incrementalInput = inputData;
        incrementalOutput = outputStream;
        incrementalUpdate = true;

        formatDecimal.setMaximumFractionDigits( 10 );
        formatDecimal.setGroupingUsed( false );
    }

    private void prepareIncrement(PDDocument doc)
    {
      try
      {
        if (doc != null)
        {
          COSDocument cosDoc = doc.getDocument();
          
          Map xrefTable = cosDoc.getXrefTable();
          Set keySet = xrefTable.keySet();
          long highestNumber=0;
          for ( COSObjectKey cosObjectKey : keySet ) 
          {
            COSBase object = cosDoc.getObjectFromPool(cosObjectKey).getObject();
            if (object != null && cosObjectKey!= null && !(object instanceof COSNumber))
            {
                objectKeys.put(object, cosObjectKey);
                keyObject.put(cosObjectKey,object);
            }
            
            if (cosObjectKey != null)
            {
                long num = cosObjectKey.getNumber();
                if (num > highestNumber)
                {
                    highestNumber = num;
                }
            }
          }
          setNumber(highestNumber);
        }
      }
      catch (IOException e)
      {
          LOG.error(e,e);
      }
    }
    
    /**
     * add an entry in the x ref table for later dump.
     *
     * @param entry The new entry to add.
     */
    protected void addXRefEntry(COSWriterXRefEntry entry)
    {
        getXRefEntries().add(entry);
    }

    /**
     * This will close the stream.
     *
     * @throws IOException If the underlying stream throws an exception.
     */
    @Override
    public void close() throws IOException
    {
        if (getStandardOutput() != null)
        {
            getStandardOutput().close();
        }
        if (getOutput() != null)
        {
            getOutput().close();
        }
        if (incrementalOutput != null)
        {
            incrementalOutput.close();
        }
        if (tempIncInput != null)
        {
            incrementalOutput.close();
        }
    }

    /**
     * This will get the current object number.
     *
     * @return The current object number.
     */
    protected long getNumber()
    {
        return number;
    }

    /**
     * This will get all available object keys.
     *
     * @return A map of all object keys.
     */
    public Map getObjectKeys()
    {
        return objectKeys;
    }

    /**
     * This will get the output stream.
     *
     * @return The output stream.
     */
    protected java.io.OutputStream getOutput()
    {
        return output;
    }

    /**
     * This will get the standard output stream.
     *
     * @return The standard output stream.
     */
    protected COSStandardOutputStream getStandardOutput()
    {
        return standardOutput;
    }

    /**
     * This will get the current start xref.
     *
     * @return The current start xref.
     */
    protected long getStartxref()
    {
        return startxref;
    }
    /**
     * This will get the xref entries.
     *
     * @return All available xref entries.
     */
    protected List getXRefEntries()
    {
        return xRefEntries;
    }

    /**
     * This will set the current object number.
     *
     * @param newNumber The new object number.
     */
    protected void setNumber(long newNumber)
    {
        number = newNumber;
    }

    /**
     * This will set the output stream.
     *
     * @param newOutput The new output stream.
     */
    private void setOutput( OutputStream newOutput )
    {
        output = newOutput;
    }

    /**
     * This will set the standard output stream.
     *
     * @param newStandardOutput The new standard output stream.
     */
    private void setStandardOutput(COSStandardOutputStream newStandardOutput)
    {
        standardOutput = newStandardOutput;
    }

    /**
     * This will set the start xref.
     *
     * @param newStartxref The new start xref attribute.
     */
    protected void setStartxref(long newStartxref)
    {
        startxref = newStartxref;
    }

    /**
     * This will write the body of the document.
     *
     * @param doc The document to write the body for.
     *
     * @throws IOException If there is an error writing the data.
     */
    protected void doWriteBody(COSDocument doc) throws IOException
    {
        COSDictionary trailer = doc.getTrailer();
        COSDictionary root = (COSDictionary)trailer.getDictionaryObject( COSName.ROOT );
        COSDictionary info = (COSDictionary)trailer.getDictionaryObject( COSName.INFO );
        COSDictionary encrypt = (COSDictionary)trailer.getDictionaryObject( COSName.ENCRYPT );
        if( root != null )
        {
            addObjectToWrite( root );
        }
        if( info != null )
        {
            addObjectToWrite( info );
        }

        doWriteObjects();
        willEncrypt = false;
        if( encrypt != null )
        {
            addObjectToWrite( encrypt );
        }

        doWriteObjects();
    }

    private void doWriteObjects() throws IOException
    {
        while( objectsToWrite.size() > 0 )
        {
            COSBase nextObject = objectsToWrite.removeFirst();
            objectsToWriteSet.remove(nextObject);
            doWriteObject( nextObject );
        }
    }

    private void addObjectToWrite( COSBase object )
    {
        COSBase actual = object;
        if( actual instanceof COSObject )
        {
            actual = ((COSObject)actual).getObject();
        }

        if( !writtenObjects.contains( object ) &&
            !objectsToWriteSet.contains( object ) &&
            !actualsAdded.contains( actual ) )
        {
            COSBase cosBase=null;
            COSObjectKey cosObjectKey = null;
            if(actual != null)
            {
                cosObjectKey= objectKeys.get(actual);
            }
            if(cosObjectKey!=null)
            {
                cosBase = keyObject.get(cosObjectKey);
            }
            if (actual != null && objectKeys.containsKey(actual) 
                    && object instanceof COSUpdateInfo && !((COSUpdateInfo)object).isNeedToBeUpdated() 
                    && cosBase instanceof COSUpdateInfo && !((COSUpdateInfo)cosBase).isNeedToBeUpdated() )
            {
                return;
            }
            objectsToWrite.add( object );
            objectsToWriteSet.add( object );
            if( actual != null )
            {
                actualsAdded.add( actual );
            }
        }
    }

    /**
     * This will write a COS object.
     *
     * @param obj The object to write.
     *
     * @throws IOException if the output cannot be written
     */
    public void doWriteObject( COSBase obj ) throws IOException
    {
        writtenObjects.add( obj );
        if(obj instanceof COSDictionary)
        {
            COSBase itemType = ((COSDictionary) obj).getItem(COSName.TYPE);
            if (COSName.SIG.equals(itemType) || COSName.DOC_TIME_STAMP.equals(itemType))
            {
                reachedSignature = true;
            }
        }

        // find the physical reference
        currentObjectKey = getObjectKey( obj );
        // add a x ref entry
        addXRefEntry( new COSWriterXRefEntry(getStandardOutput().getPos(), obj, currentObjectKey));
        // write the object
        getStandardOutput().write(String.valueOf(currentObjectKey.getNumber()).getBytes(Charsets.ISO_8859_1));
        getStandardOutput().write(SPACE);
        getStandardOutput().write(String.valueOf(currentObjectKey.getGeneration()).getBytes(Charsets.ISO_8859_1));
        getStandardOutput().write(SPACE);
        getStandardOutput().write(OBJ);
        getStandardOutput().writeEOL();
        obj.accept( this );
        getStandardOutput().writeEOL();
        getStandardOutput().write(ENDOBJ);
        getStandardOutput().writeEOL();
    }

    /**
     * This will write the header to the PDF document.
     *
     * @param doc The document to get the data from.
     *
     * @throws IOException If there is an error writing to the stream.
     */
    protected void doWriteHeader(COSDocument doc) throws IOException
    {
        String headerString;
        if (fdfDocument != null)
        {
            headerString = "%FDF-"+ Float.toString(fdfDocument.getDocument().getVersion());
        }
        else
        {
            headerString = "%PDF-"+ Float.toString(pdDocument.getDocument().getVersion());
        }
        getStandardOutput().write( headerString.getBytes(Charsets.ISO_8859_1) );
        
        getStandardOutput().writeEOL();
        getStandardOutput().write(COMMENT);
        getStandardOutput().write(GARBAGE);
        getStandardOutput().writeEOL();
    }


    /**
     * This will write the trailer to the PDF document.
     *
     * @param doc The document to create the trailer for.
     *
     * @throws IOException If there is an IOError while writing the document.
     */
    protected void doWriteTrailer(COSDocument doc) throws IOException
    {
        getStandardOutput().write(TRAILER);
        getStandardOutput().writeEOL();

        COSDictionary trailer = doc.getTrailer();
        //sort xref, needed only if object keys not regenerated
        Collections.sort(getXRefEntries());
        COSWriterXRefEntry lastEntry = getXRefEntries().get( getXRefEntries().size()-1);
        trailer.setLong(COSName.SIZE, lastEntry.getKey().getNumber()+1);
        // Only need to stay, if an incremental update will be performed
        if (!incrementalUpdate) 
        {
          trailer.removeItem( COSName.PREV );
        }
        if (!doc.isXRefStream())
        {
            trailer.removeItem( COSName.XREF_STM );
        }
        // Remove a checksum if present
        trailer.removeItem( COSName.DOC_CHECKSUM );
        
        trailer.accept(this);
    }

    private void doWriteXRefInc(COSDocument doc, long hybridPrev) throws IOException
    {
        if (doc.isXRefStream() || hybridPrev != -1)
        {
            // the file uses XrefStreams, so we need to update
            // it with an xref stream. We create a new one and fill it
            // with data available here

            // create a new XRefStrema object
            PDFXRefStream pdfxRefStream = new PDFXRefStream();

            // add all entries from the incremental update.
            List xRefEntries2 = getXRefEntries();
            for ( COSWriterXRefEntry cosWriterXRefEntry : xRefEntries2 )
            {
                pdfxRefStream.addEntry(cosWriterXRefEntry);
            }

            COSDictionary trailer = doc.getTrailer();
            if (incrementalUpdate)
            {
                // use previous startXref value as new PREV value
                trailer.setLong(COSName.PREV, doc.getStartXref());
            }
            else
            {
                trailer.removeItem(COSName.PREV);
            }
            pdfxRefStream.addTrailerInfo(trailer);
            // the size is the highest object number+1. we add one more
            // for the xref stream object we are going to write
            pdfxRefStream.setSize(getNumber() + 2);

            setStartxref(getStandardOutput().getPos());
            COSStream stream2 = pdfxRefStream.getStream();
            doWriteObject(stream2);
        }

        if (!doc.isXRefStream() || hybridPrev != -1)
        {
            COSDictionary trailer = doc.getTrailer();
            trailer.setLong(COSName.PREV, doc.getStartXref());
            if (hybridPrev != -1)
            {
                COSName xrefStm = COSName.XREF_STM;
                trailer.removeItem(xrefStm);
                trailer.setLong(xrefStm, getStartxref());
            }
            doWriteXRefTable();
            doWriteTrailer(doc);
        }
    }

    // writes the "xref" table
    private void doWriteXRefTable() throws IOException
    {
        addXRefEntry(COSWriterXRefEntry.getNullEntry());

        // sort xref, needed only if object keys not regenerated
        Collections.sort(getXRefEntries());

        // remember the position where x ref was written
        setStartxref(getStandardOutput().getPos());

        getStandardOutput().write(XREF);
        getStandardOutput().writeEOL();
        // write start object number and object count for this x ref section
        // we assume starting from scratch

        Long[] xRefRanges = getXRefRanges(getXRefEntries());
        int xRefLength = xRefRanges.length;
        int x = 0;
        int j = 0;
        while (x < xRefLength && (xRefLength % 2) == 0)
        {
            writeXrefRange(xRefRanges[x], xRefRanges[x + 1]);

            for (int i = 0; i < xRefRanges[x + 1]; ++i)
            {
                writeXrefEntry(xRefEntries.get(j++));
            }
            x += 2;
        }
    }

    /**
     * Write an incremental update for a non signature case. This can be used for e.g. augmenting signatures.
     * 
     * @throws IOException
     */
    private void doWriteIncrement() throws IOException
    {
        ByteArrayOutputStream byteOut = (ByteArrayOutputStream) output;
        byteOut.flush();
        byte[] buffer = byteOut.toByteArray();
        SequenceInputStream signStream = new SequenceInputStream(new RandomAccessInputStream(incrementalInput),
                                                                 new ByteArrayInputStream(buffer));
        // write the data to the incremental output stream
        IOUtils.copy(signStream, incrementalOutput);
    }
    
    private void doWriteSignature() throws IOException
    {
        // calculate the ByteRange values
        long inLength = incrementalInput.length();
        long beforeLength = signatureOffset;
        long afterOffset = signatureOffset + signatureLength;
        long afterLength = getStandardOutput().getPos() - (inLength + signatureLength) - (signatureOffset - inLength);

        String byteRange = "0 " + beforeLength + " " + afterOffset + " " + afterLength + "]";
        if (byteRangeLength - byteRange.length() < 0)
        {
            throw new IOException("Can't write new ByteRange, not enough space");
        }

        // copy the new incremental data into a buffer (e.g. signature dict, trailer)
        ByteArrayOutputStream byteOut = (ByteArrayOutputStream) output;
        byteOut.flush();
        byte[] buffer = byteOut.toByteArray();

        // overwrite the ByteRange in the buffer
        byte[] byteRangeBytes = byteRange.getBytes(Charsets.ISO_8859_1);
        for (int i = 0; i < byteRangeLength; i++)
        {
            if (i >= byteRangeBytes.length)
            {
                buffer[(int)(byteRangeOffset + i - inLength)] = 0x20; // SPACE
            }
            else
            {
                buffer[(int)(byteRangeOffset + i - inLength)] = byteRangeBytes[i];
            }
        }

        // get only the incremental bytes to be signed (includes /ByteRange but not /Contents)
        byte[] signBuffer = new byte[buffer.length - (int)signatureLength];
        int bufSignatureOffset = (int)(signatureOffset - inLength);
        System.arraycopy(buffer, 0, signBuffer, 0, bufSignatureOffset);
        System.arraycopy(buffer, bufSignatureOffset + (int)signatureLength,
                         signBuffer, bufSignatureOffset, buffer.length - bufSignatureOffset - (int)signatureLength);

        SequenceInputStream signStream = new SequenceInputStream(new RandomAccessInputStream(incrementalInput),
                new ByteArrayInputStream(signBuffer));

        // sign the bytes
        byte[] sign = signatureInterface.sign(signStream);
        String signature = new COSString(sign).toHexString();
        // substract 2 bytes because of the enclosing "<>"
        if (signature.length() > signatureLength - 2)
        {
            throw new IOException("Can't write signature, not enough space");
        }

        // overwrite the signature Contents in the buffer
        byte[] signatureBytes = signature.getBytes(Charsets.ISO_8859_1);
        System.arraycopy(signatureBytes, 0, buffer, bufSignatureOffset + 1, signatureBytes.length);

        // write the data to the incremental output stream
        IOUtils.copy(new RandomAccessInputStream(incrementalInput), incrementalOutput);
        incrementalOutput.write(buffer);
    }

    private void writeXrefRange(long x, long y) throws IOException
    {
        getStandardOutput().write(String.valueOf(x).getBytes(Charsets.ISO_8859_1));
        getStandardOutput().write(SPACE);
        getStandardOutput().write(String.valueOf(y).getBytes(Charsets.ISO_8859_1));
        getStandardOutput().writeEOL();
    }

    private void writeXrefEntry(COSWriterXRefEntry entry) throws IOException
    {
        String offset = formatXrefOffset.format(entry.getOffset());
        String generation = formatXrefGeneration.format(entry.getKey().getGeneration());
        getStandardOutput().write(offset.getBytes(Charsets.ISO_8859_1));
        getStandardOutput().write(SPACE);
        getStandardOutput().write(generation.getBytes(Charsets.ISO_8859_1));
        getStandardOutput().write(SPACE);
        getStandardOutput().write(entry.isFree() ? XREF_FREE : XREF_USED);
        getStandardOutput().writeCRLF();
    }

    /**
     * check the xref entries and write out the ranges.  The format of the
     * returned array is exactly the same as the pdf specification.  See section
     * 7.5.4 of ISO32000-1:2008, example 1 (page 40) for reference.
     * 

* example: 0 1 2 5 6 7 8 10 *

* will create a array with follow ranges *

* 0 3 5 4 10 1 *

* this mean that the element 0 is followed by two other related numbers * that represent a cluster of the size 3. 5 is follow by three other * related numbers and create a cluster of size 4. etc. * * @param xRefEntriesList list with the xRef entries that was written * @return a integer array with the ranges */ protected Long[] getXRefRanges(List xRefEntriesList) { long last = -2; long count = 1; List list = new ArrayList(); for( Object object : xRefEntriesList ) { long nr = (int) ((COSWriterXRefEntry) object).getKey().getNumber(); if (nr == last + 1) { ++count; last = nr; } else if (last == -2) { last = nr; } else { list.add(last - count + 1); list.add(count); last = nr; count = 1; } } // If no new entry is found, we need to write out the last result if(xRefEntriesList.size() > 0) { list.add(last - count + 1); list.add(count); } return list.toArray(new Long[list.size()]); } /** * This will get the object key for the object. * * @param obj The object to get the key for. * * @return The object key for the object. */ private COSObjectKey getObjectKey( COSBase obj ) { COSBase actual = obj; if( actual instanceof COSObject ) { actual = ((COSObject)obj).getObject(); } COSObjectKey key = null; if( actual != null ) { key = objectKeys.get(actual); } if( key == null ) { key = objectKeys.get(obj); } if (key == null) { setNumber(getNumber()+1); key = new COSObjectKey(getNumber(),0); objectKeys.put(obj, key); if( actual != null ) { objectKeys.put(actual, key); } } return key; } @Override public Object visitFromArray( COSArray obj ) throws IOException { int count = 0; getStandardOutput().write(ARRAY_OPEN); for (Iterator i = obj.iterator(); i.hasNext();) { COSBase current = i.next(); if( current instanceof COSDictionary ) { if (current.isDirect()) { visitFromDictionary((COSDictionary)current); } else { addObjectToWrite( current ); writeReference( current ); } } else if( current instanceof COSObject ) { COSBase subValue = ((COSObject)current).getObject(); if (incrementalUpdate || subValue instanceof COSDictionary || subValue == null) { addObjectToWrite( current ); writeReference( current ); } else { subValue.accept( this ); } } else if( current == null ) { COSNull.NULL.accept( this ); } else { current.accept(this); } count++; if (i.hasNext()) { if (count % 10 == 0) { getStandardOutput().writeEOL(); } else { getStandardOutput().write(SPACE); } } } getStandardOutput().write(ARRAY_CLOSE); getStandardOutput().writeEOL(); return null; } @Override public Object visitFromBoolean(COSBoolean obj) throws IOException { obj.writePDF( getStandardOutput() ); return null; } @Override public Object visitFromDictionary(COSDictionary obj) throws IOException { getStandardOutput().write(DICT_OPEN); getStandardOutput().writeEOL(); for (Map.Entry entry : obj.entrySet()) { COSBase value = entry.getValue(); if (value != null) { entry.getKey().accept(this); getStandardOutput().write(SPACE); if( value instanceof COSDictionary ) { COSDictionary dict = (COSDictionary)value; if (!incrementalUpdate) { // write all XObjects as direct objects, this will save some size COSBase item = dict.getItem(COSName.XOBJECT); if (item != null) { item.setDirect(true); } item = dict.getItem(COSName.RESOURCES); if (item != null) { item.setDirect(true); } } if(dict.isDirect()) { // If the object should be written direct, we need // to pass the dictionary to the visitor again. visitFromDictionary(dict); } else { addObjectToWrite( dict ); writeReference( dict ); } } else if( value instanceof COSObject ) { COSBase subValue = ((COSObject)value).getObject(); if (incrementalUpdate || subValue instanceof COSDictionary || subValue == null) { addObjectToWrite( value ); writeReference( value ); } else { subValue.accept( this ); } } else { // If we reach the pdf signature, we need to determinate the position of the // content and byterange if(reachedSignature && COSName.CONTENTS.equals(entry.getKey())) { signatureOffset = getStandardOutput().getPos(); value.accept(this); signatureLength = getStandardOutput().getPos()- signatureOffset; } else if(reachedSignature && COSName.BYTERANGE.equals(entry.getKey())) { byteRangeOffset = getStandardOutput().getPos() + 1; value.accept(this); byteRangeLength = getStandardOutput().getPos() - 1 - byteRangeOffset; reachedSignature = false; } else { value.accept(this); } } getStandardOutput().writeEOL(); } else { //then we won't write anything, there are a couple cases //were the value of an entry in the COSDictionary will //be a dangling reference that points to nothing //so we will just not write out the entry if that is the case } } getStandardOutput().write(DICT_CLOSE); getStandardOutput().writeEOL(); return null; } @Override public Object visitFromDocument(COSDocument doc) throws IOException { if(!incrementalUpdate) { doWriteHeader(doc); } else { // Sometimes the original file will be missing a newline at the end // In order to avoid having %%EOF the first object on the same line // as the %%EOF, we put a newline here. If there's already one at // the end of the file, an extra one won't hurt. PDFBOX-1051 getStandardOutput().writeCRLF(); } doWriteBody(doc); // get the previous trailer COSDictionary trailer = doc.getTrailer(); long hybridPrev = -1; if (trailer != null) { hybridPrev = trailer.getLong(COSName.XREF_STM); } if(incrementalUpdate || doc.isXRefStream()) { doWriteXRefInc(doc, hybridPrev); } else { doWriteXRefTable(); doWriteTrailer(doc); } // write endof getStandardOutput().write(STARTXREF); getStandardOutput().writeEOL(); getStandardOutput().write(String.valueOf(getStartxref()).getBytes(Charsets.ISO_8859_1)); getStandardOutput().writeEOL(); getStandardOutput().write(EOF); getStandardOutput().writeEOL(); if(incrementalUpdate) { if (signatureOffset == 0 || byteRangeOffset == 0) { doWriteIncrement(); } else { doWriteSignature(); } } return null; } @Override public Object visitFromFloat(COSFloat obj) throws IOException { obj.writePDF( getStandardOutput() ); return null; } @Override public Object visitFromInt(COSInteger obj) throws IOException { obj.writePDF( getStandardOutput() ); return null; } @Override public Object visitFromName(COSName obj) throws IOException { obj.writePDF( getStandardOutput() ); return null; } @Override public Object visitFromNull(COSNull obj) throws IOException { obj.writePDF(getStandardOutput()); return null; } /** * visitFromObjRef method comment. * * @param obj The object that is being visited. * * @throws IOException If there is an exception while visiting this object. */ public void writeReference(COSBase obj) throws IOException { COSObjectKey key = getObjectKey(obj); getStandardOutput().write(String.valueOf(key.getNumber()).getBytes(Charsets.ISO_8859_1)); getStandardOutput().write(SPACE); getStandardOutput().write(String.valueOf(key.getGeneration()).getBytes(Charsets.ISO_8859_1)); getStandardOutput().write(SPACE); getStandardOutput().write(REFERENCE); } @Override public Object visitFromStream(COSStream obj) throws IOException { if (willEncrypt) { pdDocument.getEncryption().getSecurityHandler() .encryptStream(obj, currentObjectKey.getNumber(), currentObjectKey.getGeneration()); } InputStream input = null; try { // write the stream content visitFromDictionary(obj); getStandardOutput().write(STREAM); getStandardOutput().writeCRLF(); input = obj.createRawInputStream(); IOUtils.copy(input, getStandardOutput()); getStandardOutput().writeCRLF(); getStandardOutput().write(ENDSTREAM); getStandardOutput().writeEOL(); return null; } finally { if (input != null) { input.close(); } } } @Override public Object visitFromString(COSString obj) throws IOException { if (willEncrypt) { pdDocument.getEncryption().getSecurityHandler().encryptString( obj, currentObjectKey.getNumber(), currentObjectKey.getGeneration()); } COSWriter.writeString(obj, getStandardOutput()); return null; } /** * This will write the pdf document. * * @throws IOException If an error occurs while generating the data. * @param doc The document to write. */ public void write(COSDocument doc) throws IOException { PDDocument pdDoc = new PDDocument( doc ); write( pdDoc ); } /** * This will write the pdf document. * * @param doc The document to write. * * @throws IOException If an error occurs while generating the data. */ public void write(PDDocument doc) throws IOException { write(doc, null); } /** * This will write the pdf document. * * @param doc The document to write. * @param signInterface class to be used for signing * * @throws IOException If an error occurs while generating the data. * @throws IllegalStateException If the document has an encryption dictionary but no protection * policy. */ public void write(PDDocument doc, SignatureInterface signInterface) throws IOException { Long idTime = doc.getDocumentId() == null ? System.currentTimeMillis() : doc.getDocumentId(); pdDocument = doc; signatureInterface = signInterface; if(incrementalUpdate) { prepareIncrement(doc); } // if the document says we should remove encryption, then we shouldn't encrypt if(doc.isAllSecurityToBeRemoved()) { willEncrypt = false; // also need to get rid of the "Encrypt" in the trailer so readers // don't try to decrypt a document which is not encrypted COSDocument cosDoc = doc.getDocument(); COSDictionary trailer = cosDoc.getTrailer(); trailer.removeItem(COSName.ENCRYPT); } else { if (pdDocument.getEncryption() != null) { if (!incrementalUpdate) { SecurityHandler securityHandler = pdDocument.getEncryption().getSecurityHandler(); if (!securityHandler.hasProtectionPolicy()) { throw new IllegalStateException("PDF contains an encryption dictionary, please remove it with " + "setAllSecurityToBeRemoved() or set a protection policy with protect()"); } securityHandler.prepareDocumentForEncryption(pdDocument); } willEncrypt = true; } else { willEncrypt = false; } } COSDocument cosDoc = pdDocument.getDocument(); COSDictionary trailer = cosDoc.getTrailer(); COSArray idArray = (COSArray)trailer.getDictionaryObject( COSName.ID ); boolean missingID = true; // check for an existing documentID if (idArray != null && idArray.size() == 2) { missingID = false; } if( missingID || incrementalUpdate) { MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { // should never happen throw new RuntimeException(e); } // algorithm says to use time/path/size/values in doc to generate the id. // we don't have path or size, so do the best we can md5.update( Long.toString(idTime).getBytes(Charsets.ISO_8859_1) ); COSDictionary info = (COSDictionary)trailer.getDictionaryObject( COSName.INFO ); if( info != null ) { Iterator values = info.getValues().iterator(); while( values.hasNext() ) { md5.update(values.next().toString().getBytes(Charsets.ISO_8859_1)); } } // reuse origin documentID if available as first value COSString firstID = missingID ? new COSString( md5.digest() ) : (COSString)idArray.get(0); // it's ok to use the same ID for the second part if the ID is created for the first time COSString secondID = missingID ? firstID : new COSString( md5.digest() ); idArray = new COSArray(); idArray.add( firstID ); idArray.add( secondID ); trailer.setItem( COSName.ID, idArray ); } cosDoc.accept(this); } /** * This will write the fdf document. * * @param doc The document to write. * * @throws IOException If an error occurs while generating the data. */ public void write(FDFDocument doc) throws IOException { fdfDocument = doc; willEncrypt = false; COSDocument cosDoc = fdfDocument.getDocument(); cosDoc.accept(this); } /** * This will output the given byte getString as a PDF object. * * @param string COSString to be written * @param output The stream to write to. * @throws IOException If there is an error writing to the stream. */ public static void writeString(COSString string, OutputStream output) throws IOException { writeString(string.getBytes(), string.getForceHexForm(), output); } /** * This will output the given text/byte getString as a PDF object. * * @param bytes byte array representation of a string to be written * @param output The stream to write to. * @throws IOException If there is an error writing to the stream. */ public static void writeString(byte[] bytes, OutputStream output) throws IOException { writeString(bytes, false, output); } /** * This will output the given text/byte string as a PDF object. * * @param output The stream to write to. * @throws IOException If there is an error writing to the stream. */ private static void writeString(byte[] bytes, boolean forceHex, OutputStream output) throws IOException { // check for non-ASCII characters boolean isASCII = true; for (byte b : bytes) { // if the byte is negative then it is an eight bit byte and is outside the ASCII range if (b < 0) { isASCII = false; break; } // PDFBOX-3107 EOL markers within a string are troublesome if (b == 0x0d || b == 0x0a) { isASCII = false; break; } } if (isASCII && !forceHex) { // write ASCII string output.write('('); for (byte b : bytes) { switch (b) { case '(': case ')': case '\\': output.write('\\'); output.write(b); break; default: output.write(b); } } output.write(')'); } else { // write hex string output.write('<'); for (byte b : bytes) { output.write(Hex.getBytes(b)); } output.write('>'); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy