/*
* 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.filter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipException;
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.COSDictionary;
import org.apache.pdfbox.cos.COSName;
/**
* This is the used for the FlateDecode filter.
*
* @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
* @author Marcel Kammer
* @version $Revision: 1.12 $
*/
public class FlateFilter implements Filter {
/**
* Log instance.
*/
private static final Log LOG = LogFactory.getLog(FlateFilter.class);
private static final int BUFFER_SIZE = 16348;
/**
* {@inheritDoc}
*/
public void decode(final InputStream compressedData, final OutputStream result, final COSDictionary options, final int filterIndex) throws IOException {
final COSBase baseObj = options.getDictionaryObject(COSName.DECODE_PARMS, COSName.DP);
COSDictionary dict = null;
if (baseObj instanceof COSDictionary) {
dict = (COSDictionary) baseObj;
}
else if (baseObj instanceof COSArray) {
final COSArray paramArray = (COSArray) baseObj;
if (filterIndex < paramArray.size()) {
dict = (COSDictionary) paramArray.getObject(filterIndex);
}
}
else if (baseObj != null) {
throw new IOException("Error: Expected COSArray or COSDictionary and not " + baseObj.getClass().getName());
}
int predictor = -1;
int colors = -1;
int bitsPerPixel = -1;
int columns = -1;
InflaterInputStream decompressor = null;
ByteArrayInputStream bais = null;
ByteArrayOutputStream baos = null;
if (dict != null) {
predictor = dict.getInt(COSName.PREDICTOR);
if (predictor > 1) {
colors = dict.getInt(COSName.COLORS);
bitsPerPixel = dict.getInt(COSName.BITS_PER_COMPONENT);
columns = dict.getInt(COSName.COLUMNS);
}
}
try {
// Decompress data to temporary ByteArrayOutputStream
decompressor = new InflaterInputStream(compressedData);
int amountRead;
final int mayRead = compressedData.available();
if (mayRead > 0) {
final byte[] buffer = new byte[Math.min(mayRead, BUFFER_SIZE)];
// Decode data using given predictor
if (predictor == -1 || predictor == 1) {
try {
// decoding not needed
while ((amountRead = decompressor.read(buffer, 0, Math.min(mayRead, BUFFER_SIZE))) != -1) {
result.write(buffer, 0, amountRead);
}
} catch (final OutOfMemoryError exception) {
// if the stream is corrupt an OutOfMemoryError may occur
LOG.error("Stop reading corrupt stream", exception);
} catch (final ZipException exception) {
// if the stream is corrupt an OutOfMemoryError may occur
LOG.error("Stop reading corrupt stream", exception);
} catch (final EOFException exception) {
// if the stream is corrupt an OutOfMemoryError may occur
LOG.error("Stop reading corrupt stream", exception);
}
}
else {
/*
* Reverting back to default values
*/
if (colors == -1) {
colors = 1;
}
if (bitsPerPixel == -1) {
bitsPerPixel = 8;
}
if (columns == -1) {
columns = 1;
}
baos = new ByteArrayOutputStream();
while ((amountRead = decompressor.read(buffer, 0, Math.min(mayRead, BUFFER_SIZE))) != -1) {
baos.write(buffer, 0, amountRead);
}
baos.flush();
// Copy data to ByteArrayInputStream for reading
bais = new ByteArrayInputStream(baos.toByteArray());
baos.close();
baos = null;
final byte[] decodedData = decodePredictor(predictor, colors, bitsPerPixel, columns, bais);
bais.close();
bais = null;
result.write(decodedData);
}
}
result.flush();
} finally {
if (decompressor != null) {
decompressor.close();
}
if (bais != null) {
bais.close();
}
if (baos != null) {
baos.close();
}
}
}
private byte[] decodePredictor(final int predictor, final int colors, final int bitsPerComponent, final int columns, final InputStream data) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final byte[] buffer = new byte[2048];
if (predictor == 1) {
// No prediction
int i = 0;
while ((i = data.read(buffer)) != -1) {
baos.write(buffer, 0, i);
}
}
else {
// calculate sizes
final int bitsPerPixel = colors * bitsPerComponent;
final int bytesPerPixel = (bitsPerPixel + 7) / 8;
final int rowlength = (columns * bitsPerPixel + 7) / 8;
final byte[] actline = new byte[rowlength];
// Initialize lastline with Zeros according to PNG-specification
byte[] lastline = new byte[rowlength];
boolean done = false;
int linepredictor = predictor;
while (!done && data.available() > 0) {
// test for PNG predictor; each value >= 10 (not only 15) indicates usage of PNG predictor
if (predictor >= 10) {
// PNG predictor; each row starts with predictor type (0, 1, 2, 3, 4)
linepredictor = data.read();// read per line predictor
if (linepredictor == -1) {
done = true;// reached EOF
break;
}
else {
linepredictor += 10; // add 10 to tread value 0 as 10, 1 as 11, ...
}
}
// read line
int i = 0;
int offset = 0;
while (offset < rowlength && (i = data.read(actline, offset, rowlength - offset)) != -1) {
offset += i;
}
// Do prediction as specified in PNG-Specification 1.2
switch (linepredictor) {
case 2:// PRED TIFF SUB
/**
* @TODO decode tiff with bitsPerComponent != 8; e.g. for 4 bpc each nibble must be subtracted separately
*/
if (bitsPerComponent != 8) {
throw new IOException("TIFF-Predictor with " + bitsPerComponent + " bits per component not supported");
}
// for 8 bits per component it is the same algorithm as PRED SUB of PNG format
for (int p = 0; p < rowlength; p++) {
final int sub = actline[p] & 0xff;
final int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;
actline[p] = (byte) (sub + left);
}
break;
case 10:// PRED NONE
// do nothing
break;
case 11:// PRED SUB
for (int p = 0; p < rowlength; p++) {
final int sub = actline[p];
final int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] : 0;
actline[p] = (byte) (sub + left);
}
break;
case 12:// PRED UP
for (int p = 0; p < rowlength; p++) {
final int up = actline[p] & 0xff;
final int prior = lastline[p] & 0xff;
actline[p] = (byte) (up + prior & 0xff);
}
break;
case 13:// PRED AVG
for (int p = 0; p < rowlength; p++) {
final int avg = actline[p] & 0xff;
final int left = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;
final int up = lastline[p] & 0xff;
actline[p] = (byte) (avg + (int) Math.floor((left + up) / 2) & 0xff);
}
break;
case 14:// PRED PAETH
for (int p = 0; p < rowlength; p++) {
final int paeth = actline[p] & 0xff;
final int a = p - bytesPerPixel >= 0 ? actline[p - bytesPerPixel] & 0xff : 0;// left
final int b = lastline[p] & 0xff;// upper
final int c = p - bytesPerPixel >= 0 ? lastline[p - bytesPerPixel] & 0xff : 0;// upperleft
final int value = a + b - c;
final int absa = Math.abs(value - a);
final int absb = Math.abs(value - b);
final int absc = Math.abs(value - c);
if (absa <= absb && absa <= absc) {
actline[p] = (byte) (paeth + a & 0xff);
}
else if (absb <= absc) {
actline[p] = (byte) (paeth + b & 0xff);
}
else {
actline[p] = (byte) (paeth + c & 0xff);
}
}
break;
default:
break;
}
lastline = actline.clone();
baos.write(actline, 0, actline.length);
}
}
return baos.toByteArray();
}
/**
* {@inheritDoc}
*/
public void encode(final InputStream rawData, final OutputStream result, final COSDictionary options, final int filterIndex) throws IOException {
final DeflaterOutputStream out = new DeflaterOutputStream(result);
int amountRead = 0;
final int mayRead = rawData.available();
if (mayRead > 0) {
final byte[] buffer = new byte[Math.min(mayRead, BUFFER_SIZE)];
while ((amountRead = rawData.read(buffer, 0, Math.min(mayRead, BUFFER_SIZE))) != -1) {
out.write(buffer, 0, amountRead);
}
}
out.close();
result.flush();
}
}