/*
* 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 com.tom_roush.pdfbox.cos;
import com.tom_roush.pdfbox.filter.Filter;
import com.tom_roush.pdfbox.filter.FilterFactory;
import com.tom_roush.pdfbox.io.IOUtils;
import com.tom_roush.pdfbox.io.RandomAccess;
import com.tom_roush.pdfbox.io.RandomAccessBuffer;
import com.tom_roush.pdfbox.io.RandomAccessInputStream;
import com.tom_roush.pdfbox.io.RandomAccessOutputStream;
import com.tom_roush.pdfbox.io.ScratchFile;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* This class represents a stream object in a PDF document.
*
* @author Ben Litchfield
*/
public class COSStream extends COSDictionary implements Closeable
{
private RandomAccess randomAccess; // backing store, in-memory or on-disk
private final ScratchFile scratchFile; // used as a temp buffer during decoding
private boolean isWriting; // true if there's an open OutputStream
/**
* Creates a new stream with an empty dictionary.
*/
public COSStream()
{
this.randomAccess = new RandomAccessBuffer();
this.scratchFile = null;
}
/**
* Creates a new stream with an empty dictionary. Data is stored in the given scratch file.
*
* @param scratchFile Scratch file for writing stream data.
*/
public COSStream(ScratchFile scratchFile)
{
super();
this.randomAccess = createRandomAccess(scratchFile);
this.scratchFile = scratchFile;
}
/**
* Creates a buffer for writing stream data, either in-memory or on-disk.
*/
private RandomAccess createRandomAccess(ScratchFile scratchFile)
{
if (scratchFile != null)
{
try
{
return scratchFile.createBuffer();
}
catch (IOException e)
{
// user can't recover from this exception anyway
throw new RuntimeException(e);
}
}
else
{
return new RandomAccessBuffer();
}
}
/**
* Throws if the random access backing store has been closed. Helpful for catching cases where
* a user tries to use a COSStream which has outlived its COSDocument.
*/
private void checkClosed() throws IOException
{
if (randomAccess.isClosed())
{
throw new IOException("COSStream has been closed and cannot be read. " +
"Perhaps its enclosing PDDocument has been closed?");
}
}
/**
* This will get the stream with all of the filters applied.
*
* @return the bytes of the physical (encoded) stream
* @throws IOException when encoding causes an exception
* @deprecated Use {@link #createInputStream()} instead.
*/
@Deprecated
public InputStream getFilteredStream() throws IOException
{
return createRawInputStream();
}
/**
* Returns a new InputStream which reads the decoded stream data.
*
* @return InputStream containing decoded stream data.
* @throws IOException If the stream could not be read.
*/
public InputStream createRawInputStream() throws IOException
{
checkClosed();
if (isWriting)
{
throw new IllegalStateException("Cannot read while there is an open stream writer");
}
return new RandomAccessInputStream(randomAccess);
}
/**
* This will get the logical content stream with none of the filters.
*
* @return the bytes of the logical (decoded) stream
* @throws IOException when decoding causes an exception
* @deprecated Use {@link #createRawInputStream()} instead.
*/
@Deprecated
public InputStream getUnfilteredStream() throws IOException
{
return createInputStream();
}
/**
* Returns a new InputStream which reads the encoded PDF stream data. Experts only!
*
* @return InputStream containing raw, encoded PDF stream data.
* @throws IOException If the stream could not be read.
*/
public COSInputStream createInputStream() throws IOException
{
checkClosed();
if (isWriting)
{
throw new IllegalStateException("Cannot read while there is an open stream writer");
}
InputStream input = new RandomAccessInputStream(randomAccess);
return COSInputStream.create(getFilterList(), this, input, scratchFile);
}
/**
* This will create an output stream that can be written to.
*
* @return An output stream which raw data bytes should be written to.
* @throws IOException If there is an error creating the stream.
* @deprecated Use {@link #createOutputStream()} instead.
*/
@Deprecated
public OutputStream createUnfilteredStream() throws IOException
{
return createOutputStream();
}
/**
* Returns a new OutputStream for writing stream data, using the current filters.
*
* @return OutputStream for un-encoded stream data.
* @throws IOException If the output stream could not be created.
*/
public OutputStream createOutputStream() throws IOException
{
return createOutputStream(null);
}
/**
* Returns a new OutputStream for writing stream data, using and the given filters.
*
* @param filters COSArray or COSName of filters to be used.
* @return OutputStream for un-encoded stream data.
* @throws IOException If the output stream could not be created.
*/
public OutputStream createOutputStream(COSBase filters) throws IOException
{
checkClosed();
if (isWriting)
{
throw new IllegalStateException("Cannot have more than one open stream writer.");
}
// apply filters, if any
if (filters != null)
{
setItem(COSName.FILTER, filters);
}
randomAccess = createRandomAccess(scratchFile); // discards old data
OutputStream randomOut = new RandomAccessOutputStream(randomAccess);
OutputStream cosOut = new COSOutputStream(getFilterList(), this, randomOut, scratchFile);
isWriting = true;
return new FilterOutputStream(cosOut)
{
@Override
public void close() throws IOException
{
super.close();
setInt(COSName.LENGTH, (int) randomAccess.length());
isWriting = false;
}
};
}
/**
* This will create a new stream for which filtered byte should be
* written to. You probably don't want this but want to use the
* createUnfilteredStream, which is used to write raw bytes to.
*
* @return A stream that can be written to.
* @throws IOException If there is an error creating the stream.
* @deprecated Use {@link #createRawOutputStream()} instead.
*/
@Deprecated
public OutputStream createFilteredStream() throws IOException
{
return createRawOutputStream();
}
/**
* Returns a new OutputStream for writing encoded PDF data. Experts only!
*
* @return OutputStream for raw PDF stream data.
* @throws IOException If the output stream could not be created.
*/
public OutputStream createRawOutputStream() throws IOException
{
checkClosed();
if (isWriting)
{
throw new IllegalStateException("Cannot have more than one open stream writer.");
}
randomAccess = createRandomAccess(scratchFile); // discards old data
OutputStream out = new RandomAccessOutputStream(randomAccess);
isWriting = true;
return new FilterOutputStream(out)
{
@Override
public void close() throws IOException
{
super.close();
setInt(COSName.LENGTH, (int) randomAccess.length());
isWriting = false;
}
};
}
/**
* Returns the list of filters.
*/
private List<Filter> getFilterList() throws IOException
{
List<Filter> filterList = new ArrayList<Filter>();
COSBase filters = getFilters();
if (filters instanceof COSName)
{
filterList.add(FilterFactory.INSTANCE.getFilter((COSName) filters));
}
else if (filters instanceof COSArray)
{
COSArray filterArray = (COSArray) filters;
for (int i = 0; i < filterArray.size(); i++)
{
COSName filterName = (COSName) filterArray.get(i);
filterList.add(FilterFactory.INSTANCE.getFilter(filterName));
}
}
return filterList;
}
/**
* Returns the length of the encoded stream.
*
* @return length in bytes
*/
public long getLength()
{
if (isWriting)
{
throw new IllegalStateException("There is an open OutputStream associated with " +
"this COSStream. It must be closed before querying" +
"length of this COSStream.");
}
return getInt(COSName.LENGTH, 0);
}
/**
* This will return the filters to apply to the byte stream.
* The method will return
* - null if no filters are to be applied
* - a COSName if one filter is to be applied
* - a COSArray containing COSNames if multiple filters are to be applied
*
* @return the COSBase object representing the filters
*/
public COSBase getFilters()
{
return getDictionaryObject(COSName.FILTER);
}
/**
* Sets the filters to be applied when encoding or decoding the stream.
*
* @param filters The filters to set on this stream.
* @throws IOException If there is an error clearing the old filters.
* @deprecated Use {@link #createOutputStream(COSBase)} instead.
*/
@Deprecated
public void setFilters(COSBase filters) throws IOException
{
setItem(COSName.FILTER, filters);
}
/**
* Returns the contents of the stream as a text string.
*
* @deprecated Use {@link #toTextString()} instead.
*/
@Deprecated
public String getString()
{
return toTextString();
}
/**
* Returns the contents of the stream as a PDF "text string".
*/
public String toTextString()
{
ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream input = null;
try
{
input = createInputStream();
IOUtils.copy(input, out);
}
catch (IOException e)
{
return "";
}
finally
{
IOUtils.closeQuietly(input);
}
COSString string = new COSString(out.toByteArray());
return string.getString();
}
@Override
public Object accept(ICOSVisitor visitor) throws IOException
{
return visitor.visitFromStream(this);
}
@Override
public void close() throws IOException
{
// marks the scratch file pages as free
randomAccess.close();
}
}