blob: 277cc6885ae95b2062e6018789877f9b3a94a2b9 [file] [log] [blame]
/*
* 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.cos;
import java.io.ByteArrayInputStream;
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.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.filter.DecodeOptions;
import org.apache.pdfbox.filter.Filter;
import org.apache.pdfbox.filter.FilterFactory;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.io.RandomAccess;
import org.apache.pdfbox.io.RandomAccessInputStream;
import org.apache.pdfbox.io.RandomAccessOutputStream;
import org.apache.pdfbox.io.RandomAccessRead;
import org.apache.pdfbox.io.RandomAccessReadBuffer;
import org.apache.pdfbox.io.RandomAccessReadView;
import org.apache.pdfbox.io.ScratchFile;
/**
* This class represents a stream object in a PDF document.
*
* @author Ben Litchfield
*/
public class COSStream extends COSDictionary implements Closeable
{
// backing store, in-memory or on-disk
private RandomAccess randomAccess;
// used as a temp buffer during decoding
private final ScratchFile scratchFile;
// true if there's an open OutputStream
private boolean isWriting;
// random access view to be read from
private RandomAccessReadView randomAccessReadView;
private static final Log LOG = LogFactory.getLog(COSStream.class);
/**
* Creates a new stream with an empty dictionary.
* <p>
* Try to avoid using this constructor because it creates a new scratch file in memory. Instead,
* use {@link COSDocument#createCOSStream() document.getDocument().createCOSStream()} which will
* use the existing scratch file (in memory or in temp file) of the document.
* </p>
*/
public COSStream()
{
this(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)
{
setInt(COSName.LENGTH, 0);
this.scratchFile = scratchFile != null ? scratchFile : ScratchFile.getMainMemoryOnlyInstance();
}
/**
* Creates a new stream with an empty dictionary. Data is read from the given random accessview. Written data is stored
* in the given scratch file.
*
* @param scratchFile Scratch file for writing stream data.
* @throws IOException if the length of the random access view isn't available
*/
public COSStream(ScratchFile scratchFile, RandomAccessReadView randomAccessReadView)
throws IOException
{
this(scratchFile);
this.randomAccessReadView = randomAccessReadView;
setInt(COSName.LENGTH, (int) randomAccessReadView.length());
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (o == this)
{
return true;
}
if (!(o instanceof COSStream))
{
return false;
}
COSStream toBeCompared = (COSStream) o;
if (toBeCompared.size() != size())
{
return false;
}
// compare dictionary content
Iterator<Entry<COSName, COSBase>> iter = entrySet().iterator();
while (iter.hasNext())
{
Entry<COSName, COSBase> entry = iter.next();
COSName key = entry.getKey();
COSBase value = entry.getValue();
if (!toBeCompared.containsKey(key))
{
return false;
}
else if (value == null)
{
if (toBeCompared.getItem(key) != null)
{
return false;
}
}
else if (!value.equals(toBeCompared.getItem(key)))
{
return false;
}
}
// compare stream content
return toBeCompared.toTextString().equals(toTextString());
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hash(items, randomAccess, scratchFile, isWriting);
}
/**
* 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 != null && randomAccess.isClosed())
{
throw new IOException("COSStream has been closed and cannot be read. " +
"Perhaps its enclosing PDDocument has been closed?");
// Tip for debugging: look at the destination file with an editor, you'll see an
// incomplete stream at the bottom.
}
}
/**
* 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 InputStream createRawInputStream() throws IOException
{
checkClosed();
if (isWriting)
{
throw new IllegalStateException("Cannot read while there is an open stream writer");
}
if (randomAccess == null)
{
if (randomAccessReadView != null)
{
randomAccessReadView.seek(0);
return new RandomAccessInputStream(randomAccessReadView);
}
else
{
throw new IOException(
"Create InputStream called without data being written before to stream.");
}
}
else
{
return new RandomAccessInputStream(randomAccess);
}
}
/**
* 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 COSInputStream createInputStream() throws IOException
{
return createInputStream(DecodeOptions.DEFAULT);
}
public COSInputStream createInputStream(DecodeOptions options) throws IOException
{
InputStream input = createRawInputStream();
return COSInputStream.create(getFilterList(), this, input, options);
}
/**
* Returns a new RandomAccessRead which reads the decoded stream data.
*
* @return RandomAccessRead containing decoded stream data.
* @throws IOException If the stream could not be read.
*/
public RandomAccessRead createView() throws IOException
{
List<Filter> filterList = getFilterList();
if (filterList.isEmpty())
{
if (randomAccess == null && randomAccessReadView != null)
{
return new RandomAccessReadView(randomAccessReadView, 0,
randomAccessReadView.length());
}
else
{
return new RandomAccessReadBuffer(createRawInputStream());
}
}
else
{
Set<Filter> filterSet = new HashSet<>(filterList);
if (filterSet.size() != filterList.size())
{
throw new IOException("Duplicate");
}
InputStream input = createRawInputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream();
// apply filters
for (int i = 0; i < filterList.size(); i++)
{
if (i > 0)
{
input = new ByteArrayInputStream(output.toByteArray());
output.reset();
}
filterList.get(i).decode(input, output, this, i, DecodeOptions.DEFAULT);
}
return new RandomAccessReadBuffer(output.toByteArray());
}
}
/**
* 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);
}
if (randomAccess != null)
randomAccess.clear();
else
randomAccess = scratchFile.createBuffer();
OutputStream randomOut = new RandomAccessOutputStream(randomAccess);
OutputStream cosOut = new COSOutputStream(getFilterList(), this, randomOut, scratchFile);
isWriting = true;
return new FilterOutputStream(cosOut)
{
@Override
public void write(byte[] b, int off, int len) throws IOException
{
this.out.write(b, off, len);
}
@Override
public void close() throws IOException
{
super.close();
setInt(COSName.LENGTH, (int)randomAccess.length());
isWriting = false;
}
};
}
/**
* 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.");
}
if (randomAccess != null)
randomAccess.clear();
else
randomAccess = scratchFile.createBuffer();
OutputStream out = new RandomAccessOutputStream(randomAccess);
isWriting = true;
return new FilterOutputStream(out)
{
@Override
public void write(byte[] b, int off, int len) throws IOException
{
this.out.write(b, off, len);
}
@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<>();
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
* <ul>
* <li>null if no filters are to be applied
* <li>a COSName if one filter is to be applied
* <li>a COSArray containing COSNames if multiple filters are to be applied
* </ul>
*
* @return the COSBase object representing the filters
*/
public COSBase getFilters()
{
return getDictionaryObject(COSName.FILTER);
}
/**
* 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)
{
LOG.debug("An exception occurred trying to get the content - returning empty string instead", 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
if (randomAccess != null)
{
randomAccess.close();
randomAccess = null;
}
if (randomAccessReadView != null)
{
randomAccessReadView.close();
randomAccessReadView = null;
}
}
/**
* Indicates wether the stream contains any data or not.
*
* @return true if the stream contains any data
*/
public boolean hasData()
{
return randomAccess != null || randomAccessReadView != null;
}
}