You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by le...@apache.org on 2014/02/02 14:47:11 UTC

svn commit: r1563611 - in /pdfbox/branches/1.8: ./ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java

Author: lehmi
Date: Sun Feb  2 13:47:10 2014
New Revision: 1563611

URL: http://svn.apache.org/r1563611
Log:
PDFBOX-1870: added interpolation a fixed an issue with the sample bits as proposed by Tilman Hausherr

Modified:
    pdfbox/branches/1.8/   (props changed)
    pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java

Propchange: pdfbox/branches/1.8/
------------------------------------------------------------------------------
  Merged /pdfbox/trunk:r1563210,1563426

Modified: pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java?rev=1563611&r1=1563610&r2=1563611&view=diff
==============================================================================
--- pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java (original)
+++ pdfbox/branches/1.8/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/common/function/PDFunctionType0.java Sun Feb  2 13:47:10 2014
@@ -17,6 +17,8 @@
 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;
@@ -30,6 +32,7 @@ import org.apache.pdfbox.pdmodel.common.
  * This class represents a type 0 function in a PDF document.
  *
  * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
+ * @author Tilman Hausherr <ti...@snafu.de>
  * 
  */
 public class PDFunctionType0 extends PDFunction
@@ -38,18 +41,18 @@ public class PDFunctionType0 extends PDF
     /**
      * Log instance.
      */
-    private static final Log log = LogFactory.getLog(PDFunctionType0.class);
+    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) ...].
+     * 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
+     * into the range appropriate for the function's output values. Default
+     * value: same as the value of Range
      */
     private COSArray decode = null;
     /**
@@ -65,23 +68,25 @@ public class PDFunctionType0 extends PDF
     /**
      * Constructor.
      *
-     * @param functionStream The function .
+     * @param function The function.
      */
     public PDFunctionType0(COSBase function)
     {
-        super( 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.
+     * 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.
      */
@@ -89,7 +94,7 @@ public class PDFunctionType0 extends PDF
     {
         if (size == null)
         {
-            size = (COSArray)getDictionary().getDictionaryObject( COSName.SIZE );
+            size = (COSArray) getDictionary().getDictionaryObject(COSName.SIZE);
         }
         return size;
     }
@@ -107,60 +112,39 @@ public class PDFunctionType0 extends PDF
             int numberOfInputValues = getNumberOfInputParameters();
             int numberOfOutputValues = getNumberOfOutputParameters();
             COSArray sizes = getSize();
-            for (int i=0;i<numberOfInputValues;i++)
+            for (int i = 0; i < numberOfInputValues; i++)
             {
                 arraySize *= sizes.getInt(i);
             }
-            samples = new int[arraySize][getNumberOfOutputParameters()];
+            samples = new int[arraySize][numberOfOutputValues];
             int bitsPerSample = getBitsPerSample();
             int index = 0;
-            int arrayIndex = 0;
-            try {
-                byte[] samplesArray = getPDStream().getByteArray();
-                for (int i=0;i<numberOfInputValues;i++) 
+            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++)
                 {
-                    int sizeInputValues = sizes.getInt(i);
-                    for (int j=0;j<sizeInputValues;j++)
+                    for (int k = 0; k < numberOfOutputValues; k++)
                     {
-                        int bitsLeft = 0;
-                        int bitsToRead = bitsPerSample;
-                        int currentValue = 0;
-                        for (int k=0;k<numberOfOutputValues;k++)
-                        {
-                            if (bitsLeft == 0)
-                            {
-                                currentValue = (samplesArray[arrayIndex++]+256)%256;
-                                bitsLeft = 8;
-                            }
-                            int value = 0;
-                            while (bitsToRead > 0)
-                            {
-                                int bits = Math.min(bitsToRead, bitsLeft);
-                                value = value << bits;
-                                int valueToAdd = currentValue >> (8 - bits); 
-                                value |= valueToAdd;
-                                bitsToRead -= bits;
-                                bitsLeft -= bits;
-                                if (bitsLeft == 0 && bitsToRead > 0)
-                                {
-                                    currentValue = (samplesArray[arrayIndex++]+256)%256;
-                                    bitsLeft = 8;
-                                }
-                            }
-                            samples[index][k] = value;
-                            bitsToRead = bitsPerSample;
-                        }
-                        index++;
+                        // 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.");
+                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.  
      * 
@@ -170,18 +154,30 @@ public class PDFunctionType0 extends PDF
      */
     public int getBitsPerSample()
     {
-        return getDictionary().getInt( COSName.BITS_PER_SAMPLE );
+        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
+     * 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 )
+    public void setBitsPerSample(int bps)
     {
-        getDictionary().setInt( COSName.BITS_PER_SAMPLE, bps );
+        getDictionary().setInt(COSName.BITS_PER_SAMPLE, bps);
     }
     
     /**
@@ -193,17 +189,17 @@ public class PDFunctionType0 extends PDF
     {
         if (encode == null)
         {
-            encode = (COSArray)getDictionary().getDictionaryObject( COSName.ENCODE );
+            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++)
+                for (int i = 0; i < sizeValuesSize; i++)
                 {
-                    encode.add( COSInteger.ZERO );
-                    encode.add( COSInteger.get( sizeValues.getInt(i) - 1) );
+                    encode.add(COSInteger.ZERO);
+                    encode.add(COSInteger.get(sizeValues.getInt(i) - 1));
                 }
             }
         }
@@ -219,7 +215,7 @@ public class PDFunctionType0 extends PDF
     {
         if (decode == null)
         {
-            decode = (COSArray)getDictionary().getDictionaryObject( COSName.DECODE );
+            decode = (COSArray) getDictionary().getDictionaryObject(COSName.DECODE);
             // if decode is null, the default values are the range values
             if (decode == null)
             {
@@ -236,13 +232,13 @@ public class PDFunctionType0 extends PDF
      *
      * @return The encode parameter range or null if none is set.
      */
-    public PDRange getEncodeForParameter( int paramNum )
+    public PDRange getEncodeForParameter(int paramNum)
     {
         PDRange retval = null;
         COSArray encodeValues = getEncodeValues();
-        if( encodeValues != null && encodeValues.size() >= paramNum*2+1 )
+        if (encodeValues != null && encodeValues.size() >= paramNum * 2 + 1)
         {
-            retval = new PDRange(encodeValues, paramNum );
+            retval = new PDRange(encodeValues, paramNum);
         }
         return retval;
     }
@@ -250,7 +246,7 @@ public class PDFunctionType0 extends PDF
     /**
      * This will set the encode values.
      *
-     * @param range The new encode values.
+     * @param encodeValues The new encode values.
      */
     public void setEncodeValues(COSArray encodeValues)
     {
@@ -265,13 +261,13 @@ public class PDFunctionType0 extends PDF
      *
      * @return The decode parameter range or null if none is set.
      */
-    public PDRange getDecodeForParameter( int paramNum )
+    public PDRange getDecodeForParameter(int paramNum)
     {
         PDRange retval = null;
         COSArray decodeValues = getDecodeValues();
-        if( decodeValues != null && decodeValues.size() >= paramNum*2+1 )
+        if (decodeValues != null && decodeValues.size() >= paramNum * 2 + 1)
         {
-            retval = new PDRange(decodeValues, paramNum );
+            retval = new PDRange(decodeValues, paramNum);
         }
         return retval;
     }
@@ -279,7 +275,7 @@ public class PDFunctionType0 extends PDF
     /**
      * This will set the decode values.
      *
-     * @param range The new decode values.
+     * @param decodeValues The new decode values.
      */
     public void setDecodeValues(COSArray decodeValues)
     {
@@ -288,61 +284,228 @@ public class PDFunctionType0 extends PDF
     }
     
     /**
+     * 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
+     * <a
+     * href="http://harmoniccode.blogspot.de/2011/04/bilinear-color-interpolation.html">bilinear
+     * interpolation</a> and here's a <a
+     * href="https://en.wikipedia.org/wiki/Trilinear_interpolation">trilinear
+     * interpolation</a> (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 intInputValuesPrevious = 0;
-        int intInputValuesNext = 0;
-        for (int i=0; i<numberOfInputValues; i++) {
+
+        int[] inputPrev = new int[numberOfInputValues];
+        int[] inputNext = new int[numberOfInputValues];
+
+        for (int i = 0; i < numberOfInputValues; i++)
+        {
             PDRange domain = getDomainForInput(i);
-            PDRange encode = getEncodeForParameter(i);
+            PDRange encodeValues = getEncodeForParameter(i);
             input[i] = clipToRange(input[i], domain.getMin(), domain.getMax());
-            input[i] = interpolate(input[i], domain.getMin(), domain.getMax(), encode.getMin(), encode.getMax());
-            input[i] = clipToRange(input[i], 0, sizeValues[i]-1);
-            intInputValuesPrevious += (int)Math.floor(input[i]);
-            intInputValuesNext += (int)Math.ceil(input[i]);
-        }
-        float[] outputValuesPrevious = null;
-        float[] outputValuesNext = null;
-        outputValuesPrevious = getSample(intInputValuesPrevious);
-        outputValuesNext = getSample(intInputValuesNext);
-        float[] outputValues = new float[numberOfOutputValues];
-        for (int i=0;i<numberOfOutputValues;i++) 
+            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 decode = getDecodeForParameter(i);
-            // TODO using only a linear interpolation. 
-            // See "Order" entry in table 3.36 of the PDF reference
-            outputValues[i] = (outputValuesPrevious[i] + outputValuesNext[i]) / 2;
-            outputValues[i] = interpolate(outputValues[i], 0, (float)Math.pow(2, bitsPerSample), decode.getMin(), decode.getMax());
+            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;
     }
-    
-    /**
-     * Get the samples for the given input values.
-     * 
-     * @param indexValue the index into the sample values array
-     * @return an array with the corresponding samples
-     */
-    private float[] getSample(int indexValue)
-    {
-        int[] sampleValues = getSamples()[indexValue];
-        int numberOfValues = sampleValues.length;
-        float[] result = new float[numberOfValues];
-        for (int i=0;i<numberOfValues;i++)
-        {
-            result[i] = sampleValues[i];
         }
-        return result;
-    }
-}