/*
* 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.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSFloat;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.common.PDRange;
import java.awt.color.ColorSpace;
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 PDCIEBasedColorSpace
{
private static final ColorSpace CIEXYZ = ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);
private final COSDictionary dictionary;
private PDColor initialColor;
// we need to cache whitepoint values, because using getWhitePoint()
// would create a new default object for each pixel conversion if the original
// PDF didn't have a whitepoint array
private float wpX = 1;
private float wpY = 1;
private float wpZ = 1;
/**
* Creates a new Lab color space.
*/
public PDLab()
{
array = new COSArray();
dictionary = new COSDictionary();
array.add(COSName.LAB);
array.add(dictionary);
}
/**
* Creates a new Lab color space from a PDF array.
* @param lab the color space array
*/
public PDLab(COSArray lab)
{
array = lab;
dictionary = (COSDictionary)array.getObject(1);
// init whitepoint cache
PDTristimulus whitepoint = getWhitepoint();
wpX = whitepoint.getX();
wpY = whitepoint.getY();
wpZ = whitepoint.getZ();
}
@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();
float minA = getARange().getMin();
float maxA = getARange().getMax();
float minB = getBRange().getMin();
float maxB = getBRange().getMax();
// 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] * (maxA - minA));
abc[2] = minB + (abc[2] * (maxB - minB));
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 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));
// toRGB() malfunctions with negative values
// XYZ must be non-negative anyway:
// http://ninedegreesbelow.com/photography/icc-profile-negative-tristimulus.html
if (x < 0)
{
x = 0;
}
if (y < 0)
{
y = 0;
}
if (z < 0)
{
z = 0;
}
// XYZ to RGB
return CIEXYZ.toRGB(new float[] { 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 = getARange();
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;
}
/**
* This will return the whitepoint tristimulus.
* As this is a required field this will never return null.
* A default of 1,1,1 will be returned if the pdf does not have any values yet.
* @return the whitepoint tristimulus
*/
public PDTristimulus getWhitepoint()
{
COSArray wp = (COSArray)dictionary.getDictionaryObject(COSName.WHITE_POINT);
if(wp == null)
{
wp = new COSArray();
wp.add(new COSFloat(1.0f));
wp.add(new COSFloat(1.0f));
wp.add(new COSFloat(1.0f));
}
return new PDTristimulus(wp);
}
/**
* This will return the BlackPoint tristimulus.
* This is an optional field but has defaults so this will never return null.
* A default of 0,0,0 will be returned if the pdf does not have any values yet.
* @return the blackpoint tristimulus
*/
public PDTristimulus getBlackPoint()
{
COSArray bp = (COSArray)dictionary.getDictionaryObject(COSName.BLACK_POINT);
if(bp == null)
{
bp = new COSArray();
bp.add(new COSFloat(0.0f));
bp.add(new COSFloat(0.0f));
bp.add(new COSFloat(0.0f));
}
return new PDTristimulus(bp);
}
/**
* 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 = (COSArray) dictionary.getDictionaryObject(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 = (COSArray) dictionary.getDictionaryObject(COSName.RANGE);
if (rangeArray == null)
{
rangeArray = getDefaultRangeArray();
}
return new PDRange(rangeArray, 1);
}
/**
* This will set the whitepoint tristimulus.
* As this is a required field this null should not be passed into this function.
* @param whitepoint the whitepoint tristimulus
*/
public void setWhitePoint(PDTristimulus whitepoint)
{
COSBase wpArray = whitepoint.getCOSObject();
if(wpArray != null)
{
dictionary.setItem(COSName.WHITE_POINT, wpArray);
}
// update cached values
wpX = whitepoint.getX();
wpY = whitepoint.getY();
wpZ = whitepoint.getZ();
}
/**
* This will set the BlackPoint tristimulus.
* As this is a required field this null should not be passed into this function.
* @param blackpoint the BlackPoint tristimulus
*/
public void setBlackPoint(PDTristimulus blackpoint)
{
COSBase bpArray = null;
if(blackpoint != null)
{
bpArray = blackpoint.getCOSObject();
}
dictionary.setItem(COSName.BLACK_POINT, bpArray);
}
/**
* 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)
{
COSArray rangeArray = (COSArray) dictionary.getDictionaryObject(COSName.RANGE);
if (rangeArray == null)
{
rangeArray = getDefaultRangeArray();
}
//if null then reset to defaults
if(range == null)
{
rangeArray.set(0, new COSFloat(-100));
rangeArray.set(1, new COSFloat(100));
}
else
{
rangeArray.set(0, new COSFloat(range.getMin()));
rangeArray.set(1, new COSFloat(range.getMax()));
}
dictionary.setItem(COSName.RANGE, rangeArray);
initialColor = null;
}
/**
* 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)
{
COSArray rangeArray = (COSArray) dictionary.getDictionaryObject(COSName.RANGE);
if (rangeArray == null)
{
rangeArray = getDefaultRangeArray();
}
//if null then reset to defaults
if(range == null)
{
rangeArray.set(2, new COSFloat(-100));
rangeArray.set(3, new COSFloat(100));
}
else
{
rangeArray.set(2, new COSFloat(range.getMin()));
rangeArray.set(3, new COSFloat(range.getMax()));
}
dictionary.setItem(COSName.RANGE, rangeArray);
initialColor = null;
}
}