/*
* Created on 28 gen 2016
* Copyright 2015 by Andrea Vacondio (andrea.vacondio@gmail.com).
* This file is part of Sejda.
*
* Sejda is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Sejda 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 Sejda. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sejda.impl.sambox.component;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static org.sejda.util.RequireUtils.requireIOCondition;
import static org.sejda.util.RequireUtils.requireNotNullArg;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.GregorianCalendar;
import java.util.zip.DeflaterInputStream;
import org.sejda.io.SeekableSource;
import org.sejda.model.exception.SejdaRuntimeException;
import org.sejda.model.exception.TaskIOException;
import org.sejda.model.input.FileSource;
import org.sejda.model.input.Source;
import org.sejda.model.input.SourceDispatcher;
import org.sejda.model.input.StreamSource;
import org.sejda.sambox.cos.COSBase;
import org.sejda.sambox.cos.COSDictionary;
import org.sejda.sambox.cos.COSName;
import org.sejda.sambox.cos.COSStream;
import org.sejda.sambox.cos.IndirectCOSObjectIdentifier;
import org.sejda.sambox.pdmodel.graphics.color.PDColorSpace;
import org.sejda.util.IOUtils;
/**
* A read only, filtered, encryptable, indirect reference length {@link COSStream} whose purpose is to be read by the PDF writer during the write process. This can allow to create
* streams from File input streams and predefine the expected dictionary without having to read anything into memory.
*
* @author Andrea Vacondio
*
*/
public class ReadOnlyFilteredCOSStream extends COSStream {
private InputStreamSupplier<InputStream> stream;
private long length;
private COSDictionary wrapped;
ReadOnlyFilteredCOSStream(COSDictionary existingDictionary, InputStream stream, long length) {
this(existingDictionary, () -> stream, length);
requireNotNullArg(stream, "input stream cannot be null");
}
public ReadOnlyFilteredCOSStream(COSDictionary existingDictionary, InputStreamSupplier<InputStream> stream,
long length) {
super(ofNullable(existingDictionary)
.orElseThrow(() -> new IllegalArgumentException("wrapped dictionary cannot be null")));
requireNotNullArg(stream, "input stream provider cannot be null");
this.stream = stream;
this.length = length;
this.wrapped = existingDictionary;
}
@Override
protected InputStream doGetFilteredStream() throws IOException {
return stream.get();
}
@Override
public long getFilteredLength() throws IOException {
requireIOCondition(length >= 0, "Filtered length cannot be requested");
return length;
}
@Override
public long getUnfilteredLength() throws IOException {
throw new IOException("Unfiltered length cannot be requested");
}
@Override
public InputStream getUnfilteredStream() throws IOException {
throw new IOException("getUnfilteredStream cannot be requested");
}
@Override
public SeekableSource getUnfilteredSource() throws IOException {
throw new IOException("getUnfilteredSource cannot be requested");
}
@Override
public OutputStream createFilteredStream() {
throw new SejdaRuntimeException("createFilteredStream cannot be called on this stream");
}
@Override
public OutputStream createFilteredStream(COSBase filters) {
throw new SejdaRuntimeException("createFilteredStream cannot be called on this stream");
}
@Override
public void setFilters(COSBase filters) {
throw new SejdaRuntimeException("setFilters cannot be called on this stream");
}
@Override
public void addCompression() {
// do nothing, it's already supposed to be compressed
}
@Override
public boolean encryptable() {
return true;
}
@Override
public void encryptable(boolean encryptable) {
// do nothing, it can be encrypted
}
@Override
public OutputStream createUnfilteredStream() {
throw new SejdaRuntimeException("createUnfilteredStream cannot be called on this stream");
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean indirectLength() {
return true;
}
@Override
public void indirectLength(boolean indirectLength) {
// do nothing, it's always written as indirect
}
@Override
public IndirectCOSObjectIdentifier id() {
return wrapped.id();
}
@Override
public void idIfAbsent(IndirectCOSObjectIdentifier id) {
wrapped.idIfAbsent(id);
}
@Override
public void close() throws IOException {
IOUtils.closeQuietly(stream.get());
}
/**
* a {@link ReadOnlyFilteredCOSStream} from an existing {@link COSStream}
*
* @param existing
* @return
* @throws IOException
*/
public static ReadOnlyFilteredCOSStream readOnly(COSStream existing) throws IOException {
requireNotNullArg(existing, "input stream cannot be null");
// let's make sure we get the unencrypted and filtered
existing.setEncryptor(null);
return new ReadOnlyFilteredCOSStream(existing, existing.getFilteredStream(), existing.getFilteredLength());
}
/**
* a {@link ReadOnlyFilteredCOSStream} that represents an xobject JPEG image
*
* @param imageFile
* the image file
* @param width
* @param height
* @param bitsPerComponent
* @param colorSpace
* @return
* @throws FileNotFoundException
*/
public static ReadOnlyFilteredCOSStream readOnlyJpegImage(File imageFile, int width, int height,
int bitsPerComponent, PDColorSpace colorSpace) throws FileNotFoundException {
requireNotNullArg(imageFile, "input file cannot be null");
requireNotNullArg(colorSpace, "color space cannot be null");
COSDictionary dictionary = new COSDictionary();
dictionary.setItem(COSName.TYPE, COSName.XOBJECT);
dictionary.setItem(COSName.SUBTYPE, COSName.IMAGE);
dictionary.setItem(COSName.FILTER, COSName.DCT_DECODE);
dictionary.setInt(COSName.BITS_PER_COMPONENT, bitsPerComponent);
dictionary.setInt(COSName.HEIGHT, height);
dictionary.setInt(COSName.WIDTH, width);
of(colorSpace).map(PDColorSpace::getCOSObject).ifPresent(cs -> dictionary.setItem(COSName.COLORSPACE, cs));
return new ReadOnlyFilteredCOSStream(dictionary, new FileInputStream(imageFile), imageFile.length());
}
/**
* a {@link ReadOnlyFilteredCOSStream} representing an embedded file stream
*
* @param source
* @return
* @throws TaskIOException
*/
public static final ReadOnlyFilteredCOSStream readOnlyEmbeddedFile(Source<?> source) throws TaskIOException {
COSDictionary dictionary = new COSDictionary();
dictionary.setItem(COSName.FILTER, COSName.FLATE_DECODE);
return source.dispatch(new SourceDispatcher<ReadOnlyFilteredCOSStream>() {
@Override
public ReadOnlyFilteredCOSStream dispatch(FileSource source) throws TaskIOException {
try {
ReadOnlyFilteredCOSStream retVal = new ReadOnlyFilteredCOSStream(dictionary,
new DeflaterInputStream(new FileInputStream(source.getSource())), -1);
retVal.setEmbeddedInt(COSName.PARAMS.getName(), COSName.SIZE, source.getSource().length());
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTimeInMillis(source.getSource().lastModified());
retVal.setEmbeddedDate(COSName.PARAMS.getName(), COSName.MOD_DATE, calendar);
return retVal;
} catch (FileNotFoundException e) {
throw new TaskIOException(e);
}
}
@Override
public ReadOnlyFilteredCOSStream dispatch(StreamSource source) {
return new ReadOnlyFilteredCOSStream(dictionary, new DeflaterInputStream(source.getSource()), -1);
}
});
}
@FunctionalInterface
public interface InputStreamSupplier<T extends InputStream> {
T get() throws IOException;
}
}