# PDLab.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 org.apache.pdfbox.cos.COSArray;
    import org.apache.pdfbox.cos.COSFloat;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.pdmodel.common.PDRange;

    import java.awt.image.BufferedImage;
    import java.awt.image.WritableRaster;
    import java.io.IOException;

    /**
     * A Lab colour space is a CIE-based ABC colour space with two transformation stages.
     *
     * @author Ben Litchfield
     * @author John Hewson
     */
    public final class PDLab extends PDCIEDictionaryBasedColorSpace
    {
        private PDColor initialColor;

        /**
         * Creates a new Lab color space.
         */
        public PDLab()
        {
            super(COSName.LAB);
        }

        /**
         * Creates a new Lab color space from a PDF array.
         * @param lab the color space array
         */
        public PDLab(COSArray lab)
        {
            super(lab);
        }

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

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

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

            PDRange aRange = getARange();
            PDRange bRange = getBRange();
            float minA = aRange.getMin();
            float maxA = aRange.getMax();
            float minB = bRange.getMin();
            float maxB = bRange.getMax();
            float deltaA = maxA - minA;
            float deltaB = maxB - minB;

            // always three components: ABC
            float[] abc = new float[3];
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    raster.getPixel(x, y, abc);

                    // 0..255 -> 0..1
                    abc[0] /= 255;
                    abc[1] /= 255;
                    abc[2] /= 255;

                    // scale to range
                    abc[0] *= 100;
                    abc[1] = minA + abc[1] * deltaA;
                    abc[2] = minB + abc[2] * deltaB;

                    float[] rgb = toRGB(abc);

                    // 0..1 -> 0..255
                    rgb[0] *= 255;
                    rgb[1] *= 255;
                    rgb[2] *= 255;

                    rgbRaster.setPixel(x, y, rgb);
                }
            }

            return rgbImage;
        }

        @Override
        public BufferedImage toRawImage(WritableRaster raster)
        {
            // Not handled at the moment.
           return null;
        }

        @Override
        public float[] toRGB(float[] value)
        {
            // CIE LAB to RGB, see http://en.wikipedia.org/wiki/Lab_color_space

            // L*
            float lstar = (value[0] + 16f) * (1f / 116f);

            // TODO: how to use the blackpoint? scale linearly between black & white?

            // XYZ
            float x = wpX * inverse(lstar + value[1] * (1f / 500f));
            float y = wpY * inverse(lstar);
            float z = wpZ * inverse(lstar - value[2] * (1f / 200f));

            return convXYZtoRGB(x, y, z);
        }

        // reverse transformation (f^-1)
        private float inverse(float x)
        {
            if (x > 6.0 / 29.0)
            {
                return x * x * x;
            }
            else
            {
                return (108f / 841f) * (x - (4f / 29f));
            }
        }

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

        @Override
        public float[] getDefaultDecode(int bitsPerComponent)
        {
            PDRange a = getARange();
            PDRange b = getBRange();
            return new float[] { 0, 100, a.getMin(), a.getMax(), b.getMin(), b.getMax() };
        }

        @Override
        public PDColor getInitialColor()
        {
            if (initialColor == null)
            {
                initialColor = new PDColor(new float[] {
                        0,
                        Math.max(0, getARange().getMin()),
                        Math.max(0, getBRange().getMin()) },
                        this);
            }
            return initialColor;
        }

        /**
         * creates a range array with default values (-100..100 -100..100).
         * @return the new range array.
         */
        private COSArray getDefaultRangeArray()
        {
            COSArray range = new COSArray();
            range.add(new COSFloat(-100));
            range.add(new COSFloat(100));
            range.add(new COSFloat(-100));
            range.add(new COSFloat(100));
            return range;
        }

        /**
         * This will get the valid range for the "a" component.
         * If none is found then the default will be returned, which is -100..100.
         * @return the "a" range.
         */
        public PDRange getARange()
        {
            COSArray rangeArray = dictionary.getCOSArray(COSName.RANGE);
            if (rangeArray == null)
            {
                rangeArray = getDefaultRangeArray();
            }
            return new PDRange(rangeArray, 0);
        }

        /**
         * This will get the valid range for the "b" component.
         * If none is found  then the default will be returned, which is -100..100.
         * @return the "b" range.
         */
        public PDRange getBRange()
        {
            COSArray rangeArray = dictionary.getCOSArray(COSName.RANGE);
            if (rangeArray == null)
            {
                rangeArray = getDefaultRangeArray();
            }
            return new PDRange(rangeArray, 1);
        }

        /**
         * This will set the a range for the "a" component.
         * @param range the new range for the "a" component,
         * or null if defaults (-100..100) are to be set.
         */
        public void setARange(PDRange range)
        {
            setComponentRangeArray(range, 0);
        }

        /**
         * This will set the "b" range for this color space.
         * @param range the new range for the "b" component,
         * or null if defaults (-100..100) are to be set.
         */
        public void setBRange(PDRange range)
        {
            setComponentRangeArray(range, 2);
        }

        private void setComponentRangeArray(PDRange range, int index)
        {
            COSArray rangeArray = dictionary.getCOSArray(COSName.RANGE);
            if (rangeArray == null)
            {
                rangeArray = getDefaultRangeArray();
            }
            if (range == null)
            {
                // reset to defaults
                rangeArray.set(index, new COSFloat(-100));
                rangeArray.set(index + 1, new COSFloat(100));
            }
            else
            {
                rangeArray.set(index, new COSFloat(range.getMin()));
                rangeArray.set(index + 1, new COSFloat(range.getMax()));
            }
            dictionary.setItem(COSName.RANGE, rangeArray);
            initialColor = null;
        }

    }