/*
* $Id: PDFImage.java,v 1.12 2010-06-14 17:32:09 lujke Exp $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package com.sun.pdfview;
import com.sun.pdfview.colorspace.IndexedColor;
import com.sun.pdfview.colorspace.PDFColorSpace;
import com.sun.pdfview.colorspace.YCCKColorSpace;
import com.sun.pdfview.decode.PDFDecoder;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Encapsulates a PDF Image
*/
public class PDFImage {
public static void dump(PDFObject obj) throws IOException {
p("dumping PDF object: " + obj);
if (obj == null) {
return;
}
HashMap dict = obj.getDictionary();
p(" dict = " + dict);
for (Object key : dict.keySet()) {
p("key = " + key + " value = " + dict.get(key));
}
}
public static void p(String string) {
System.out.println(string);
}
private static int[][] GREY_TO_ARGB = new int[8][];
private static int[] getGreyToArgbMap(int numBits)
{
assert numBits <= 8;
int[] argbVals = GREY_TO_ARGB[numBits - 1];
if (argbVals == null) {
argbVals = createGreyToArgbMap(numBits);
}
return argbVals;
}
/**
* Create a map from all bit-patterns of a certain depth greyscale to the
* corresponding sRGB values via the ICC colorr converter.
* @param numBits the number of greyscale bits
* @return a 2^bits array of standard 32-bit ARGB fits for each greyscale value
* at that bitdepth
*/
private static int[] createGreyToArgbMap(int numBits)
{
final ColorSpace greyCs = PDFColorSpace.getColorSpace(
PDFColorSpace.COLORSPACE_GRAY).getColorSpace();
byte[] greyVals = new byte[1 << numBits];
for (int i = 0; i < greyVals.length; ++i) {
greyVals[i] = (byte) (i & 0xFF);
}
final int[] argbVals = new int[greyVals.length];
final int mask = (1 << numBits) - 1;
final WritableRaster inRaster = Raster.createPackedRaster(
new DataBufferByte(
greyVals,
greyVals.length),
greyVals.length, 1,
greyVals.length,
new int[] {mask},
null);
final BufferedImage greyImage = new
BufferedImage(
new PdfComponentColorModel(
greyCs,
new int[] {numBits}),
inRaster, false, null);
final ColorModel ccm = ColorModel.getRGBdefault();
final WritableRaster outRaster = Raster.createPackedRaster(
new DataBufferInt(
argbVals,
argbVals.length),
argbVals.length, 1,
argbVals.length,
((PackedColorModel)ccm).getMasks(),
null);
final BufferedImage srgbImage = new BufferedImage(
ccm,
outRaster,
false,
null);
final ColorConvertOp op = new ColorConvertOp(
greyCs,
ColorSpace.getInstance(ColorSpace.CS_sRGB), null);
op.filter(greyImage, srgbImage);
GREY_TO_ARGB[numBits - 1] = argbVals;
return argbVals;
}
/** color key mask. Array of start/end pairs of ranges of color components to
* mask out. If a component falls within any of the ranges it is clear. */
private int[] colorKeyMask = null;
/** the width of this image in pixels */
private int width;
/** the height of this image in pixels */
private int height;
/** the colorspace to interpret the samples in */
private PDFColorSpace colorSpace;
/** the number of bits per sample component */
private int bpc;
/** whether this image is a mask or not */
private boolean imageMask = false;
/** the SMask image, if any */
private PDFImage sMask;
/** the decode array */
private float[] decode;
private float[] decodeMins;
private float[] decodeCoefficients;
/** the actual image data */
private PDFObject imageObj;
/**
* Create an instance of a PDFImage
*/
protected PDFImage(PDFObject imageObj) {
this.imageObj = imageObj;
}
/**
* Read a PDFImage from an image dictionary and stream
*
* @param obj the PDFObject containing the image's dictionary and stream
* @param resources the current resources
*/
public static PDFImage createImage(PDFObject obj, Map resources)
throws IOException {
// create the image
PDFImage image = new PDFImage(obj);
// get the width (required)
PDFObject widthObj = obj.getDictRef("Width");
if (widthObj == null) {
throw new PDFParseException("Unable to read image width: " + obj);
}
image.setWidth(widthObj.getIntValue());
// get the height (required)
PDFObject heightObj = obj.getDictRef("Height");
if (heightObj == null) {
throw new PDFParseException("Unable to get image height: " + obj);
}
image.setHeight(heightObj.getIntValue());
// figure out if we are an image mask (optional)
PDFObject imageMaskObj = obj.getDictRef("ImageMask");
if (imageMaskObj != null) {
image.setImageMask(imageMaskObj.getBooleanValue());
}
// read the bpc and colorspace (required except for masks)
if (image.isImageMask()) {
image.setBitsPerComponent(1);
// create the indexed color space for the mask
// [PATCHED by michal.busta@gmail.com] - default value od Decode according to PDF spec. is [0, 1]
// so the color arry should be:
Color[] colors = {Color.BLACK, Color.WHITE};
PDFObject imageMaskDecode = obj.getDictRef("Decode");
if (imageMaskDecode != null) {
PDFObject[] array = imageMaskDecode.getArray();
float decode0 = array[0].getFloatValue();
if (decode0 == 1.0f) {
colors = new Color[]{Color.WHITE, Color.BLACK};
}
}
image.setColorSpace(new IndexedColor(colors));
} else {
// get the bits per component (required)
PDFObject bpcObj = obj.getDictRef("BitsPerComponent");
if (bpcObj == null) {
throw new PDFParseException("Unable to get bits per component: " + obj);
}
image.setBitsPerComponent(bpcObj.getIntValue());
// get the color space (required)
PDFObject csObj = obj.getDictRef("ColorSpace");
if (csObj == null) {
throw new PDFParseException("No ColorSpace for image: " + obj);
}
PDFColorSpace cs = PDFColorSpace.getColorSpace(csObj, resources);
image.setColorSpace(cs);
}
// read the decode array
PDFObject decodeObj = obj.getDictRef("Decode");
if (decodeObj != null) {
PDFObject[] decodeArray = decodeObj.getArray();
float[] decode = new float[decodeArray.length];
for (int i = 0; i < decodeArray.length; i++) {
decode[i] = decodeArray[i].getFloatValue();
}
image.setDecode(decode);
}
// read the soft mask.
// If ImageMask is true, this entry must not be present.
// (See implementation note 52 in Appendix H.)
if (imageMaskObj == null) {
PDFObject sMaskObj = obj.getDictRef("SMask");
if (sMaskObj == null) {
// try the explicit mask, if there is no SoftMask
sMaskObj = obj.getDictRef("Mask");
}
if (sMaskObj != null) {
if (sMaskObj.getType() == PDFObject.STREAM) {
try {
PDFImage sMaskImage = PDFImage.createImage(sMaskObj, resources);
image.setSMask(sMaskImage);
} catch (IOException ex) {
p("ERROR: there was a problem parsing the mask for this object");
dump(obj);
ex.printStackTrace(System.out);
}
} else if (sMaskObj.getType() == PDFObject.ARRAY) {
// retrieve the range of the ColorKeyMask
// colors outside this range will not be painted.
try {
image.setColorKeyMask(sMaskObj);
} catch (IOException ex) {
p("ERROR: there was a problem parsing the color mask for this object");
dump(obj);
ex.printStackTrace(System.out);
}
}
}
}
return image;
}
/**
* Get the image that this PDFImage generates.
*
* @return a buffered image containing the decoded image data
*/
public BufferedImage getImage() {
try {
BufferedImage bi = (BufferedImage) imageObj.getCache();
if (bi == null) {
byte[] data = null;
ByteBuffer jpegBytes = null;
final boolean jpegDecode = PDFDecoder.isLastFilter(imageObj, PDFDecoder.DCT_FILTERS);
if (jpegDecode) {
// if we're lucky, the stream will have just the DCT
// filter applied to it, and we'll have a reference to
// an underlying mapped file, so we'll manage to avoid
// a copy of the encoded JPEG bytes
jpegBytes = imageObj.getStreamBuffer(PDFDecoder.DCT_FILTERS);
} else {
data = imageObj.getStream();
}
// parse the stream data into an actual image
bi = parseData(data, jpegBytes);
imageObj.setCache(bi);
}
// if(bi != null)
// ImageIO.write(bi, "png", new File("/tmp/test/" + System.identityHashCode(this) + ".png"));
return bi;
} catch (IOException ioe) {
System.out.println("Error reading image");
ioe.printStackTrace();
return null;
}
}
/**
* Decodes jpeg data, possibly attempting a manual YCCK decode
* if requested. Users should use {@link #getColorModel()} to
* see which color model should now be used after a successful
* decode.
*/
private class JpegDecoder
{
/** The jpeg bytes */
private ByteBuffer jpegData;
/** The color model employed */
private ColorModel cm;
/** Whether the YCCK decode work-around should be used */
private boolean ycckDecodeMode = false;
/**
* Class constructor
* @param jpegData the JPEG data
* @param cm the color model as presented in the PDF
*/
private JpegDecoder(ByteBuffer jpegData, ColorModel cm) {
this.jpegData = jpegData;
this.cm = cm;
}
/**
* Identify whether the decoder should operate in YCCK
* decode mode, whereby the YCCK Chroma is specifically
* looked for and the color model is changed to support
* converting raw YCCK color values, working around
* a lack of YCCK/CMYK report in the standard Java
* jpeg readers. Non-YCCK images will not be decoded
* while in this mode.
* @param ycckDecodeMode
*/
public void setYcckDecodeMode(boolean ycckDecodeMode) {
this.ycckDecodeMode = ycckDecodeMode;
}
/**
* Get the color model that should be used now
* @return
*/
public ColorModel getColorModel() {
return cm;
}
/**
* Attempt to decode the jpeg data
* @return the successfully decoded image
* @throws IOException if the image couldn't be decoded due
* to a lack of support or some IO problem
*/
private BufferedImage decode() throws IOException {
ImageReadParam readParam = null;
if (getDecode() != null) {
// we have to allocate our own buffered image so that we can
// install our colour model which will do the desired decode
readParam = new ImageReadParam();
SampleModel sm =
cm.createCompatibleSampleModel (getWidth (), getHeight ());
final WritableRaster raster =
Raster.createWritableRaster(sm, new Point(0, 0));
readParam.setDestination(new BufferedImage(cm, raster, true, null));
}
final Iterator<ImageReader> jpegReaderIt =
ImageIO.getImageReadersByFormatName("jpeg");
IIOException lastIioEx = null;
while (jpegReaderIt.hasNext()) {
try {
final ImageReader jpegReader = jpegReaderIt.next();
jpegReader.setInput(ImageIO.createImageInputStream(
new ByteBufferInputStream(jpegData)), true, false);
return readImage(jpegReader, readParam);
} catch (IIOException e) {
// its most likely complaining about an unsupported image
// type; hopefully the next image reader will be able to
// understand it
jpegData.reset();
lastIioEx = e;
}
}
throw lastIioEx;
}
private BufferedImage readImage(ImageReader jpegReader, ImageReadParam param) throws IOException {
if (ycckDecodeMode) {
// The standard Oracle Java JPEG readers can't deal with CMYK YCCK encoded images
// without a little help from us. We'll try and pick up such instances and work around it.
final IIOMetadata imageMeta = jpegReader.getImageMetadata(0);
if (imageMeta != null) {
final Node standardMeta = imageMeta.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
if (standardMeta != null) {
final Node chroma = getChild(standardMeta, "Chroma");
if (chroma != null) {
final Node csType = getChild(chroma, "ColorSpaceType");
if (csType != null) {
final Attr csTypeNameNode = (Attr)csType.getAttributes().getNamedItem("name");
if (csTypeNameNode != null && "YCCK".equals(csTypeNameNode.getValue())) {
// So it's a YCCK image, and we can coax a workable image out of it
// by grabbing the raw raster and installing a YCCK converting
// color space wrapper around the existing (CMYK) color space; this will
// do the YCCK conversion for us
// first make sure we can get the unadjusted raster
final Raster raster = jpegReader.readRaster(0, param);
// and now use it with a YCCK converting color space.
PDFImage.this.colorSpace = new PDFColorSpace(new YCCKColorSpace(colorSpace.getColorSpace()));
// re-calculate the color model since the color space has changed
cm = PDFImage.this.createColorModel();
return new BufferedImage(
cm,
Raster.createWritableRaster(raster.getSampleModel(), raster.getDataBuffer(), null),
true,
null);
}
}
}
}
}
throw new IIOException("Not a YCCK image");
} else {
if (param != null && param.getDestination() != null) {
// if we've already set up a destination image then we'll use it
return jpegReader.read(0, param);
} else {
// otherwise we'll create a new buffered image with the
// desired color model
return new BufferedImage(cm, jpegReader.read(0, param).getRaster(), true, null);
}
}
}
/**
* Get a named child node
* @param aNode the node
* @param aChildName the name of the child node
* @return the first direct child node with that name or null
* if it doesn't exist
*/
private Node getChild(Node aNode, String aChildName) {
for (int i = 0; i < aNode.getChildNodes().getLength(); ++i) {
final Node child = aNode.getChildNodes().item(i);
if (child.getNodeName().equals(aChildName)) {
return child;
}
}
return null;
}
}
/**
* <p>Parse the image stream into a buffered image. Note that this is
* guaranteed to be called after all the other setXXX methods have been
* called.</p>
*
* <p>NOTE: the color convolving is extremely slow on large images.
* It would be good to see if it could be moved out into the rendering
* phases, where we might be able to scale the image down first.</p
*
* @param data the data when already completely filtered and uncompressed
* @param jpegData a byte buffer if data still requiring the DCDTecode filter
* is being used
*/
protected BufferedImage parseData(byte[] data, ByteBuffer jpegData) throws IOException {
// String hex;
// String name;
// synchronized (System.out) {
// System.out.println("\n\n" + name + ": " + data.length);
// for (int i = 0; i < data.length; i++) {
// hex = "0x" + Integer.toHexString(0xFF & data[i]);
// System.out.print(hex);
// if (i < data.length - 1) {
// System.out.print(", ");
// }
// if ((i + 1) % 25 == 0) {
// System.out.print("\n");
// }
// }
// System.out.println("\n");
// System.out.flush();
// }
// pick a color model, based on the number of components and
// bits per component
ColorModel cm = createColorModel();
BufferedImage bi = null;
if (jpegData != null) {
// Use imageio to decode the JPEG into
// a BufferedImage. Perhaps JAI will be installed
// so that decodes will be faster and better supported
// TODO - strictly speaking, the application of the YUV->RGB
// transformation when reading JPEGs does not adhere to the spec.
// We're just going to let java read this in - as it is, the standard
// jpeg reader looks for the specific Adobe marker header so that
// it may apply the transform, so that's good. If that marker
// isn't present, then it also applies a number of other heuristics
// to determine whether the transform should be applied.
// (http://java.sun.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html)
// In practice, it probably almost always does the right thing here,
// though note that the present or default value of the ColorTransform
// dictionary entry is not being observed, so there is scope for
// error. Hopefully the JAI reader does the same.
// We might need to attempt this with multiple readers, so let's
// remember where the jpeg data starts
jpegData.mark();
JpegDecoder decoder = new JpegDecoder(jpegData, cm);
IIOException decodeEx = null;
try {
bi = decoder.decode();
} catch (IIOException e) {
decodeEx = e;
// The native readers weren't able to process the image.
// One common situation is that the image is YCCK encoded,
// which isn't supported by the default jpeg readers.
// We've got a work-around we can attempt, though:
decoder.setYcckDecodeMode(true);
try {
bi = decoder.decode();
} catch (IOException e2) {
// It probably wasn't the YCCK issue! We'll drop
// through and allow the initial exception to be reported
}
}
// the decoder may have requested installation of a new color model
cm = decoder.getColorModel();
// make these immediately unreachable, as the referenced
// jpeg data might be quite large
jpegData = null;
decoder = null;
if (bi == null) {
// This isn't pretty, but it's what's been happening
// previously, so we'll preserve it for the time
// being. At least we'll offer a hint now!
assert decodeEx != null;
throw new IIOException(decodeEx.getMessage() +
". Maybe installing JAI for expanded image format " +
"support would help?", decodeEx);
}
} else {
DataBuffer db = new DataBufferByte(data, data.length);
// create a compatible raster
SampleModel sm =
cm.createCompatibleSampleModel (getWidth (), getHeight ());
WritableRaster raster;
try {
raster =
Raster.createWritableRaster (sm, db, new Point (0, 0));
} catch (RasterFormatException e) {
// this here seems a bit on the odd side. Is this really required,
// or was it compensating for an old bug?
int calculatedLineBits = getWidth() *
getColorSpace().getNumComponents() *
getBitsPerComponent();
int calculatedLineBytes = (calculatedLineBits + 7 / 8);
int calculatedBytes = calculatedLineBytes * getHeight();
if (calculatedBytes > data.length) {
byte[] tempLargerData = new byte[calculatedBytes];
System.arraycopy (data, 0, tempLargerData, 0, data.length);
db = new DataBufferByte (tempLargerData, calculatedBytes);
raster = Raster.createWritableRaster(sm, db, new Point(0, 0));
} else {
throw e;
}
}
/*
* Workaround for a bug on the Mac -- a class cast exception in
* drawImage() due to the wrong data buffer type (?)
*/
if (cm instanceof IndexColorModel) {
IndexColorModel icm = (IndexColorModel) cm;
// choose the image type based on the size
int type = BufferedImage.TYPE_BYTE_BINARY;
if (getBitsPerComponent() == 8) {
type = BufferedImage.TYPE_BYTE_INDEXED;
}
// create the image with an explicit indexed color model.
bi = new BufferedImage(getWidth(), getHeight(), type, icm);
// set the data explicitly as well
bi.setData(raster);
} else {
bi = new BufferedImage(cm, raster, true, null);
}
}
ColorSpace cs = cm.getColorSpace();
ColorSpace rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB);
if (isGreyscale(cs) && bpc <= 8 && getDecode() == null && jpegData == null) {
bi = convertGreyscaleToArgb(data, bi);
} else if (!isImageMask() && cs instanceof ICC_ColorSpace && !cs.equals(rgbCS)
&& !Boolean.getBoolean("PDFRenderer.avoidColorConvertOp")) {
// users can use the PDFRenderer.avoidColorConvertOp property to avoid
// use of this color convert op which may segfault on some platforms
// due to a variety of problems related to thread safety and
// the native cmm library underlying this conversioon op, e.g.,
// https://forums.oracle.com/forums/thread.jspa?threadID=1261882&tstart=225&messageID=5356357
// (Unix platforms seem the most affected)
// If the system is bug-free, though, this does make use
// of native libraries and sees a not insignificant speed-up,
// though it's still not exactly fast. If we don't run this op
// now, it's performed at some later stage, but without using
// the native code
ColorConvertOp op = new ColorConvertOp(cs, rgbCS, null);
BufferedImage converted = new BufferedImage(getWidth(),
getHeight(), BufferedImage.TYPE_INT_ARGB);
bi = op.filter(bi, converted);
}
// add in the alpha data supplied by the SMask, if any
PDFImage sMaskImage = getSMask();
if (sMaskImage != null) {
BufferedImage si = sMaskImage.getImage();
BufferedImage outImage = new BufferedImage(getWidth(),
getHeight(), BufferedImage.TYPE_INT_ARGB);
int[] srcArray = new int[width];
int[] maskArray = new int[width];
for (int i = 0; i < height; i++) {
bi.getRGB(0, i, width, 1, srcArray, 0, width);
si.getRGB(0, i, width, 1, maskArray, 0, width);
for (int j = 0; j < width; j++) {
int ac = 0xff000000;
maskArray[j] = ((maskArray[j] & 0xff) << 24) | (srcArray[j] & ~ac);
}
outImage.setRGB(0, i, width, 1, maskArray, 0, width);
}
bi = outImage;
}
return (bi);
}
private boolean isGreyscale(ColorSpace aCs)
{
return aCs == PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_GRAY).
getColorSpace();
}
private BufferedImage convertGreyscaleToArgb(byte[] data, BufferedImage bi)
{
// we use an optimised greyscale colour conversion, as with scanned
// greyscale/mono documents consisting of nothing but page-size
// images, using the ICC converter is perhaps 15 times slower than this
// method. Using an example scanned, mainly monochrome document, on this
// developer's machine pages took an average of 3s to render using the
// ICC converter filter, and around 115ms using this method. We use
// pre-calculated tables generated using the ICC converter to map between
// each possible greyscale value and its desired value in sRGB.
// We also try to avoid going through SampleModels, WritableRasters or
// BufferedImages as that takes about 3 times as long.
final int[] convertedPixels = new int[getWidth() * getHeight()];
final WritableRaster r = bi.getRaster();
int i = 0;
final int[] greyToArgbMap = getGreyToArgbMap(bpc);
if (bpc == 1) {
int calculatedLineBytes = (getWidth() + 7) / 8;
int rowStartByteIndex;
// avoid hitting the WritableRaster for the common 1 bpc case
if (greyToArgbMap[0] == 0 && greyToArgbMap[1] == 0xFFFFFFFF) {
// optimisation for common case of a direct map to full white
// and black, using bit twiddling instead of consulting the
// greyToArgb map
for (int y = 0; y < getHeight(); ++y) {
// each row is byte-aligned
rowStartByteIndex = y * calculatedLineBytes;
for (int x = 0; x < getWidth(); ++x) {
final byte b = data[rowStartByteIndex + x / 8];
final int white = b >> (7 - (x & 7)) & 1;
// if white == 0, white - 1 will be 0xFFFFFFFF,
// which when xored with 0xFFFFFF will produce 0
// if white == 1, white - 1 will be 0,
// which when xored with 0xFFFFFF will produce 0xFFFFFF
// (ignoring the top two bytes, which are always set high anyway)
convertedPixels[i] = 0xFF000000 | ((white - 1) ^ 0xFFFFFF);
++i;
}
}
} else {
// 1 bpc case where we can't bit-twiddle and need to consult
// the map
for (int y = 0; y < getHeight(); ++y) {
rowStartByteIndex = y * calculatedLineBytes;
for (int x = 0; x < getWidth(); ++x) {
final byte b = data[rowStartByteIndex + x / 8];
final int val = b >> (7 - (x & 7)) & 1;
convertedPixels[i] = greyToArgbMap[val];
++i;
}
}
}
} else {
for (int y = 0; y < getHeight(); ++y) {
for (int x = 0; x < getWidth(); ++x) {
final int greyscale = r.getSample(x, y, 0);
convertedPixels[i] = greyToArgbMap[greyscale];
++i;
}
}
}
final ColorModel ccm = ColorModel.getRGBdefault();
return new BufferedImage(
ccm,
Raster.createPackedRaster(
new DataBufferInt(
convertedPixels,
convertedPixels.length),
getWidth(), getHeight(),
getWidth(), ((PackedColorModel)ccm).getMasks(),
null),
false,
null);
}
/**
* Get the image's width
*/
public int getWidth() {
return width;
}
/**
* Set the image's width
*/
protected void setWidth(int width) {
this.width = width;
}
/**
* Get the image's height
*/
public int getHeight() {
return height;
}
/**
* Set the image's height
*/
protected void setHeight(int height) {
this.height = height;
}
/**
* set the color key mask. It is an array of start/end entries
* to indicate ranges of color indicies that should be masked out.
*
* @param maskArrayObject
*/
private void setColorKeyMask(PDFObject maskArrayObject) throws IOException {
PDFObject[] maskObjects = maskArrayObject.getArray();
colorKeyMask = null;
int[] masks = new int[maskObjects.length];
for (int i = 0; i < masks.length; i++) {
masks[i] = maskObjects[i].getIntValue();
}
colorKeyMask = masks;
}
/**
* Get the colorspace associated with this image, or null if there
* isn't one
*/
protected PDFColorSpace getColorSpace() {
return colorSpace;
}
/**
* Set the colorspace associated with this image
*/
protected void setColorSpace(PDFColorSpace colorSpace) {
this.colorSpace = colorSpace;
}
/**
* Get the number of bits per component sample
*/
protected int getBitsPerComponent() {
return bpc;
}
/**
* Set the number of bits per component sample
*/
protected void setBitsPerComponent(int bpc) {
this.bpc = bpc;
}
/**
* Return whether or not this is an image mask
*/
public boolean isImageMask() {
return imageMask;
}
/**
* Set whether or not this is an image mask
*/
public void setImageMask(boolean imageMask) {
this.imageMask = imageMask;
}
/**
* Return the soft mask associated with this image
*/
public PDFImage getSMask() {
return sMask;
}
/**
* Set the soft mask image
*/
protected void setSMask(PDFImage sMask) {
this.sMask = sMask;
}
/**
* Get the decode array
*/
protected float[] getDecode() {
return decode;
}
/**
* Set the decode array
*/
protected void setDecode(float[] decode) {
float max = (1 << getBitsPerComponent()) - 1;
this.decode = decode;
this.decodeCoefficients = new float[decode.length / 2];
this.decodeMins = new float[decode.length / 2];
for (int i = 0; i < decode.length; i += 2) {
decodeMins[i/2] = decode[i];
decodeCoefficients[i/2] = (decode[i + 1] - decode[i]) / max;
}
}
/**
* get a Java ColorModel consistent with the current color space,
* number of bits per component and decode array
*
* @param bpc the number of bits per component
*/
private ColorModel createColorModel() {
PDFColorSpace cs = getColorSpace();
if (cs instanceof IndexedColor) {
IndexedColor ics = (IndexedColor) cs;
byte[] components = ics.getColorComponents();
int num = ics.getCount();
// process the decode array
if (decode != null) {
byte[] normComps = new byte[components.length];
// move the components array around
for (int i = 0; i < num; i++) {
byte[] orig = new byte[1];
orig[0] = (byte) i;
float[] res = normalize(orig, null, 0);
int idx = (int) res[0];
normComps[i * 3] = components[idx * 3];
normComps[(i * 3) + 1] = components[(idx * 3) + 1];
normComps[(i * 3) + 2] = components[(idx * 3) + 2];
}
components = normComps;
}
// make sure the size of the components array is 2 ^ numBits
// since if it's not, Java will complain
int correctCount = 1 << getBitsPerComponent();
if (correctCount < num) {
byte[] fewerComps = new byte[correctCount * 3];
System.arraycopy(components, 0, fewerComps, 0, correctCount * 3);
components = fewerComps;
num = correctCount;
}
if (colorKeyMask == null || colorKeyMask.length == 0) {
return new IndexColorModel(getBitsPerComponent(), num, components,
0, false);
} else {
byte[] aComps = new byte[num * 4];
int idx = 0;
for (int i = 0; i < num; i++) {
aComps[idx++] = components[(i * 3)];
aComps[idx++] = components[(i * 3) + 1];
aComps[idx++] = components[(i * 3) + 2];
aComps[idx++] = (byte) 0xFF;
}
for (int i = 0; i < colorKeyMask.length; i += 2) {
for (int j = colorKeyMask[i]; j <= colorKeyMask[i + 1]; j++) {
aComps[(j * 4) + 3] = 0; // make transparent
}
}
return new IndexColorModel(getBitsPerComponent(), num, aComps,
0, true);
}
} else {
int[] bits = new int[cs.getNumComponents()];
for (int i = 0; i < bits.length; i++) {
bits[i] = getBitsPerComponent();
}
return decode != null ?
new DecodeComponentColorModel(cs.getColorSpace(), bits) :
new PdfComponentColorModel(cs.getColorSpace(), bits);
}
}
private ColorModel createColorModel(PDFColorSpace cs) {
if (cs instanceof IndexedColor) {
IndexedColor ics = (IndexedColor) cs;
byte[] components = ics.getColorComponents();
int num = ics.getCount();
// process the decode array
if (decode != null) {
byte[] normComps = new byte[components.length];
// move the components array around
for (int i = 0; i < num; i++) {
byte[] orig = new byte[1];
orig[0] = (byte) i;
float[] res = normalize(orig, null, 0);
int idx = (int) res[0];
normComps[i * 3] = components[idx * 3];
normComps[(i * 3) + 1] = components[(idx * 3) + 1];
normComps[(i * 3) + 2] = components[(idx * 3) + 2];
}
components = normComps;
}
// make sure the size of the components array is 2 ^ numBits
// since if it's not, Java will complain
int correctCount = 1 << getBitsPerComponent();
if (correctCount < num) {
byte[] fewerComps = new byte[correctCount * 3];
System.arraycopy(components, 0, fewerComps, 0, correctCount * 3);
components = fewerComps;
num = correctCount;
}
if (colorKeyMask == null || colorKeyMask.length == 0) {
return new IndexColorModel(getBitsPerComponent(), num, components,
0, false);
} else {
byte[] aComps = new byte[num * 4];
int idx = 0;
for (int i = 0; i < num; i++) {
aComps[idx++] = components[(i * 3)];
aComps[idx++] = components[(i * 3) + 1];
aComps[idx++] = components[(i * 3) + 2];
aComps[idx++] = (byte) 0xFF;
}
for (int i = 0; i < colorKeyMask.length; i += 2) {
for (int j = colorKeyMask[i]; j <= colorKeyMask[i + 1]; j++) {
aComps[(j * 4) + 3] = 0; // make transparent
}
}
return new IndexColorModel(getBitsPerComponent(), num, aComps,
0, true);
}
} else {
int[] bits = new int[cs.getNumComponents()];
for (int i = 0; i < bits.length; i++) {
bits[i] = getBitsPerComponent();
}
return decode != null ?
new DecodeComponentColorModel(cs.getColorSpace(), bits) :
new PdfComponentColorModel(cs.getColorSpace(), bits);
}
}
/**
* Normalize an array of values to match the decode array
*/
private float[] normalize(byte[] pixels, float[] normComponents,
int normOffset) {
if (normComponents == null) {
normComponents = new float[normOffset + pixels.length];
}
// trivial loop unroll - saves a little time
switch (pixels.length) {
case 4:
normComponents[normOffset + 3] = decodeMins[3] + (float)(pixels[3] & 0xFF) * decodeCoefficients[3];
case 3:
normComponents[normOffset + 2] = decodeMins[2] + (float)(pixels[2] & 0xFF) * decodeCoefficients[2];
case 2:
normComponents[normOffset + 1] = decodeMins[1] + (float)(pixels[1] & 0xFF) * decodeCoefficients[1];
case 1:
normComponents[normOffset ] = decodeMins[0] + (float)(pixels[0] & 0xFF) * decodeCoefficients[0];
break;
default:
throw new IllegalArgumentException("Someone needs to add support for more than 4 components");
}
return normComponents;
}
/**
* A wrapper for ComponentColorSpace which normalizes based on the
* decode array.
*/
static class PdfComponentColorModel extends ComponentColorModel {
int bitsPerComponent;
public PdfComponentColorModel(ColorSpace cs, int[] bpc) {
super(cs, bpc, false, false, Transparency.OPAQUE,
DataBuffer.TYPE_BYTE);
pixel_bits = bpc.length * bpc[0];
this.bitsPerComponent = bpc[0];
}
@Override
public SampleModel createCompatibleSampleModel(int width, int height) {
if (bitsPerComponent >= 8) {
assert bitsPerComponent == 8 || bitsPerComponent == 16;
final int numComponents = getNumComponents();
int[] bandOffsets = new int[numComponents];
for (int i=0; i < numComponents; i++) {
bandOffsets[i] = i;
}
return new PixelInterleavedSampleModel(
getTransferType(), width, height,
numComponents,
width * numComponents,
bandOffsets);
} else {
switch (getPixelSize()) {
case 1:
case 2:
case 4:
// pixels don't span byte boundaries, so we can use the standard multi pixel
// packing, which offers a slight performance advantage over the other sample
// model, which must consider such cases. Given that sample model interactions
// can dominate processing, this small distinction is worthwhile
return new MultiPixelPackedSampleModel(getTransferType(),
width,
height,
getPixelSize());
default:
// pixels will cross byte boundaries
assert getTransferType() == DataBuffer.TYPE_BYTE;
return new PdfSubByteSampleModel(width, height, getNumComponents(), bitsPerComponent);
}
}
}
@Override
public boolean isCompatibleRaster(Raster raster) {
if (bitsPerComponent < 8 || getNumComponents() == 1) {
SampleModel sm = raster.getSampleModel();
return sm.getSampleSize(0) == bitsPerComponent;
}
return super.isCompatibleRaster(raster);
}
}
class DecodeComponentColorModel extends PdfComponentColorModel
{
DecodeComponentColorModel(ColorSpace cs, int[] bpc)
{
super(cs, bpc);
}
public int getRGB(Object inData) {
float[] norm = getNormalizedComponents(inData, null, 0);
// super-class wants to do a (VERY expensive!) colorspace conversion
// here, but we'll ignore it - I think we'll already have the
// colour space converted.
float[] rgb = norm;//this.colorSpace.toRGB(norm);
// Note that getNormalizedComponents returns non-premult values
return (this.getAlpha(inData) << 24)
| (((int) (rgb[0] * 255.0f + 0.5f)) << 16)
| (((int) (rgb[1] * 255.0f + 0.5f)) << 8)
| (((int) (rgb[2] * 255.0f + 0.5f)));
}
@Override
public float[] getNormalizedComponents(Object pixel,
float[] normComponents, int normOffset) {
return normalize((byte[]) pixel, normComponents, normOffset);
}
}
}