PDColorSpace.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.Graphics;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ComponentColorModel;
import java.awt.image.WritableRaster;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.io.IOException;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.pdmodel.MissingResourceException;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.ResourceCache;
/**
* A color space specifies how the colours of graphics objects will be painted on the page.
*
* @author John Hewson
* @author Ben Litchfield
*/
public abstract class PDColorSpace implements COSObjectable
{
private final ColorConvertOp colorConvertOp = new ColorConvertOp(null);
/**
* Creates a color space given a name or array.
* @param colorSpace the color space COS object
* @return a new color space
* @throws IOException if the color space is unknown or cannot be created
*/
public static PDColorSpace create(COSBase colorSpace) throws IOException
{
return create(colorSpace, null);
}
/**
* Creates a color space given a name or array. Abbreviated device color names are not supported
* here, please replace them first.
*
* @param colorSpace the color space COS object
* @param resources the current resources.
* @return a new color space
* @throws MissingResourceException if the color space is missing in the resources dictionary
* @throws IOException if the color space is unknown or cannot be created
*/
public static PDColorSpace create(COSBase colorSpace,
PDResources resources)
throws IOException
{
return create(colorSpace, resources, false);
}
/**
* Creates a color space given a name or array. Abbreviated device color names are not supported
* here, please replace them first. This method is for PDFBox internal use only, others should
* use {@link #create(COSBase, PDResources)}.
*
* @param colorSpace the color space COS object
* @param resources the current resources.
* @param wasDefault if current color space was used by a default color space.
* @return a new color space.
* @throws MissingResourceException if the color space is missing in the resources dictionary
* @throws IOException if the color space is unknown or cannot be created.
*/
public static PDColorSpace create(COSBase colorSpace,
PDResources resources,
boolean wasDefault)
throws IOException
{
if (colorSpace instanceof COSObject)
{
return createFromCOSObject((COSObject) colorSpace, resources);
}
else if (colorSpace instanceof COSName)
{
COSName name = (COSName)colorSpace;
// default color spaces
if (resources != null)
{
COSName defaultName = null;
if (name.equals(COSName.DEVICECMYK) &&
resources.hasColorSpace(COSName.DEFAULT_CMYK))
{
defaultName = COSName.DEFAULT_CMYK;
}
else if (name.equals(COSName.DEVICERGB) &&
resources.hasColorSpace(COSName.DEFAULT_RGB))
{
defaultName = COSName.DEFAULT_RGB;
}
else if (name.equals(COSName.DEVICEGRAY) &&
resources.hasColorSpace(COSName.DEFAULT_GRAY))
{
defaultName = COSName.DEFAULT_GRAY;
}
if (resources.hasColorSpace(defaultName) && !wasDefault)
{
return resources.getColorSpace(defaultName, true);
}
}
// built-in color spaces
if (name == COSName.DEVICECMYK)
{
return PDDeviceCMYK.INSTANCE;
}
else if (name == COSName.DEVICERGB)
{
return PDDeviceRGB.INSTANCE;
}
else if (name == COSName.DEVICEGRAY)
{
return PDDeviceGray.INSTANCE;
}
else if (name == COSName.PATTERN)
{
return new PDPattern(resources);
}
else if (resources != null)
{
if (!resources.hasColorSpace(name))
{
throw new MissingResourceException("Missing color space: " + name.getName());
}
return resources.getColorSpace(name);
}
else
{
throw new MissingResourceException("Unknown color space: " + name.getName());
}
}
else if (colorSpace instanceof COSArray)
{
COSArray array = (COSArray)colorSpace;
if (array.size() == 0)
{
throw new IOException("Colorspace array is empty");
}
COSBase base = array.getObject(0);
if (!(base instanceof COSName))
{
throw new IOException("First element in colorspace array must be a name");
}
COSName name = (COSName) base;
// TODO cache these returned color spaces?
if (name == COSName.CALGRAY)
{
return new PDCalGray(array);
}
else if (name == COSName.CALRGB)
{
return new PDCalRGB(array);
}
else if (name == COSName.DEVICEN)
{
return new PDDeviceN(array);
}
else if (name == COSName.INDEXED)
{
return new PDIndexed(array, resources);
}
else if (name == COSName.SEPARATION)
{
return new PDSeparation(array);
}
else if (name == COSName.ICCBASED)
{
return PDICCBased.create(array, resources);
}
else if (name == COSName.LAB)
{
return new PDLab(array);
}
else if (name == COSName.PATTERN)
{
if (array.size() == 1)
{
return new PDPattern(resources);
}
else
{
return new PDPattern(resources, PDColorSpace.create(array.get(1)));
}
}
else if (name == COSName.DEVICECMYK ||
name == COSName.DEVICERGB ||
name == COSName.DEVICEGRAY)
{
// not allowed in an array, but we sometimes encounter these regardless
return create(name, resources, wasDefault);
}
else
{
throw new IOException("Invalid color space kind: " + name);
}
}
else if (colorSpace instanceof COSDictionary &&
((COSDictionary) colorSpace).containsKey(COSName.COLORSPACE))
{
// PDFBOX-4833: dictionary with /ColorSpace entry
COSBase base = ((COSDictionary) colorSpace).getDictionaryObject(COSName.COLORSPACE);
if (base == colorSpace)
{
// PDFBOX-5315
throw new IOException("Recursion in colorspace: " +
((COSDictionary) colorSpace).getItem(COSName.COLORSPACE) + " points to itself");
}
return create(base, resources, wasDefault);
}
else
{
throw new IOException("Expected a name or array but got: " + colorSpace);
}
}
private static PDColorSpace createFromCOSObject(COSObject colorSpace, PDResources resources)
throws IOException
{
PDColorSpace cs;
if (resources != null && resources.getResourceCache() != null)
{
ResourceCache resourceCache = resources.getResourceCache();
cs = resourceCache.getColorSpace(colorSpace);
if (cs != null)
{
return cs;
}
}
cs = create(colorSpace.getObject(), resources);
if (resources != null && resources.getResourceCache() != null && cs != null)
{
ResourceCache resourceCache = resources.getResourceCache();
resourceCache.put(colorSpace, cs);
}
return cs;
}
// array for the given parameters
protected COSArray array;
/**
* Returns the name of the color space.
* @return the name of the color space
*/
public abstract String getName();
/**
* Returns the number of components in this color space
* @return the number of components in this color space
*/
public abstract int getNumberOfComponents();
/**
* Returns the default decode array for this color space.
* @param bitsPerComponent the number of bits per component.
* @return the default decode array
*/
public abstract float[] getDefaultDecode(int bitsPerComponent);
/**
* Returns the initial color value for this color space.
* @return the initial color value for this color space
*/
public abstract PDColor getInitialColor();
/**
* Returns the RGB equivalent of the given color value.
* @param value a color value with component values between 0 and 1
* @return an array of R,G,B value between 0 and 255
* @throws IOException if the color conversion fails
*/
public abstract float[] toRGB(float[] value) throws IOException;
/**
* Returns the (A)RGB equivalent of the given raster.
* @param raster the source raster
* @return an (A)RGB buffered image
* @throws IOException if the color conversion fails
*/
public abstract BufferedImage toRGBImage(WritableRaster raster) throws IOException;
/**
* Returns the image in this colorspace or null. No conversion is performed.
*
* For special colorspaces like PDSeparation the image is returned in the gray colorspace.
* For undefined colorspaces like DeviceCMYK/DeviceRGB and DeviceGray null is returned.
*
* You can always fallback to {@link #toRGBImage(WritableRaster)} if this returns null.
*
* @param raster the source raster
* @return an buffered image in this colorspace. Or null if it is not possible to extract
* that image with the original colorspace without conversion.
*/
public abstract BufferedImage toRawImage(WritableRaster raster) throws IOException;
/**
* Returns the given raster as BufferedImage with the given awtColorSpace using a
* ComponentColorModel.
* @param raster the source raster
* @param awtColorSpace the AWT colorspace
* @return a BufferedImage in this colorspace
*/
protected final BufferedImage toRawImage(WritableRaster raster, ColorSpace awtColorSpace)
{
ColorModel colorModel = new ComponentColorModel(awtColorSpace,
false, false, Transparency.OPAQUE, raster.getDataBuffer().getDataType());
return new BufferedImage(colorModel, raster, false, null);
}
/**
* Returns the (A)RGB equivalent of the given raster, using the given AWT color space
* to perform the conversion.
* @param raster the source raster
* @param colorSpace the AWT
* @return an (A)RGB buffered image
*/
protected BufferedImage toRGBImageAWT(WritableRaster raster, ColorSpace colorSpace)
{
//
// WARNING: this method is performance sensitive, modify with care!
//
// ICC Profile color transforms are only fast when performed using ColorConvertOp
ColorModel colorModel = new ComponentColorModel(colorSpace,
false, false, Transparency.OPAQUE, raster.getDataBuffer().getDataType());
BufferedImage src = new BufferedImage(colorModel, raster, false, null);
BufferedImage dest = new BufferedImage(raster.getWidth(), raster.getHeight(),
BufferedImage.TYPE_INT_RGB);
if (src.getWidth() == 1 || src.getHeight() == 1)
{
// PDFBOX-5051: ColorConvertOp is too slow for tiny images. But we can't use drawImage()
// for all images because it is slower when used for all images.
// Re quality & speed: the quality gold standard is setRGB(getRGB()) but this is also
// the slowest. drawImage() is identical in quality (but faster) except for ICC based
// images with 1 component. ColorConvertOp is fastest except for small images, there's
// somehow a slowness "price" paid per call and the quality is slightly flawed sometimes,
// and rendering hints are ignored.
// All the above tested with jdk8 and LCMS.
Graphics g2d = dest.getGraphics();
g2d.drawImage(src, 0, 0, null);
g2d.dispose();
return dest;
}
colorConvertOp.filter(src, dest);
return dest;
}
@Override
public COSBase getCOSObject()
{
return array;
}
}