org.apache.pdfbox.pdmodel.common.function.PDFunctionType0 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pdfbox Show documentation
Show all versions of pdfbox Show documentation
The Apache PDFBox library is an open source Java tool for working with PDF documents.
/*
* 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.pdmodel.common.function;
import java.io.IOException;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
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.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.common.PDRange;
/**
* This class represents a type 0 function in a PDF document.
*
* @author Ben Litchfield
* @author Tilman Hausherr
*
*/
public class PDFunctionType0 extends PDFunction
{
/**
* Log instance.
*/
private static final Log LOG = LogFactory.getLog(PDFunctionType0.class);
/**
* An array of 2 x m numbers specifying the linear mapping of input values
* into the domain of the function's sample table. Default value: [ 0 (Size0
* - 1) 0 (Size1 - 1) ...].
*/
private COSArray encode = null;
/**
* An array of 2 x n numbers specifying the linear mapping of sample values
* into the range appropriate for the function's output values. Default
* value: same as the value of Range
*/
private COSArray decode = null;
/**
* An array of m positive integers specifying the number of samples in each
* input dimension of the sample table.
*/
private COSArray size = null;
/**
* The samples of the function.
*/
private int[][] samples = null;
/**
* Constructor.
*
* @param function The function.
*/
public PDFunctionType0(COSBase function)
{
super(function);
}
/**
* {@inheritDoc}
*/
@Override
public int getFunctionType()
{
return 0;
}
/**
* The "Size" entry, which is the number of samples in each input dimension
* of the sample table.
*
* @return A List of java.lang.Integer objects.
*/
public COSArray getSize()
{
if (size == null)
{
size = (COSArray) getDictionary().getDictionaryObject(COSName.SIZE);
}
return size;
}
/**
* Get all sample values of this function.
*
* @return an array with all samples.
*/
public int[][] getSamples()
{
if (samples == null)
{
int arraySize = 1;
int numberOfInputValues = getNumberOfInputParameters();
int numberOfOutputValues = getNumberOfOutputParameters();
COSArray sizes = getSize();
for (int i = 0; i < numberOfInputValues; i++)
{
arraySize *= sizes.getInt(i);
}
samples = new int[arraySize][numberOfOutputValues];
int bitsPerSample = getBitsPerSample();
int index = 0;
try
{
// PDF spec 1.7 p.171:
// Each sample value is represented as a sequence of BitsPerSample bits.
// Successive values are adjacent in the bit stream;
// there is no padding at byte boundaries.
ImageInputStream mciis = new MemoryCacheImageInputStream(getPDStream().createInputStream());
for (int i = 0; i < arraySize; i++)
{
for (int k = 0; k < numberOfOutputValues; k++)
{
// TODO will this cast work properly for 32 bitsPerSample or should we use long[]?
samples[index][k] = (int) mciis.readBits(bitsPerSample);
}
index++;
}
mciis.close();
}
catch (IOException exception)
{
LOG.error("IOException while reading the sample values of this function.", exception);
}
}
return samples;
}
/**
* Get the number of bits that the output value will take up.
*
* Valid values are 1,2,4,8,12,16,24,32.
*
* @return Number of bits for each output value.
*/
public int getBitsPerSample()
{
return getDictionary().getInt(COSName.BITS_PER_SAMPLE);
}
/**
* Get the order of interpolation between samples. Valid values are 1 and 3,
* specifying linear and cubic spline interpolation, respectively. Default
* is 1. See p.170 in PDF spec 1.7.
*
* @return order of interpolation.
*/
public int getOrder()
{
return getDictionary().getInt(COSName.ORDER, 1);
}
/**
* Set the number of bits that the output value will take up. Valid values
* are 1,2,4,8,12,16,24,32.
*
* @param bps The number of bits for each output value.
*/
public void setBitsPerSample(int bps)
{
getDictionary().setInt(COSName.BITS_PER_SAMPLE, bps);
}
/**
* Returns all encode values as COSArray.
*
* @return the encode array.
*/
private COSArray getEncodeValues()
{
if (encode == null)
{
encode = (COSArray) getDictionary().getDictionaryObject(COSName.ENCODE);
// the default value is [0 (size[0]-1) 0 (size[1]-1) ...]
if (encode == null)
{
encode = new COSArray();
COSArray sizeValues = getSize();
int sizeValuesSize = sizeValues.size();
for (int i = 0; i < sizeValuesSize; i++)
{
encode.add(COSInteger.ZERO);
encode.add(COSInteger.get(sizeValues.getInt(i) - 1));
}
}
}
return encode;
}
/**
* Returns all decode values as COSArray.
*
* @return the decode array.
*/
private COSArray getDecodeValues()
{
if (decode == null)
{
decode = (COSArray) getDictionary().getDictionaryObject(COSName.DECODE);
// if decode is null, the default values are the range values
if (decode == null)
{
decode = getRangeValues();
}
}
return decode;
}
/**
* Get the encode for the input parameter.
*
* @param paramNum The function parameter number.
*
* @return The encode parameter range or null if none is set.
*/
public PDRange getEncodeForParameter(int paramNum)
{
PDRange retval = null;
COSArray encodeValues = getEncodeValues();
if (encodeValues != null && encodeValues.size() >= paramNum * 2 + 1)
{
retval = new PDRange(encodeValues, paramNum);
}
return retval;
}
/**
* This will set the encode values.
*
* @param encodeValues The new encode values.
*/
public void setEncodeValues(COSArray encodeValues)
{
encode = encodeValues;
getDictionary().setItem(COSName.ENCODE, encodeValues);
}
/**
* Get the decode for the input parameter.
*
* @param paramNum The function parameter number.
*
* @return The decode parameter range or null if none is set.
*/
public PDRange getDecodeForParameter(int paramNum)
{
PDRange retval = null;
COSArray decodeValues = getDecodeValues();
if (decodeValues != null && decodeValues.size() >= paramNum * 2 + 1)
{
retval = new PDRange(decodeValues, paramNum);
}
return retval;
}
/**
* This will set the decode values.
*
* @param decodeValues The new decode values.
*/
public void setDecodeValues(COSArray decodeValues)
{
decode = decodeValues;
getDictionary().setItem(COSName.DECODE, decodeValues);
}
/**
* calculate array index (structure described in p.171 PDF spec 1.7) in
* multiple dimensions.
*
* @param vector with coordinates
* @return index in flat array
*/
private int calcSampleIndex(int[] vector)
{
// inspiration: http://stackoverflow.com/a/12113479/535646
// but used in reverse
float[] sizeValues = getSize().toFloatArray();
int index = 0;
int sizeProduct = 1;
int dimension = vector.length;
for (int i = dimension - 2; i >= 0; --i)
{
sizeProduct *= sizeValues[i];
}
for (int i = dimension - 1; i >= 0; --i)
{
index += sizeProduct * vector[i];
if (i - 1 >= 0)
{
sizeProduct /= sizeValues[i - 1];
}
}
return index;
}
/**
* Inner class do to an interpolation in the Nth dimension by comparing the
* content size of N-1 dimensional objects. This is done with the help of
* recursive calls. To understand the algorithm without recursion, here is a
* bilinear
* interpolation and here's a trilinear
* interpolation (external links).
*/
class Rinterpol
{
final float[] in; // coordinate that is to be interpolated
final int[] inPrev; // coordinate of the "ceil" point
final int[] inNext; // coordinate of the "floor" point
final int numberOfInputValues;
final int numberOfOutputValues = getNumberOfOutputParameters();
/**
* Constructor.
*
* @param input the input coordinates
* @param inputPrev coordinate of the "ceil" point
* @param inputNext coordinate of the "floor" point
*
*/
public Rinterpol(float[] input, int[] inputPrev, int[] inputNext)
{
in = input;
inPrev = inputPrev;
inNext = inputNext;
numberOfInputValues = input.length;
}
/**
* Calculate the interpolation.
*
* @return interpolated result sample
*/
public float[] rinterpolate()
{
return rinterpol(new int[numberOfInputValues], 0);
}
/**
* Do a linear interpolation if the two coordinates can be known, or
* call itself recursively twice.
*
* @param coord coord partially set coordinate (not set from step
* upwards); gets fully filled in the last call ("leaf"), where it is
* used to get the correct sample
* @param step between 0 (first call) and dimension - 1
* @return interpolated result sample
*/
private float[] rinterpol(int[] coord, int step)
{
float[] resultSample = new float[numberOfOutputValues];
if (step == in.length - 1)
{
// leaf
if (inPrev[step] == inNext[step])
{
coord[step] = inPrev[step];
int[] tmpSample = getSamples()[calcSampleIndex(coord)];
for (int i = 0; i < numberOfOutputValues; ++i)
{
resultSample[i] = tmpSample[i];
}
return resultSample;
}
coord[step] = inPrev[step];
int[] sample1 = getSamples()[calcSampleIndex(coord)];
coord[step] = inNext[step];
int[] sample2 = getSamples()[calcSampleIndex(coord)];
for (int i = 0; i < numberOfOutputValues; ++i)
{
resultSample[i] = interpolate(in[step], inPrev[step], inNext[step], sample1[i], sample2[i]);
}
return resultSample;
}
else
{
// branch
if (inPrev[step] == inNext[step])
{
coord[step] = inPrev[step];
return rinterpol(coord, step + 1);
}
coord[step] = inPrev[step];
float[] sample1 = rinterpol(coord, step + 1);
coord[step] = inNext[step];
float[] sample2 = rinterpol(coord, step + 1);
for (int i = 0; i < numberOfOutputValues; ++i)
{
resultSample[i] = interpolate(in[step], inPrev[step], inNext[step], sample1[i], sample2[i]);
}
return resultSample;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public float[] eval(float[] input) throws IOException
{
//This involves linear interpolation based on a set of sample points.
//Theoretically it's not that difficult ... see section 3.9.1 of the PDF Reference.
float[] sizeValues = getSize().toFloatArray();
int bitsPerSample = getBitsPerSample();
float maxSample = (float) (Math.pow(2, bitsPerSample) - 1.0);
int numberOfInputValues = input.length;
int numberOfOutputValues = getNumberOfOutputParameters();
int[] inputPrev = new int[numberOfInputValues];
int[] inputNext = new int[numberOfInputValues];
for (int i = 0; i < numberOfInputValues; i++)
{
PDRange domain = getDomainForInput(i);
PDRange encodeValues = getEncodeForParameter(i);
input[i] = clipToRange(input[i], domain.getMin(), domain.getMax());
input[i] = interpolate(input[i], domain.getMin(), domain.getMax(),
encodeValues.getMin(), encodeValues.getMax());
input[i] = clipToRange(input[i], 0, sizeValues[i] - 1);
inputPrev[i] = (int) Math.floor(input[i]);
inputNext[i] = (int) Math.ceil(input[i]);
}
// old code for N=1 and N=2, don't delete in case one uses this for optimization
//
// if (numberOfInputValues == 1)
// {
// int[] sample1 = getSamples()[calcSampleIndex(new int[]
// {
// inputPrev[0]
// })];
// int[] sample2 = getSamples()[calcSampleIndex(new int[]
// {
// inputNext[0]
// })];
// for (int i = 0; i < numberOfOutputValues; ++i)
// {
// outputValues[i] = inputPrev[0] == inputNext[0] ? sample1[i] : interpolate(input[0], inputPrev[0], inputNext[0], sample1[i], sample2[i]);
// }
// //TODO optimize so that sample is collected only when needed
// }
// if (numberOfInputValues == 2)
// {
// int[] sample1 = getSamples()[calcSampleIndex(new int[]
// {
// inputPrev[0], inputPrev[1]
// })];
// int[] sample2 = getSamples()[calcSampleIndex(new int[]
// {
// inputPrev[0], inputNext[1]
// })];
// int[] sample3 = getSamples()[calcSampleIndex(new int[]
// {
// inputNext[0], inputPrev[1]
// })];
// int[] sample4 = getSamples()[calcSampleIndex(new int[]
// {
// inputNext[0], inputNext[1]
// })];
//
// for (int i = 0; i < numberOfOutputValues; ++i)
// {
// // bilinear color interpolation, see e.g.
// // http://harmoniccode.blogspot.de/2011/04/bilinear-color-interpolation.html
// // interpolate the color at top and bottom edges (x-axis)
// // then interpolate the color between these two results (y-axis)
// double lowerVal = inputPrev[0] == inputNext[0] ? sample1[i] : interpolate(input[0], inputPrev[0], inputNext[0], sample1[i], sample3[i]);
// double upperVal = inputPrev[0] == inputNext[0] ? sample2[i] : interpolate(input[0], inputPrev[0], inputNext[0], sample2[i], sample4[i]);
// outputValues[i] = (float) (inputPrev[1] == inputNext[1] ? lowerVal : interpolate(input[1], inputPrev[1], inputNext[1], (float) lowerVal, (float) upperVal));
// //TODO optimize so that sample is collected only when needed
// }
// }
//
float[] outputValues = new Rinterpol(input, inputPrev, inputNext).rinterpolate();
for (int i = 0; i < numberOfOutputValues; i++)
{
PDRange range = getRangeForOutput(i);
PDRange decodeValues = getDecodeForParameter(i);
outputValues[i] = interpolate(outputValues[i], 0, maxSample, decodeValues.getMin(), decodeValues.getMax());
outputValues[i] = clipToRange(outputValues[i], range.getMin(), range.getMax());
}
return outputValues;
}
}