# PDIndexed.java

    /*
     * 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.graphics.color;

    import java.awt.Point;
    import java.awt.image.BufferedImage;
    import java.awt.image.ColorModel;
    import java.awt.image.DataBuffer;
    import java.awt.image.IndexColorModel;
    import java.awt.image.Raster;
    import java.awt.image.WritableRaster;
    import java.io.IOException;

    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.cos.COSNumber;
    import org.apache.pdfbox.cos.COSStream;
    import org.apache.pdfbox.cos.COSString;
    import org.apache.pdfbox.pdmodel.PDResources;
    import org.apache.pdfbox.pdmodel.common.PDStream;

    /**
     * An Indexed colour space specifies that an area is to be painted using a colour table
     * of arbitrary colours from another color space.
     *
     * @author John Hewson
     * @author Ben Litchfield
     */
    public final class PDIndexed extends PDSpecialColorSpace
    {
        private final PDColor initialColor = new PDColor(new float[] { 0 }, this);

        private PDColorSpace baseColorSpace = null;

        // cached lookup data
        private byte[] lookupData;
        private float[][] colorTable;
        private int actualMaxIndex;
        private int[][] rgbColorTable;

        /**
         * Creates a new Indexed color space.
         * Default DeviceRGB, hival 255.
         */
        public PDIndexed()
        {
            array = new COSArray();
            array.add(COSName.INDEXED);
            array.add(COSName.DEVICERGB);
            array.add(COSInteger.get(255));
            array.add(org.apache.pdfbox.cos.COSNull.NULL);
        }

        /**
         * Creates a new indexed color space from the given PDF array.
         * @param indexedArray the array containing the indexed parameters
         * @throws java.io.IOException
         */
        public PDIndexed(COSArray indexedArray) throws IOException
        {
            this(indexedArray, null);
        }

        /**
         * Creates a new indexed color space from the given PDF array.
         * @param indexedArray the array containing the indexed parameters
         * @param resources the resources, can be null. Allows to use its cache for the colorspace.
         * @throws java.io.IOException
         */
        public PDIndexed(COSArray indexedArray, PDResources resources) throws IOException
        {
            array = indexedArray;
            // don't call getObject(1), we want to pass a reference if possible
            // to profit from caching (PDFBOX-4149)
            baseColorSpace = PDColorSpace.create(array.get(1), resources);
            readColorTable();
            initRgbColorTable();
        }

        @Override
        public String getName()
        {
            return COSName.INDEXED.getName();
        }

        @Override
        public int getNumberOfComponents()
        {
            return 1;
        }

        @Override
        public float[] getDefaultDecode(int bitsPerComponent)
        {
            return new float[] { 0, (float)Math.pow(2, bitsPerComponent) - 1 };
        }

        @Override
        public PDColor getInitialColor()
        {
            return initialColor;
        }

        //
        // WARNING: this method is performance sensitive, modify with care!
        //
        private void initRgbColorTable() throws IOException
        {
            int numBaseComponents = baseColorSpace.getNumberOfComponents();

            // convert the color table into a 1-row BufferedImage in the base color space,
            // using a writable raster for high performance
            WritableRaster baseRaster;
            try
            {
                baseRaster = Raster.createBandedRaster(DataBuffer.TYPE_BYTE,
                        actualMaxIndex + 1, 1, numBaseComponents, new Point(0, 0));
            }
            catch (IllegalArgumentException ex)
            {
                // PDFBOX-4503: when stream is empty or null
                throw new IOException(ex);
            }

            int[] base = new int[numBaseComponents];
            for (int i = 0, n = actualMaxIndex; i <= n; i++)
            {
                for (int c = 0; c < numBaseComponents; c++)
                {
                    base[c] = (int)(colorTable[i][c] * 255f);
                }
                baseRaster.setPixel(i, 0, base);
            }

            // convert the base image to RGB
            BufferedImage rgbImage = baseColorSpace.toRGBImage(baseRaster);
            WritableRaster rgbRaster = rgbImage.getRaster();

            // build an RGB lookup table from the raster
            rgbColorTable = new int[actualMaxIndex + 1][3];
            int[] nil = null;

            for (int i = 0, n = actualMaxIndex; i <= n; i++)
            {
                rgbColorTable[i] = rgbRaster.getPixel(i, 0, nil);
            }
        }

        //
        // WARNING: this method is performance sensitive, modify with care!
        //
        @Override
        public float[] toRGB(float[] value)
        {
            if (value.length > 1)
            {
                throw new IllegalArgumentException("Indexed color spaces must have one color value");
            }

            // scale and clamp input value
            int index = Math.round(value[0]);
            index = Math.max(index, 0);
            index = Math.min(index, actualMaxIndex);

            // lookup rgb
            int[] rgb = rgbColorTable[index];
            return new float[] { rgb[0] / 255f, rgb[1] / 255f, rgb[2] / 255f };
        }

        //
        // WARNING: this method is performance sensitive, modify with care!
        //
        @Override
        public BufferedImage toRGBImage(WritableRaster raster) throws IOException
        {
            // use lookup table
            int width = raster.getWidth();
            int height = raster.getHeight();

            BufferedImage rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            WritableRaster rgbRaster = rgbImage.getRaster();

            int[] src = new int[1];
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    raster.getPixel(x, y, src);

                    // lookup
                    int index = Math.min(src[0], actualMaxIndex);
                    rgbRaster.setPixel(x, y, rgbColorTable[index]);
                }
            }

            return rgbImage;
        }

        @Override
        public BufferedImage toRawImage(WritableRaster raster)
        {
            // We can only convert sRGB index colorspaces, depending on the base colorspace
            if (baseColorSpace instanceof PDICCBased && ((PDICCBased) baseColorSpace).isSRGB())
            {
                byte[] r = new byte[colorTable.length];
                byte[] g = new byte[colorTable.length];
                byte[] b = new byte[colorTable.length];
                for (int i = 0; i < colorTable.length; i++)
                {
                    r[i] = (byte) ((int) (colorTable[i][0] * 255) & 0xFF);
                    g[i] = (byte) ((int) (colorTable[i][1] * 255) & 0xFF);
                    b[i] = (byte) ((int) (colorTable[i][2] * 255) & 0xFF);
                }
                ColorModel colorModel = new IndexColorModel(8, colorTable.length, r, g, b);
                return new BufferedImage(colorModel, raster, false, null);
            }

            // We can't handle all other cases at the moment.
            return null;
        }

        /**
         * Returns the base color space.
         * @return the base color space.
         */
        public PDColorSpace getBaseColorSpace()
        {
            return baseColorSpace;
        }

        // returns "hival" array element
        private int getHival()
        {
            return ((COSNumber) array.getObject(2)).intValue();
        }

        // reads the lookup table data from the array
        private void readLookupData() throws IOException
        {
            if (lookupData == null)
            {
                COSBase lookupTable = array.getObject(3);
                if (lookupTable instanceof COSString)
                {
                    lookupData = ((COSString) lookupTable).getBytes();
                }
                else if (lookupTable instanceof COSStream)
                {
                    lookupData = new PDStream((COSStream)lookupTable).toByteArray();
                }
                else if (lookupTable == null)
                {
                    lookupData = new byte[0];
                }
                else
                {
                    throw new IOException("Error: Unknown type for lookup table " + lookupTable);
                }
            }
        }

        //
        // WARNING: this method is performance sensitive, modify with care!
        //
        private void readColorTable() throws IOException
        {
            readLookupData();

            int maxIndex = Math.min(getHival(), 255);
            int numComponents = baseColorSpace.getNumberOfComponents();

            // some tables are too short
            if (lookupData.length / numComponents < maxIndex + 1)
            {
                maxIndex = lookupData.length / numComponents - 1;
            }
            actualMaxIndex = maxIndex;  // TODO "actual" is ugly, tidy this up

            colorTable = new float[maxIndex + 1][numComponents];
            for (int i = 0, offset = 0; i <= maxIndex; i++)
            {
                for (int c = 0; c < numComponents; c++)
                {
                    colorTable[i][c] = (lookupData[offset] & 0xff) / 255f;
                    offset++;
                }
            }
        }

        /**
         * Sets the base color space.
         * @param base the base color space
         */
        public void setBaseColorSpace(PDColorSpace base)
        {
            array.set(1, base.getCOSObject());
            baseColorSpace = base;
        }

        /**
         * Sets the highest value that is allowed. This cannot be higher than 255.
         * @param high the highest value for the lookup table
         */
        public void setHighValue(int high)
        {
            array.set(2, high);
        }

        @Override
        public String toString()
        {
            return "Indexed{base:" + baseColorSpace + " " +
                    "hival:" + getHival() + " " +
                    "lookup:(" + colorTable.length + " entries)}";
        }
    }