
org.openpdf.renderer.PDFImage Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openpdf-renderer Show documentation
Show all versions of openpdf-renderer Show documentation
PDF renderer implementation supporting the subset of PDF 1.4 specification.
The newest version!
/*
* 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 org.openpdf.renderer;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.PackedColorModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RasterFormatException;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
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 javax.imageio.stream.ImageInputStream;
import org.openpdf.renderer.colorspace.AlternateColorSpace;
import org.openpdf.renderer.colorspace.IndexedColor;
import org.openpdf.renderer.colorspace.PDFColorSpace;
import org.openpdf.renderer.decode.PDFDecoder;
import org.openpdf.renderer.function.FunctionType0;
import org.w3c.dom.Attr;
import org.w3c.dom.Node;
import org.openpdf.renderer.colorspace.YCCKColorSpace;
/**
* Encapsulates a PDF Image
*/
public class PDFImage {
private static int[][] GREY_TO_ARGB = new int[8][];
/**
* 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;
/** the actual image data */
private final PDFObject imageObj;
/** true if the image is in encoded in JPEG*/
private final boolean jpegDecode;
/**
* Create an instance of a PDFImage
* @throws IOException if {@link PDFDecoder} throws one while evaluating if the image is a Jpeg
*/
protected PDFImage(PDFObject imageObj) throws IOException {
this.imageObj = imageObj;
this.jpegDecode = PDFDecoder.isLastFilter(imageObj, PDFDecoder.DCT_FILTERS);
}
/**
* 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
* @param useAsSMask
* - flag for switching colors in case image is used as sMask
* internally this is needed for handling transparency in smask
* images.
*/
public static PDFImage createImage(PDFObject obj, Map resources, boolean useAsSMask)
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 [email protected]] - default value od Decode
// according to PDF spec. is [0, 1]
// so the color arry should be:
// [PATCHED by XOND] - switched colors in case the image is used as
// SMask for another image, otherwise transparency isn't
// handled correctly.
Color[] colors = useAsSMask ? new Color[] { Color.WHITE, Color.BLACK }
: new Color[] { Color.BLACK, Color.WHITE };
PDFObject imageMaskDecode = obj.getDictRef("Decode");
if (imageMaskDecode != null) {
PDFObject[] decodeArray = imageMaskDecode.getArray();
float decode0 = decodeArray[0].getFloatValue();
if (decode0 == 1.0f) {
colors = useAsSMask ? new Color[] { Color.BLACK, Color.WHITE }
: new Color[] { Color.WHITE, Color.BLACK };
}
/*
* float[] decode = new float[decodeArray.length]; for (int i =
* 0; i < decodeArray.length; i++) { decode[i] =
* decodeArray[i].getFloatValue(); } image.setDecode(decode);
*/
}
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.)
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, true);
image.setSMask(sMaskImage);
} catch (IOException ex) {
PDFDebugger.debug("ERROR: there was a problem parsing the mask for this object");
PDFDebugger.dump(obj);
BaseWatchable.getErrorHandler().publishException(ex);
}
} 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) {
PDFDebugger.debug("ERROR: there was a problem parsing the color mask for this object");
PDFDebugger.dump(obj);
BaseWatchable.getErrorHandler().publishException(ex);
}
}
}
}
return image;
}
/**
* Get the image that this PDFImage generates.
*
* @return a buffered image containing the decoded image data
* @throws PDFImageParseException
*/
public BufferedImage getImage() throws PDFImageParseException {
try {
BufferedImage bi = (BufferedImage) this.imageObj.getCache();
if (bi == null) {
byte[] data = imageObj.getStream();
ByteBuffer jpegBytes = null;
if (this.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);
}
// parse the stream data into an actual image
bi = parseData(data, jpegBytes);
this.imageObj.setCache(bi);
}
return bi;
} catch (IOException ioe) {
// let the caller know that there was a problem parsing the image
throw new PDFImageParseException("Error reading image: "+ioe.getMessage(), ioe);
}
}
/**
*
* 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.
*
*
*
* 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.
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);
IOException decodeEx = null;
try {
bi = decoder.decode();
} catch (IOException e) {
decodeEx = e;
// The native readers weren't able to process the image.
// One common situation is that the image is YCCK/CMYK encoded,
// which isn't supported by the default jpeg readers.
// We've got a work-around we can attempt, though:
decoder.ycckcmykDecodeMode(true);
try {
bi = decoder.decode();
} catch (IOException e2) {
// It probably wasn't the YCCK/CMYK issue!
// try the "old" implementation
bi = parseData(data, null);
return bi;
}
}
// 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 IOException(decodeEx.getMessage() + ". Maybe installing JAI for expanded image format "
+ "support would help?", decodeEx);
}
} else {
// create the data buffer
DataBuffer db = new DataBufferByte(data, data.length);
// pick a color model, based on the number of components and
// bits per component
cm = getColorModel();
// 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) {
int tempExpectedSize = getWidth() * getHeight() * getColorSpace().getNumComponents()
* Math.max(8, getBitsPerComponent()) / 8;
if (tempExpectedSize < 3) {
tempExpectedSize = 3;
}
if (tempExpectedSize > data.length) {
byte[] tempLargerData = new byte[tempExpectedSize];
System.arraycopy(data, 0, tempLargerData, 0, data.length);
db = new DataBufferByte(tempLargerData, tempExpectedSize);
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 (?)
*/
bi = null;
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 if (cm.getPixelSize() == 1 && cm.getNumComponents() == 1) {
// If the image is black and white only, convert it into
// BYTE_GRAY
// format
// This is a lot faster compared to just drawing the original
// image
// Are pixels decoded?
int[] cc = new int[] { 0, 1 };
PDFObject o = imageObj.getDictRef("Decode");
if (o != null && o.getAt(0) != null) {
cc[0] = o.getAt(0).getIntValue();
cc[1] = o.getAt(1).getIntValue();
}
final byte[] ncc = new byte[] { (byte) -cc[0], (byte) -cc[1] };
bi = biColorToGrayscale(raster, ncc);
// Return when there is no SMask
if (getSMask() == null)
return bi;
} else {
// Raster is already in a format which is supported by Java2D,
// such as RGB or Gray.
bi = new BufferedImage(cm, raster, true, null);
}
}
// hack to avoid *very* slow conversion
ColorSpace cs = cm.getColorSpace();
ColorSpace rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB);
if (isGreyscale(cs) && bpc <= 8 && getDecode() == null && jpegData == null
&& Configuration.getInstance().isConvertGreyscaleImagesToArgb()) {
bi = convertGreyscaleToArgb(data, bi);
} else if (!isImageMask() && cs instanceof ICC_ColorSpace && !cs.equals(rgbCS)
&& !Configuration.getInstance().isAvoidColorConvertOp()) {
ColorConvertOp op = new ColorConvertOp(cs, rgbCS, null);
BufferedImage converted = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
bi = op.filter(bi, converted);
}
else if (cs.getType() == ColorSpace.TYPE_CMYK) {
// convert to ARGB for faster drawing without ColorConvertOp
BufferedImage converted = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = converted.createGraphics();
graphics.drawImage(bi,0,0,null);
graphics.dispose();
bi = converted;
}
// add in the alpha data supplied by the SMask, if any
PDFImage sMaskImage = getSMask();
if (sMaskImage != null) {
BufferedImage si = null;
try {
int w = bi.getWidth();
int h = bi.getHeight();
// if the bitmap is only a few pixels it just defines the color
boolean maskOnly = (w <= 2);
if (maskOnly) {
// use size of mask
si = sMaskImage.getImage();
w = si.getWidth();
h = si.getHeight();
}
else if (sMaskImage.getHeight() != h && sMaskImage.getWidth() != w) {
// in case the two images do not have the same size, scale
if (sMaskImage.getHeight()*sMaskImage.getWidth() > w*h) {
// upscale image
si = sMaskImage.getImage();
w = si.getWidth();
h = si.getHeight();
int hints = Image.SCALE_FAST;
Image scaledInstance = bi.getScaledInstance(w, h, hints );
bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics graphics = bi.createGraphics();
graphics.drawImage(scaledInstance, 0, 0, null);
graphics.dispose();
}
else {
// upscale mask
si = scaleSMaskImage(sMaskImage);
}
}
else {
si = sMaskImage.getImage();
}
PDFDebugger.debugImage(si, "smask" + this.imageObj.getObjNum());
BufferedImage outImage = new BufferedImage(w,h, BufferedImage.TYPE_INT_ARGB);
PDFDebugger.debugImage(si, "outImage" + this.imageObj.getObjNum());
int[] srcArray = new int[w];
int[] maskArray = new int[w];
for (int i = 0; i < h; i++) {
if (maskOnly) {
// use first pixel color from image
Arrays.fill(srcArray, bi.getRGB(0,0));
}
else {
// pixel row from image
bi.getRGB(0, i, w, 1, srcArray, 0, w);
}
// pixel row from mask
si.getRGB(0, i, w, 1, maskArray, 0, w);
for (int j = 0; j < w; j++) {
int ac = 0xff000000;
// alpha from mask with color from image
maskArray[j] = ((maskArray[j] & 0xff) << 24) | (srcArray[j] & ~ac);
}
// write pixel row
outImage.setRGB(0, i, w, 1, maskArray, 0, w);
}
bi = outImage;
} catch (PDFImageParseException e) {
PDFDebugger.debug("Error parsing sMask image caused by:" + e.getMessage(), 100);
}
}
PDFDebugger.debugImage(bi, "result" + this.imageObj.getObjNum());
return bi;
}
/**
* Scale the softmask image to the size of the actual image
*
* @param sMaskImage
* @return
* @throws PDFImageParseException
*/
private BufferedImage scaleSMaskImage(PDFImage sMaskImage) throws PDFImageParseException {
BufferedImage before = sMaskImage.getImage();
int w = before.getWidth();
int h = before.getHeight();
if (PDFDebugger.DEBUG_IMAGES) {
PDFDebugger.debug("Scaling image from " + w + "/" + h + " to " + this.width + "/" + this.height);
}
BufferedImage after = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(((double) this.width / w), ((double) this.height / h));
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
return scaleOp.filter(before, after);
}
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);
}
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;
}
/**
* Creates a new image of type {@link TYPE_BYTE_GRAY} which represents the
* given raster
*
* @param raster
* Raster of an image with just two colors, bitwise encoded
* @param ncc
* Array with two entries that describe the corresponding gray
* values
*/
private BufferedImage biColorToGrayscale(final WritableRaster raster, final byte[] ncc) {
final byte[] bufferO = ((DataBufferByte) raster.getDataBuffer()).getData();
BufferedImage converted = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_BYTE_GRAY);
byte[] buffer = ((DataBufferByte) converted.getRaster().getDataBuffer()).getData();
int i = 0;
final int height = converted.getHeight();
final int width = converted.getWidth();
for (int y = 0; y < height; y++) {
int base = y * width + 7;
if ((y + 1) * width < buffer.length) {
for (int x = 0; x < width; x += 8) {
final byte bits = bufferO[i];
i++;
for (byte j = 7; j >= 0; j--) {
if (buffer.length <= (base - j)) {
break;
}
final int c = ((bits >>> j) & 1);
buffer[base - j] = ncc[c];
}
base += 8;
}
} else {
for (int x = 0; x < width; x += 8) {
final byte bits = bufferO[i];
i++;
for (byte j = 7; j >= 0; j--) {
if (base - j >= buffer.length)
break;
buffer[base - j] = ncc[((bits >>> j) & 1)];
}
base += 8;
}
}
}
return converted;
}
/**
* Get the image's width
*/
public int getWidth() {
return this.width;
}
/**
* Set the image's width
*/
protected void setWidth(int width) {
this.width = width;
}
/**
* Get the image's height
*/
public int getHeight() {
return this.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();
this.colorKeyMask = null;
int[] masks = new int[maskObjects.length];
for (int i = 0; i < masks.length; i++) {
masks[i] = maskObjects[i].getIntValue();
}
this.colorKeyMask = masks;
}
/**
* Get the colorspace associated with this image, or null if there isn't one
*/
protected PDFColorSpace getColorSpace() {
return this.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 this.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 this.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 this.sMask;
}
/**
* Set the soft mask image
*/
protected void setSMask(PDFImage sMask) {
this.sMask = sMask;
}
/**
* Get the decode array
*/
protected float[] getDecode() {
return this.decode;
}
/**
* Set the decode array
*/
protected void setDecode(float[] decode) {
this.decode = decode;
}
/**
* 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 getColorModel() {
PDFColorSpace cs = getColorSpace();
if (cs instanceof IndexedColor) {
IndexedColor ics = (IndexedColor) cs;
byte[] components = ics.getColorComponents();
int num = ics.getCount();
// process the decode array
if (this.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 (this.colorKeyMask == null || this.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 < this.colorKeyMask.length; i += 2) {
for (int j = this.colorKeyMask[i]; j <= this.colorKeyMask[i + 1]; j++) {
aComps[(j * 4) + 3] = 0; // make transparent
}
}
return new IndexColorModel(getBitsPerComponent(), num, aComps, 0, true);
}
} else if (cs instanceof AlternateColorSpace) {
// ColorSpace altCS = new AltColorSpace(((AlternateColorSpace)
// cs).getFunktion(), cs.getColorSpace());
ColorSpace altCS = cs.getColorSpace();
int[] bits = new int[altCS.getNumComponents()];
for (int i = 0; i < bits.length; i++) {
bits[i] = getBitsPerComponent();
}
return new DecodeComponentColorModel(altCS, bits);
} else {
// If the image is a JPEG, then CMYK color space has been converted to RGB in DCTDecode
if (this.jpegDecode && cs.getColorSpace().getType() == ColorSpace.TYPE_CMYK) {
ColorSpace rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB);
int[] bits = new int[rgbCS.getNumComponents()];
for (int i = 0; i < bits.length; i++) {
bits[i] = getBitsPerComponent();
}
return new DecodeComponentColorModel(rgbCS, bits);
}
ColorSpace colorSpace = cs.getColorSpace();
int[] bits = new int[colorSpace.getNumComponents()];
for (int i = 0; i < bits.length; i++){
bits[i] = getBitsPerComponent();
}
return new DecodeComponentColorModel(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];
}
float[] decodeArray = getDecode();
for (int i = 0; i < pixels.length; i++) {
int val = pixels[i] & 0xff;
int pow = ((int) Math.pow(2, getBitsPerComponent())) - 1;
float ymin = decodeArray[i * 2];
float ymax = decodeArray[(i * 2) + 1];
normComponents[normOffset + i] = FunctionType0.interpolate(val, 0, pow, ymin, ymax);
}
return normComponents;
}
/**
* A wrapper for ComponentColorSpace which normalizes based on the decode
* array.
*/
class DecodeComponentColorModel extends ComponentColorModel {
public DecodeComponentColorModel(ColorSpace cs, int[] bpc) {
super(cs, bpc, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
if (bpc != null) {
this.pixel_bits = bpc.length * bpc[0];
}
}
@Override
public SampleModel createCompatibleSampleModel(int width, int height) {
// workaround -- create a MultiPixelPackedSample models for
// single-sample, less than 8bpp color models
if (getNumComponents() == 1 && getPixelSize() < 8) {
return new MultiPixelPackedSampleModel(getTransferType(), width, height, getPixelSize());
}
return super.createCompatibleSampleModel(width, height);
}
@Override
public boolean isCompatibleRaster(Raster raster) {
if (getNumComponents() == 1 && getPixelSize() < 8) {
SampleModel sm = raster.getSampleModel();
if (sm instanceof MultiPixelPackedSampleModel) {
return (sm.getSampleSize(0) == getPixelSize());
} else {
return false;
}
}
return super.isCompatibleRaster(raster);
}
@Override
public float[] getNormalizedComponents(Object pixel, float[] normComponents, int normOffset) {
if (getDecode() == null) {
return super.getNormalizedComponents(pixel, normComponents, normOffset);
}
return normalize((byte[]) pixel, normComponents, normOffset);
}
}
/**
* 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);
}
}
/**
* 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 final ByteBuffer jpegData;
/** The color model employed */
private ColorModel cm;
/** Whether the YCCK/CMYK decode work-around should be used */
private boolean ycckcmykDecodeMode = 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/CMYK 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 ycckcmykDecodeMode
*/
public void ycckcmykDecodeMode(boolean ycckcmykDecodeMode) {
this.ycckcmykDecodeMode = ycckcmykDecodeMode;
}
/**
* 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 {
byte[] jpegBytes = jpegData.hasArray()
? Arrays.copyOfRange(jpegData.array(), jpegData.position(), jpegData.limit())
: ByteBuffer.allocate(jpegData.remaining()).put(jpegData.duplicate()).array();
ImageReadParam readParam = null;
if (getDecode() != null) {
readParam = new ImageReadParam();
SampleModel sm = cm.createCompatibleSampleModel(getWidth(), getHeight());
WritableRaster raster = Raster.createWritableRaster(sm, new Point(0, 0));
readParam.setDestination(new BufferedImage(cm, raster, true, null));
}
IIOException lastIioEx = null;
for (Iterator it = ImageIO.getImageReadersByFormatName("jpeg"); it.hasNext(); ) {
ImageReader reader = it.next();
try (
ByteArrayInputStream bais = new ByteArrayInputStream(jpegBytes);
ImageInputStream iis = ImageIO.createImageInputStream(bais)
) {
reader.setInput(iis, true, false);
return readImage(reader, readParam);
} catch (IIOException e) {
lastIioEx = e; // try next reader
} catch (Exception e) {
throw new IIOException("Internal reader error?", e);
} finally {
reader.dispose();
}
}
throw lastIioEx != null ? lastIioEx : new IIOException("No suitable JPEG reader found.");
}
private BufferedImage readImage(ImageReader jpegReader, ImageReadParam param) throws IOException {
if (ycckcmykDecodeMode) {
// 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) {
final String typeName = csTypeNameNode.getValue();
final boolean YCCK;
if ((YCCK = "YCCK".equals(typeName)) || "CMYK".equals(typeName)) {
// If it's a YCCK image, then 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
//
// If it's a CMYK image - just raster it
// in existing CMYK color space
// first make sure we can get the
// unadjusted raster
final Raster raster = jpegReader.readRaster(0, param);
if (YCCK) {
// 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("Neither YCCK nor CMYK 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);
BufferedImage bi = jpegReader.read(0, param);
try {
return new BufferedImage(cm, bi.getRaster(), true, null);
} catch (IllegalArgumentException raster_ByteInterleavedRaster) {
BufferedImage bi2 = new BufferedImage(bi.getWidth(), bi.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED,
new IndexColorModel(8, 1, new byte[] { 0 }, new byte[] { 0 }, new byte[] { 0 }, 0));
cm = bi2.getColorModel();
return bi2;
}
}
}
}
/**
* 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;
}
}
/**
* 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);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy