| /* |
| * 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.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 |
| * |
| * @throws IOException if the colorspace could not be created |
| */ |
| public PDDeviceN(COSArray deviceN) throws IOException |
| { |
| array = deviceN; |
| alternateColorSpace = PDColorSpace.create(array.getObject(ALTERNATE_CS)); |
| tintTransform = PDFunction.create(array.getObject(TINT_TRANSFORM)); |
| |
| if (array.size() > DEVICEN_ATTRIBUTES) |
| { |
| attributes = new PDDeviceNAttributes((COSDictionary)array.getObject(DEVICEN_ATTRIBUTES)); |
| } |
| initColorConversionCache(); |
| |
| // 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() 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 to the corresponding process component (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(); |
| |
| // 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(); |
| } |
| } |