PDDeviceN.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.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.COSNull;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.function.PDFunction;
/**
* DeviceN colour spaces may contain an arbitrary number of colour components.
* DeviceN represents a colour space containing multiple components that correspond to colorants
* of some target device. As with Separation colour spaces, readers are able to approximate the
* colorants if they are not available on the current output device, such as a display
*
* @author John Hewson
* @author Ben Litchfield
*/
public class PDDeviceN extends PDSpecialColorSpace
{
// array indexes
private static final int COLORANT_NAMES = 1;
private static final int ALTERNATE_CS = 2;
private static final int TINT_TRANSFORM = 3;
private static final int DEVICEN_ATTRIBUTES = 4;
// fields
private PDColorSpace alternateColorSpace = null;
private PDFunction tintTransform = null;
private PDDeviceNAttributes attributes;
private PDColor initialColor;
// color conversion cache
private int numColorants;
private int[] colorantToComponent;
private PDColorSpace processColorSpace;
private PDSeparation[] spotColorSpaces;
/**
* Creates a new DeviceN color space.
*/
public PDDeviceN()
{
array = new COSArray();
array.add(COSName.DEVICEN);
// empty placeholder
array.add(COSNull.NULL);
array.add(COSNull.NULL);
array.add(COSNull.NULL);
}
/**
* Creates a new DeviceN color space from the given COS array.
*
* @param deviceN an array containing the color space information
* @param resources resources, can be null.
*
* @throws IOException if the colorspace could not be created
*/
public PDDeviceN(COSArray deviceN, PDResources resources) throws IOException
{
array = deviceN;
alternateColorSpace = PDColorSpace.create(array.getObject(ALTERNATE_CS), resources);
tintTransform = PDFunction.create(array.getObject(TINT_TRANSFORM));
if (array.size() > DEVICEN_ATTRIBUTES)
{
attributes = new PDDeviceNAttributes((COSDictionary)array.getObject(DEVICEN_ATTRIBUTES));
}
initColorConversionCache(resources);
// set initial color space
int n = getNumberOfComponents();
float[] initial = new float[n];
Arrays.fill(initial, 1);
initialColor = new PDColor(initial, this);
}
// initializes the color conversion cache
private void initColorConversionCache(PDResources resources) throws IOException
{
// there's nothing to cache for non-attribute spaces
if (attributes == null)
{
return;
}
// colorant names
List<String> colorantNames = getColorantNames();
numColorants = colorantNames.size();
// process components
colorantToComponent = new int[numColorants];
if (attributes.getProcess() != null)
{
List<String> components = attributes.getProcess().getComponents();
// map each colorant name to the corresponding process component name (if any)
for (int c = 0; c < numColorants; c++)
{
colorantToComponent[c] = components.indexOf(colorantNames.get(c));
}
// process color space
processColorSpace = attributes.getProcess().getColorSpace();
}
else
{
for (int c = 0; c < numColorants; c++)
{
colorantToComponent[c] = -1;
}
}
// spot colorants
spotColorSpaces = new PDSeparation[numColorants];
// spot color spaces
Map<String, PDSeparation> spotColorants = attributes.getColorants(resources);
// map each colorant to the corresponding spot color space
for (int c = 0; c < numColorants; c++)
{
String name = colorantNames.get(c);
PDSeparation spot = spotColorants.get(name);
if (spot != null)
{
// spot colorant
spotColorSpaces[c] = spot;
// spot colors may replace process colors with same name
// providing that the subtype is not NChannel.
if (!isNChannel())
{
colorantToComponent[c] = -1;
}
}
else
{
// process colorant
spotColorSpaces[c] = null;
}
}
}
@Override
public BufferedImage toRGBImage(WritableRaster raster) throws IOException
{
if (attributes != null)
{
return toRGBWithAttributes(raster);
}
else
{
return toRGBWithTintTransform(raster);
}
}
//
// WARNING: this method is performance sensitive, modify with care!
//
private BufferedImage toRGBWithAttributes(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();
// white background
Graphics2D g = rgbImage.createGraphics();
g.setBackground(Color.WHITE);
g.clearRect(0, 0, width, height);
g.dispose();
// look up each colorant
for (int c = 0; c < numColorants; c++)
{
PDColorSpace componentColorSpace;
if (colorantToComponent[c] >= 0)
{
// process color
componentColorSpace = processColorSpace;
}
else if (spotColorSpaces[c] == null)
{
// TODO this happens in the Altona Visual test, is there a better workaround?
// missing spot color, fallback to using tintTransform
return toRGBWithTintTransform(raster);
}
else
{
// spot color
componentColorSpace = spotColorSpaces[c];
}
int numberOfComponents = componentColorSpace.getNumberOfComponents();
// copy single-component to its own raster in the component color space
WritableRaster componentRaster = Raster.createBandedRaster(DataBuffer.TYPE_BYTE,
width, height, numberOfComponents, new Point(0, 0));
int[] samples = new int[numColorants];
int[] componentSamples = new int[numberOfComponents];
boolean isProcessColorant = colorantToComponent[c] >= 0;
int componentIndex = colorantToComponent[c];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
raster.getPixel(x, y, samples);
if (isProcessColorant)
{
// process color
componentSamples[componentIndex] = samples[c];
}
else
{
// spot color
componentSamples[0] = samples[c];
}
componentRaster.setPixel(x, y, componentSamples);
}
}
// convert single-component raster to RGB
BufferedImage rgbComponentImage = componentColorSpace.toRGBImage(componentRaster);
WritableRaster rgbComponentRaster = rgbComponentImage.getRaster();
// combine the RGB component with the RGB composite raster
int[] rgbChannel = new int[3];
int[] rgbComposite = new int[3];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
rgbComponentRaster.getPixel(x, y, rgbChannel);
rgbRaster.getPixel(x, y, rgbComposite);
// multiply (blend mode)
rgbChannel[0] = rgbChannel[0] * rgbComposite[0] >> 8;
rgbChannel[1] = rgbChannel[1] * rgbComposite[1] >> 8;
rgbChannel[2] = rgbChannel[2] * rgbComposite[2] >> 8;
rgbRaster.setPixel(x, y, rgbChannel);
}
}
}
return rgbImage;
}
//
// WARNING: this method is performance sensitive, modify with care!
//
private BufferedImage toRGBWithTintTransform(WritableRaster raster) throws IOException
{
// cache color mappings
Map<String, int[]> map1 = new HashMap<>();
String key;
StringBuilder keyBuilder = new StringBuilder();
int width = raster.getWidth();
int height = raster.getHeight();
// use the tint transform to convert the sample into
// the alternate color space (this is usually 1:many)
BufferedImage rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
WritableRaster rgbRaster = rgbImage.getRaster();
int[] rgb = new int[3];
int numSrcComponents = getColorantNames().size();
float[] src = new float[numSrcComponents];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
raster.getPixel(x, y, src);
// use a string representation as key
keyBuilder.append(src[0]);
for (int s = 1; s < numSrcComponents; s++)
{
keyBuilder.append('#').append(src[s]);
}
key = keyBuilder.toString();
keyBuilder.setLength(0);
int[] pxl = map1.get(key);
if (pxl != null)
{
rgbRaster.setPixel(x, y, pxl);
continue;
}
// scale to 0..1
for (int s = 0; s < numSrcComponents; s++)
{
src[s] = src[s] / 255;
}
// convert to alternate color space via tint transform
float[] result = tintTransform.eval(src);
// convert from alternate color space to RGB
float[] rgbFloat = alternateColorSpace.toRGB(result);
// scale to 0..255
rgb[0] = (int) (rgbFloat[0] * 255f);
rgb[1] = (int) (rgbFloat[1] * 255f);
rgb[2] = (int) (rgbFloat[2] * 255f);
// must clone because rgb is reused
map1.put(key, rgb.clone());
rgbRaster.setPixel(x, y, rgb);
}
}
return rgbImage;
}
@Override
public float[] toRGB(float[] value) throws IOException
{
if (attributes != null)
{
return toRGBWithAttributes(value);
}
else
{
return toRGBWithTintTransform(value);
}
}
private float[] toRGBWithAttributes(float[] value) throws IOException
{
float[] rgbValue = { 1, 1, 1 };
// look up each colorant
for (int c = 0; c < numColorants; c++)
{
PDColorSpace componentColorSpace;
boolean isProcessColorant = colorantToComponent[c] >= 0;
if (isProcessColorant)
{
// process color
componentColorSpace = processColorSpace;
}
else if (spotColorSpaces[c] == null)
{
// TODO this happens in the Altona Visual test, is there a better workaround?
// missing spot color, fallback to using tintTransform
return toRGBWithTintTransform(value);
}
else
{
// spot color
componentColorSpace = spotColorSpaces[c];
}
// get the single component
float[] componentSamples = new float[componentColorSpace.getNumberOfComponents()];
if (isProcessColorant)
{
// process color
int componentIndex = colorantToComponent[c];
componentSamples[componentIndex] = value[c];
}
else
{
// spot color
componentSamples[0] = value[c];
}
// convert single component to RGB
float[] rgbComponent = componentColorSpace.toRGB(componentSamples);
// combine the RGB component value with the RGB composite value
// multiply (blend mode)
rgbValue[0] *= rgbComponent[0];
rgbValue[1] *= rgbComponent[1];
rgbValue[2] *= rgbComponent[2];
}
return rgbValue;
}
private float[] toRGBWithTintTransform(float[] value) throws IOException
{
// use the tint transform to convert the sample into
// the alternate color space (this is usually 1:many)
float[] altValue = tintTransform.eval(value);
// convert the alternate color space to RGB
return alternateColorSpace.toRGB(altValue);
}
@Override
public BufferedImage toRawImage(WritableRaster raster)
{
// We don't know how to convert that.
return null;
}
/**
* Returns true if this color space has the NChannel subtype.
* @return true if subtype is NChannel
*/
public boolean isNChannel()
{
return attributes != null && attributes.isNChannel();
}
@Override
public String getName()
{
return COSName.DEVICEN.getName();
}
@Override
public final int getNumberOfComponents()
{
return getColorantNames().size();
}
@Override
public float[] getDefaultDecode(int bitsPerComponent)
{
int n = getNumberOfComponents();
float[] decode = new float[n * 2];
for (int i = 0; i < n; i++)
{
decode[i * 2 + 1] = 1;
}
return decode;
}
@Override
public PDColor getInitialColor()
{
return initialColor;
}
/**
* Returns the list of colorants.
* @return the list of colorants
*/
public List<String> getColorantNames()
{
return ((COSArray) array.getObject(COLORANT_NAMES)).toCOSNameStringList();
}
/**
* Returns the attributes associated with the DeviceN color space.
* @return the DeviceN attributes
*/
public PDDeviceNAttributes getAttributes()
{
return attributes;
}
/**
* Sets the list of colorants
* @param names the list of colorants
*/
public void setColorantNames(List<String> names)
{
COSArray namesArray = COSArray.ofCOSNames(names);
array.set(COLORANT_NAMES, namesArray);
}
/**
* Sets the color space attributes.
* If null is passed in then all attribute will be removed.
* @param attributes the color space attributes, or null
*/
public void setAttributes(PDDeviceNAttributes attributes)
{
this.attributes = attributes;
if (attributes == null)
{
array.remove(DEVICEN_ATTRIBUTES);
}
else
{
// make sure array is large enough
while (array.size() <= DEVICEN_ATTRIBUTES)
{
array.add(COSNull.NULL);
}
array.set(DEVICEN_ATTRIBUTES, attributes.getCOSDictionary());
}
}
/**
* This will get the alternate color space for this separation.
*
* @return The alternate color space.
*
* @throws IOException If there is an error getting the alternate color
* space.
*/
public PDColorSpace getAlternateColorSpace() throws IOException
{
if (alternateColorSpace == null)
{
alternateColorSpace = PDColorSpace.create(array.getObject(ALTERNATE_CS));
}
return alternateColorSpace;
}
/**
* This will set the alternate color space.
*
* @param cs The alternate color space.
*/
public void setAlternateColorSpace(PDColorSpace cs)
{
alternateColorSpace = cs;
COSBase space = null;
if (cs != null)
{
space = cs.getCOSObject();
}
array.set(ALTERNATE_CS, space);
}
/**
* This will get the tint transform function.
*
* @return The tint transform function.
*
* @throws IOException if there is an error creating the function.
*/
public PDFunction getTintTransform() throws IOException
{
if (tintTransform == null)
{
tintTransform = PDFunction.create(array.getObject(TINT_TRANSFORM));
}
return tintTransform;
}
/**
* This will set the tint transform function.
*
* @param tint The tint transform function.
*/
public void setTintTransform(PDFunction tint)
{
tintTransform = tint;
array.set(TINT_TRANSFORM, tint);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder(getName());
sb.append('{');
for (String col : getColorantNames())
{
sb.append('\"');
sb.append(col);
sb.append("\" ");
}
sb.append(alternateColorSpace.getName());
sb.append(' ');
sb.append(tintTransform);
sb.append(' ');
if (attributes != null)
{
sb.append(attributes);
}
sb.append('}');
return sb.toString();
}
}