/*
* Copyright 2006-2017 ICEsoft Technologies Canada Corp.
*
* Licensed 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.icepdf.core.pobjects;
import org.icepdf.core.io.BitStream;
import org.icepdf.core.io.SeekableInputConstrainedWrapper;
import org.icepdf.core.pobjects.filters.CCITTFax;
import org.icepdf.core.pobjects.filters.CCITTFaxDecoder;
import org.icepdf.core.pobjects.graphics.*;
import org.icepdf.core.util.Defs;
import org.icepdf.core.util.Library;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* ImageStream contains image data that is contains in an XObject of subtype
* Image.
*
* @since 5.0
*/
public class ImageStream extends Stream {
private static final Logger logger =
Logger.getLogger(ImageStream.class.toString());
public static final Name TYPE_VALUE = new Name("Image");
public static final Name BITSPERCOMPONENT_KEY = new Name("BitsPerComponent");
public static final Name BPC_KEY = new Name("BPC");
public static final Name DECODE_KEY = new Name("Decode");
public static final Name D_KEY = new Name("D");
public static final Name SMASK_KEY = new Name("SMask");
public static final Name MASK_KEY = new Name("Mask");
public static final Name JBIG2GLOBALS_KEY = new Name("JBIG2Globals");
public static final Name DECODEPARMS_KEY = new Name("DecodeParms");
public static final Name DP_KEY = new Name("DP");
public static final Name K_KEY = new Name("K");
public static final Name ENCODEDBYTEALIGN_KEY = new Name("EncodedByteAlign");
public static final Name COLUMNS_KEY = new Name("Columns");
public static final Name ROWS_KEY = new Name("Rows");
public static final Name BLACKIS1_KEY = new Name("BlackIs1");
// filter names
protected static final String[] CCITTFAX_DECODE_FILTERS = new String[]{"CCITTFaxDecode", "/CCF", "CCF"};
protected static final String[] DCT_DECODE_FILTERS = new String[]{"DCTDecode", "/DCT", "DCT"};
protected static final String[] JBIG2_DECODE_FILTERS = new String[]{"JBIG2Decode"};
protected static final String[] JPX_DECODE_FILTERS = new String[]{"JPXDecode"};
// paper size for rare corner case when ccittfax is missing a dimension.
private static double pageRatio;
// flag the forces jai to be use over our fax decode class.
private static boolean forceJaiccittfax;
private PColorSpace colourSpace;
private final Object colorSpaceAssignmentLock = new Object();
private static boolean isLevigoJBIG2ImageReaderClass;
/**
* Gets the value of the system property "org.icepdf.core.ccittfax.checkParentBlackIs1".
*/
public static boolean CHECK_PARENT_BLACK_IS_1;
static {
// define alternate page size ration w/h, default Legal.
pageRatio =
Defs.sysPropertyDouble("org.icepdf.core.pageRatio",
8.26 / 11.68);
// force jai as the default ccittfax decode.
forceJaiccittfax =
Defs.sysPropertyBoolean("org.icepdf.core.ccittfax.jai",
false);
try {
Class.forName("com.levigo.jbig2.JBIG2ImageReader");
isLevigoJBIG2ImageReaderClass = true;
logger.info("Levigo JBIG2 image library was found on classpath");
} catch (ClassNotFoundException e) {
logger.info("Levigo JBIG2 image library was not found on classpath");
}
CHECK_PARENT_BLACK_IS_1 = Defs.booleanProperty("org.icepdf.core.ccittfax.checkParentBlackIs1", false);
}
private int width;
private int height;
private ImageUtility imageUtility;
/**
* Create a new instance of a Stream.
*
* @param l library containing a hash of all document objects
* @param h HashMap of parameters specific to the Stream object.
* @param streamInputWrapper Accessor to stream byte data
*/
public ImageStream(Library l, HashMap h, SeekableInputConstrainedWrapper streamInputWrapper) {
super(l, h, streamInputWrapper);
init();
}
public ImageStream(Library l, HashMap h, byte[] rawBytes) {
super(l, h, rawBytes);
init();
}
public void init() {
imageUtility = new ImageUtility();
// get dimension of image stream
width = library.getInt(entries, WIDTH_KEY);
height = library.getInt(entries, HEIGHT_KEY);
// PDF-458 corner case/one off for trying to guess the width or height
// of an CCITTfax image that is basically the same use as the page, we
// use the page dimensions to try and determine the page size.
// This will fail miserably if the image isn't full page.
if (height == 0) {
height = (int) ((1 / pageRatio) * width);
} else if (width == 0) {
width = (int) (pageRatio * height);
}
}
public ImageUtility getImageUtility() {
return imageUtility;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
/**
* Gets the image object for the given resource. This method can optionally
* scale an image to reduce the total memory foot print or to increase the
* perceived render quality on screen at low zoom levels.
*
* @param graphicsState graphic state for image or parent form
* @param resources resources containing image reference
* @return new image object
*/
// was synchronized, not think it is needed?
@SuppressWarnings("unchecked")
public synchronized BufferedImage getImage(GraphicsState graphicsState, Resources resources) throws InterruptedException {
// check the pool encase we already parse this image.
if (pObjectReference != null) {
BufferedImage tmp = library.getImagePool().get(pObjectReference);
if (tmp != null) {
return tmp;
}
}
// parse colour space, lock is to insure that getColorSpace()
// will return only after colourSpace has been set.
synchronized (colorSpaceAssignmentLock) {
Object o = entries.get(COLORSPACE_KEY);
if (resources != null && o != null) {
colourSpace = resources.getColorSpace(o);
}
// assume b&w image is no colour space
if (colourSpace == null) {
colourSpace = new DeviceGray(library, null);
}
}
// A flag indicating whether the image shall be treated as an image mask
boolean isImageMask = isImageMask();
// If this flag is true, the value of BitsPerComponent shall be 1 and
// Mask and ColorSpace shall not be specified; unmasked areas shall be
// painted using the current nonstroking colour
int bitsPerComponent = library.getInt(entries, BITSPERCOMPONENT_KEY);
if (isImageMask && bitsPerComponent == 0) {
bitsPerComponent = 1;
}
// check for available memory, get colour space and bit count
// to better estimate size of image in memory
int colorSpaceCompCount = colourSpace.getNumComponents();
// parse decode information
int maxValue = ((int) Math.pow(2, bitsPerComponent)) - 1;
float[] decode = new float[2 * colorSpaceCompCount];
List<Number> decodeVec = (List<Number>) library.getObject(entries, DECODE_KEY);
if (decodeVec == null) {
// add a decode param for each colour channel.
for (int i = 0, j = 0; i < colorSpaceCompCount; i++) {
decode[j++] = 0.0f;
decode[j++] = 1.0f / maxValue;
}
} else {
for (int i = 0, j = 0; i < colorSpaceCompCount; i++) {
float Dmin = decodeVec.get(j).floatValue();
float Dmax = decodeVec.get(j + 1).floatValue();
decode[j++] = Dmin;
decode[j++] = (Dmax - Dmin) / maxValue;
}
}
BufferedImage smaskImage = null;
BufferedImage maskImage = null;
int[] maskMinRGB = null;
int[] maskMaxRGB = null;
int maskMinIndex = -1;
int maskMaxIndex = -1;
Object smaskObj = library.getObject(entries, SMASK_KEY);
Object maskObj = library.getObject(entries, MASK_KEY);
// If present, this entry shall override the current soft mask in the
// graphics state, as well as the image’s Mask entry, if any. However,
// the other transparency-related graphics state parameters—blend mode
// and alpha constant—shall remain in effect.
if (smaskObj instanceof Stream) {
ImageStream smaskStream = (ImageStream) smaskObj;
if (smaskStream.isImageSubtype()) {
smaskImage = smaskStream.getImage(graphicsState, resources);
}
}
// An image XObject defining an image mask to be applied to this image
// ("Explicit Masking"), or an array specifying a range of colours to be
// applied to it as a colour key mask ("Colour Key Masking").
if (maskObj != null && smaskImage == null) {
if (maskObj instanceof Stream) {
ImageStream maskStream = (ImageStream) maskObj;
if (maskStream.isImageSubtype()) {
maskImage = maskStream.getImage(graphicsState, resources);
}
} else if (maskObj instanceof List) {
List maskVector = (List) maskObj;
int[] maskMinOrigCompsInt = new int[colorSpaceCompCount];
int[] maskMaxOrigCompsInt = new int[colorSpaceCompCount];
for (int i = 0; i < colorSpaceCompCount; i++) {
if ((i * 2) < maskVector.size())
maskMinOrigCompsInt[i] = ((Number) maskVector.get(i * 2)).intValue();
if ((i * 2 + 1) < maskVector.size())
maskMaxOrigCompsInt[i] = ((Number) maskVector.get(i * 2 + 1)).intValue();
}
if (colourSpace instanceof Indexed) {
Indexed icolourSpace = (Indexed) colourSpace;
Color[] colors = icolourSpace.accessColorTable();
if (colors != null &&
maskMinOrigCompsInt.length >= 1 &&
maskMaxOrigCompsInt.length >= 1) {
maskMinIndex = maskMinOrigCompsInt[0];
maskMaxIndex = maskMaxOrigCompsInt[0];
if (maskMinIndex >= 0 && maskMinIndex < colors.length &&
maskMaxIndex >= 0 && maskMaxIndex < colors.length) {
Color minColor = colors[maskMinOrigCompsInt[0]];
Color maxColor = colors[maskMaxOrigCompsInt[0]];
maskMinRGB = new int[]{minColor.getRed(), minColor.getGreen(), minColor.getBlue()};
maskMaxRGB = new int[]{maxColor.getRed(), maxColor.getGreen(), maxColor.getBlue()};
}
}
} else {
PColorSpace.reverseInPlace(maskMinOrigCompsInt);
PColorSpace.reverseInPlace(maskMaxOrigCompsInt);
float[] maskMinOrigComps = new float[colorSpaceCompCount];
float[] maskMaxOrigComps = new float[colorSpaceCompCount];
colourSpace.normaliseComponentsToFloats(maskMinOrigCompsInt, maskMinOrigComps, (1 << bitsPerComponent) - 1);
colourSpace.normaliseComponentsToFloats(maskMaxOrigCompsInt, maskMaxOrigComps, (1 << bitsPerComponent) - 1);
Color minColor = colourSpace.getColor(maskMinOrigComps);
Color maxColor = colourSpace.getColor(maskMaxOrigComps);
PColorSpace.reverseInPlace(maskMinOrigComps);
PColorSpace.reverseInPlace(maskMaxOrigComps);
maskMinRGB = new int[]{minColor.getRed(), minColor.getGreen(), minColor.getBlue()};
maskMaxRGB = new int[]{maxColor.getRed(), maxColor.getGreen(), maxColor.getBlue()};
}
}
}
BufferedImage image = getImage(
colourSpace, graphicsState, width, height,
colorSpaceCompCount, bitsPerComponent,
isImageMask,
decode,
smaskImage, maskImage,
maskMinRGB, maskMaxRGB, maskMinIndex, maskMaxIndex);
// add the image to the pool, just encase it get painted again.
if (pObjectReference != null) {
library.getImagePool().put(pObjectReference, image);
}
return image;
}
/**
* Utility to to the image work, the public version pretty much just
* parses out image dictionary parameters. This method start the actual
* image decoding.
*
* @param colourSpace colour space of image.
* @param graphicsState graphic state used to render image.
* @param width width of image.
* @param height heigth of image
* @param colorSpaceCompCount colour space component count, 1, 3, 4 etc.
* @param bitsPerComponent number of bits that represent one component.
* @param isImageMask boolean flag to use image mask or not.
* @param decode decode array, 1,0 or 0,1 can effect colour interpretation.
* @param sMaskImage smaask image value, optional.
* @param maskImage buffered image image mask to apply to decoded image, optional.
* @param maskMinRGB max rgb values for the mask
* @param maskMaxRGB min rgb values for the mask.
* @param maskMinIndex max indexed colour values for the mask.
* @param maskMaxIndex min indexed colour values for the mask.
* @return buffered image of decoded image stream, null if an error occured.
*/
private BufferedImage getImage(
PColorSpace colourSpace,
GraphicsState graphicsState,
int width, int height,
int colorSpaceCompCount,
int bitsPerComponent,
boolean isImageMask,
float[] decode,
BufferedImage sMaskImage,
BufferedImage maskImage,
int[] maskMinRGB, int[] maskMaxRGB,
int maskMinIndex, int maskMaxIndex) {
// check to see if we need to create an imge with alpha, a mask
// will have imageMask=true and in this case we don't need alpha
BufferedImage decodedImage = null;
// JPEG writes out image if successful
if (shouldUseDCTDecode()) {
decodedImage = dctDecode(width, height, colourSpace, bitsPerComponent, decode);
}
// JBIG2 writes out image if successful
else if (shouldUseJBIG2Decode()) {
decodedImage = jbig2Decode(width, height, colourSpace, bitsPerComponent, decode);
}
// JPEG2000 writes out image if successful
else if (shouldUseJPXDecode()) {
decodedImage = jpxDecode(width, height, colourSpace, bitsPerComponent, decode);
}
// CCITTFax data is raw byte decode.
else if (shouldUseCCITTFaxDecode()) {
// try default ccittfax decode.
decodedImage = ccittFaxDecode(colourSpace, graphicsState, width, height,
colorSpaceCompCount, bitsPerComponent, isImageMask, decode, sMaskImage, maskImage,
maskMinRGB, maskMaxRGB, maskMinIndex, maskMaxIndex, false);
}
// we have some raw data so, CCITTfax or some other image primitive.
else {
byte[] data = getDecodedStreamBytes(
width * height
* colourSpace.getNumComponents()
* bitsPerComponent / 8);
int dataLength = data.length;
// finally push the bytes though the common image processor to try
// and build a a Buffered image.
try {
decodedImage = imageUtility.makeImageWithRasterFromBytes(
colourSpace,
graphicsState,
width, height,
colorSpaceCompCount,
bitsPerComponent,
isImageMask,
decode,
sMaskImage,
maskImage,
maskMinRGB, maskMaxRGB,
maskMinIndex, maskMaxIndex,
data, dataLength);
// ImageUtility.displayImage(decodedImage, pObjectReference.toString());
} catch (Exception e) {
logger.log(Level.FINE, "Error building image raster.", e);
}
}
// Fallback image cod the will use pixel primitives to build out the image.
if (decodedImage == null) {
byte[] data = getDecodedStreamBytes(
width * height
* colourSpace.getNumComponents()
* bitsPerComponent / 8);
// decodes the image stream and returns an image object. Legacy fallback
// code, should never get here, but there are always corner cases. .
decodedImage = parseImage(
width,
height,
colourSpace,
isImageMask,
graphicsState,
bitsPerComponent,
decode,
data);
}
if (decodedImage != null) {
// ImageUtility.displayImage(decodedImage, pObjectReference.toString());
// ImageUtility.writeImage(decodedImage, pObjectReference.toString(), "D:\\log\\");
if (isImageMask) {
decodedImage = imageUtility.applyExplicitMask(decodedImage, graphicsState.getFillColor());
}
// apply common mask and sMask processing
if (sMaskImage != null) {
decodedImage = new ImageUtility().applyExplicitSMask(decodedImage, sMaskImage);
}
if (maskImage != null) {
decodedImage = new ImageUtility().applyExplicitMask(decodedImage, maskImage);
}
// experimental check for different blending modes and apply a basic white = transparent,
// ExtGState extGState = graphicsState.getExtGState();
// if (extGState != null && extGState.getBlendingMode() != null ) {
// decodedImage = ImageUtility.applyBlendingMode(decodedImage, extGState.getBlendingMode(), Color.WHITE);
// }
// ImageUtility.displayImage(decodedImage, pObjectReference.toString());
// with little luck the image is ready for viewing.
return decodedImage;
}
return null;
}
/**
* Utility to try and parse out the CCITTFax data. We have two code paths one using our own decode algorithms
* as well as the use of JAI if available on the class path. We've had trouble in the past with gracefully falling
* back onto working implementation as each decoding method often errors without any exception.
*/
private BufferedImage ccittFaxDecode(
PColorSpace colourSpace,
GraphicsState graphicsState,
int width, int height,
int colorSpaceCompCount,
int bitsPerComponent,
boolean isImageMask,
float[] decode,
BufferedImage sMaskImage,
BufferedImage maskImage,
int[] maskMinRGB, int[] maskMaxRGB,
int maskMinIndex, int maskMaxIndex, boolean forceJAI) {
BufferedImage decodedImage = null;
byte[] data = getDecodedStreamBytes(
width * height
* colourSpace.getNumComponents()
* bitsPerComponent / 8);
int dataLength = data.length;
// try default ccittfax decode.
try {
// corner case where a user may want to use JAI because of
// speed or compatibility requirements.
if (forceJaiccittfax || forceJAI) {
throw new Throwable("Forcing CCITTFAX decode via JAI");
}
data = ccittFaxDecode(data, width, height);
dataLength = data.length;
} catch (Throwable e) {
// on a failure then fall back to JAI for a try. likely
// will not happen.
try {
decodedImage = CCITTFax.attemptDeriveBufferedImageFromBytes(
this, library, entries, graphicsState.getFillColor());
} catch (Throwable e1) {
// fall back on ccittfax code.
data = ccittFaxDecode(data, width, height);
dataLength = data.length;
}
if (decodedImage != null) {
return decodedImage;
}
}
// basic error check for an all white or black image, if we try and load the image again.
boolean allWhite = true;
boolean allBlack = true;
for (int i = 0; i < dataLength; i++) {
if (data[i] != -1) {
allWhite = false;
break;
}
}
for (int i = 0; i < dataLength; i++) {
if (data[i] != 0) {
allBlack = false;
break;
}
}
if (!forceJaiccittfax && (allBlack || allWhite)) {
try {
decodedImage = CCITTFax.attemptDeriveBufferedImageFromBytes(
this, library, entries, graphicsState.getFillColor());
if (decodedImage != null) {
return decodedImage;
}
} catch (Throwable e1) {
logger.finer("Failed to do secondary load attempt with JAI");
}
} else if (forceJaiccittfax && (allBlack || allWhite)) {
try {
data = ccittFaxDecode(data, width, height);
dataLength = data.length;
} catch (Throwable e) {
logger.finer("Failed to do secondary load attempt with ccittFax");
}
}
try {
decodedImage = imageUtility.makeImageWithRasterFromBytes(
colourSpace,
graphicsState,
width, height,
colorSpaceCompCount,
bitsPerComponent,
isImageMask,
decode,
sMaskImage,
maskImage,
maskMinRGB, maskMaxRGB,
maskMinIndex, maskMaxIndex,
data, dataLength);
} catch (Exception e) {
logger.log(Level.FINE, "Error building image raster.", e);
if (!forceJAI) {
decodedImage = ccittFaxDecode(colourSpace, graphicsState, width, height,
colorSpaceCompCount, bitsPerComponent, isImageMask, decode, sMaskImage, maskImage,
maskMinRGB, maskMaxRGB, maskMinIndex, maskMaxIndex, true);
}
}
return decodedImage;
}
/**
* The DCTDecode filter decodes grayscale or color image data that has been
* encoded in the JPEG baseline format. Because DCTDecode only deals
* with images, the instance of image is update instead of decoded
* stream.
*
* @return buffered images representation of the decoded JPEG data. Null
* if the image could not be properly decoded.
*/
private BufferedImage dctDecode(
int width, int height, PColorSpace colourSpace, int bitspercomponent,
float[] decode) {
// BIS's buffer size should be equal to mark() size, and greater than data size (below)
InputStream input = getDecodedByteArrayInputStream();
// Used to just read 1000, but found a PDF that included thumbnails first
final int MAX_BYTES_TO_READ_FOR_ENCODING = 2048;
BufferedInputStream bufferedInput = new BufferedInputStream(
input, MAX_BYTES_TO_READ_FOR_ENCODING);
bufferedInput.mark(MAX_BYTES_TO_READ_FOR_ENCODING);
// We don't use the PColorSpace to determine how to decode the JPEG, because it tends to be wrong
// Some files say DeviceCMYK, or ICCBased, when neither would work, because it's really YCbCrA
// What does work though, is to look into the JPEG headers themself, via getJPEGEncoding()
int jpegEncoding;
BufferedImage tmpImage = null;
ImageReader reader = null;
ImageInputStream imageInputStream = null;
try {
// get the full image data.
byte[] data = getDecodedStreamBytes(
width * height
* colourSpace.getNumComponents()
* bitspercomponent / 8);
int dataRead = data.length;
if (dataRead > MAX_BYTES_TO_READ_FOR_ENCODING) {
dataRead = MAX_BYTES_TO_READ_FOR_ENCODING;
}
// check the encoding type for colour conversion.
jpegEncoding = imageUtility.getJPEGEncoding(data, dataRead);
imageInputStream = ImageIO.createImageInputStream(
new ByteArrayInputStream(data));
// get a reader that supports getting the raster.
Iterator<ImageReader> iter = ImageIO.getImageReaders(imageInputStream);
while (iter.hasNext()) {
reader = iter.next();
if (reader.canReadRaster()) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("DCTDecode Image reader: " + reader + " " + width + "x" + height);
}
break;
}
}
// should never happen but bail on an empty reader.
if (reader == null) {
imageInputStream.close();
return null;
}
reader.setInput(imageInputStream, true, true);
// read the raster data only, as we have our own logic to covert
// the raster data to RGB colours.
ImageReadParam param = reader.getDefaultReadParam();
WritableRaster wr = (WritableRaster) reader.readRaster(0, param);
if (jpegEncoding == ImageUtility.JPEG_ENC_RGB && bitspercomponent == 8) {
tmpImage = imageUtility.convertSpaceToRgb(wr, colourSpace, decode);
} else if (jpegEncoding == ImageUtility.JPEG_ENC_CMYK && bitspercomponent == 8) {
tmpImage = imageUtility.convertCmykToRgb(wr, decode);
} else if (jpegEncoding == ImageUtility.JPEG_ENC_YCbCr && bitspercomponent == 8 &&
!(colourSpace instanceof Indexed)) {
tmpImage = imageUtility.convertYCbCrToRGB(wr, decode);
} else if (jpegEncoding == ImageUtility.JPEG_ENC_YCCK && bitspercomponent == 8) {
// YCCK to RGB works better if an CMYK intermediate is used, but slower.
tmpImage = imageUtility.convertYCCKToRgb(wr, decode);
} else if (jpegEncoding == ImageUtility.JPEG_ENC_GRAY && bitspercomponent == 8) {
// In DCTDecode with ColorSpace=DeviceGray, the samples are gray values (2000_SID_Service_Info.core)
// In DCTDecode with ColorSpace=Separation, the samples are Y values (45-14550BGermanForWeb.core AKA 4570.core)
// Avoid converting images that are already likely gray.
if (!(colourSpace instanceof DeviceGray) &&
!(colourSpace instanceof ICCBased) &&
!(colourSpace instanceof Indexed)) {
if (colourSpace instanceof Separation &&
((Separation) colourSpace).isNamedColor()) {
tmpImage = imageUtility.convertGrayToRgb(wr, decode);
} else {
tmpImage = imageUtility.convertSpaceToRgb(wr, colourSpace, decode);
}
} else {
if (colourSpace instanceof Indexed) {
tmpImage = imageUtility.applyIndexColourModel(wr, colourSpace, bitspercomponent);
} else if (wr.getNumBands() == 1) {
tmpImage = imageUtility.makeGrayBufferedImage(wr);
} else {
tmpImage = imageUtility.convertYCbCrToRGB(wr, decode);
}
}
} else {
if (colourSpace instanceof Indexed) {
return imageUtility.applyIndexColourModel(wr, colourSpace, bitspercomponent);
} // assume gray based jpeg.
if (wr.getNumBands() == 1) {
tmpImage = imageUtility.convertSpaceToRgb(wr, colourSpace, decode);
} else if (wr.getNumBands() == 2) {
tmpImage = imageUtility.convertGrayToRgb(wr, decode);
}
// otherwise assume YCbCr bands = 3.
else if (wr.getNumBands() == 3) {
tmpImage = imageUtility.convertYCbCrToRGB(wr, decode);
}
// still some corner cases around 4 components and one or the other.
else if (wr.getNumBands() == 4 && !(colourSpace instanceof ICCBased)) {
tmpImage = imageUtility.convertCmykToRgb(wr, decode);
} else {
tmpImage = imageUtility.convertYCbCrToRGB(wr, decode);
}
}
} catch (IOException e) {
logger.log(Level.FINE, "Problem loading JPEG image via ImageIO: ", e);
} finally {
try {
input.close();
// clean up the image reader and image stream
if (reader != null) {
reader.dispose();
}
if (imageInputStream != null) {
imageInputStream.close();
}
} catch (IOException e) {
logger.log(Level.FINE, "Problem loading JPEG image via ImageIO: ", e);
}
}
// legacy fallback code for creating Image.
// if (tmpImage == null) {
// try {
// //System.out.println("Stream.dctDecode() Toolkit");
// byte[] data = getDecodedStreamBytes(width * height
// * colourSpace.getNumComponents()
// * bitspercomponent / 8);
// if (data != null) {
// Image img = Toolkit.getDefaultToolkit().createImage(data);
// if (img != null) {
// tmpImage = ImageUtility.makeRGBABufferedImageFromImage(img);
// }
// }
// } catch (Exception e) {
// logger.log(Level.FINE, "Problem loading JPEG image via Toolkit: ", e);
// }
// }
return tmpImage;
}
/**
* Utility method to decode JBig2 images.
*
* @param width width of image
* @param height height of image
* @return buffered image of decoded jbig2 image stream. Null if an error
* occured during decode.
*/
private BufferedImage jbig2Decode(int width, int height,
PColorSpace colourSpace,
int bitsPerComponent, float[] decode) {
BufferedImage tmpImage;
// get the decode params form the stream
HashMap decodeParms = library.getDictionary(entries, DECODEPARMS_KEY);
Stream globalsStream = null;
if (decodeParms != null) {
Object jbigGlobals = library.getObject(decodeParms, JBIG2GLOBALS_KEY);
if (jbigGlobals instanceof Stream) {
globalsStream = (Stream) jbigGlobals;
}
}
// grab the data,
byte[] data = getDecodedStreamBytes(
width * height
* colourSpace.getNumComponents()
* bitsPerComponent / 8);
// ICEpdf-pro has a commercial license of the levigo library but the OS
// library can use it to if the project can comply with levigo's open
// source licence.
if (isLevigoJBIG2ImageReaderClass) {
try {
tmpImage = imageUtility.proJBig2Decode(
ImageIO.createImageInputStream(new ByteArrayInputStream(data)),
decodeParms, globalsStream);
} catch (Exception e) {
logger.log(Level.WARNING, "Problem loading JBIG2 image using Levigo: ", e);
// fall back and try and load with the OS jbig2 implementation.
tmpImage = imageUtility.jbig2Decode(
data,
decodeParms, globalsStream);
}
} else {
tmpImage = imageUtility.jbig2Decode(
data,
decodeParms, globalsStream);
}
// apply decode
if ((colourSpace instanceof DeviceGray)) {
tmpImage = imageUtility.applyGrayDecode(tmpImage, bitsPerComponent, decode);
}
// apply the fill colour and alpha if masking is enabled.
return tmpImage;
}
/**
* Utility method to decode JPEG2000 images.
*
* @param width width of image.
* @param height height of image.
* @param colourSpace colour space to apply to image.
* @param bitsPerComponent bits used to represent a colour
* @return buffered image of the jpeg2000 image stream. Null if a problem
* occurred during the decode.
*/
private BufferedImage jpxDecode(int width, int height, PColorSpace colourSpace,
int bitsPerComponent, float[] decode) {
BufferedImage tmpImage = null;
try {
// Verify that ImageIO can read JPEG2000
Iterator<ImageReader> iterator = ImageIO.getImageReadersByFormatName("JPEG2000");
if (!iterator.hasNext()) {
logger.info(
"ImageIO missing required plug-in to read JPEG 2000 images. " +
"You can download the JAI ImageIO Tools from: " +
"http://www.oracle.com/technetwork/java/current-142188.html");
return null;
}
// decode the image.
byte[] data = getDecodedStreamBytes(
width * height
* colourSpace.getNumComponents()
* bitsPerComponent / 8);
ImageInputStream imageInputStream = ImageIO.createImageInputStream(
new ByteArrayInputStream(data));
// getting the raster for JPX seems to fail in most cases.
Iterator<ImageReader> iter = ImageIO.getImageReaders(imageInputStream);
ImageReader reader = null;
while (iter.hasNext()) {
reader = iter.next();
if (reader.canReadRaster()) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("JPXDecode Image reader: " + reader);
}
break;
}
}
// read the raster data only, as we have our own logic to covert
// the raster data to RGB colours.
if (reader == null) {
imageInputStream.close();
return null;
}
ImageReadParam param = reader.getDefaultReadParam();
reader.setInput(imageInputStream, true, true);
try {
tmpImage = reader.read(0, param);
} finally {
reader.dispose();
imageInputStream.close();
}
WritableRaster wr = tmpImage.getRaster();
// special fallback scenario for ICCBased colours.
if (colourSpace instanceof ICCBased) {
ICCBased iccBased = (ICCBased) colourSpace;
// first try and apply the color space
try {
ColorSpace cs = iccBased.getColorSpace();
ColorConvertOp cco = new ColorConvertOp(cs, null);
tmpImage = imageUtility.makeRGBBufferedImage(wr);
cco.filter(tmpImage, tmpImage);
} catch (Throwable e) {
logger.warning("Error processing ICC Color profile, failing " +
"back to alternative.");
// set the alternate as the current and try and process
// using the below rules.
colourSpace = iccBased.getAlternate();
}
}
// apply respective colour models to the JPEG2000 image.
if (colourSpace instanceof DeviceRGB && bitsPerComponent == 8) {
tmpImage = imageUtility.convertSpaceToRgb(wr, colourSpace, decode);
} else if (colourSpace instanceof DeviceCMYK && bitsPerComponent == 8) {
tmpImage = imageUtility.convertCmykToRgb(wr, decode);
} else if ((colourSpace instanceof DeviceGray)
&& bitsPerComponent == 8) {
tmpImage = imageUtility.makeGrayBufferedImage(wr);
} else if (colourSpace instanceof Separation) {
if (colourSpace instanceof Separation &&
((Separation) colourSpace).isNamedColor()) {
tmpImage = imageUtility.convertGrayToRgb(wr, decode);
// tmpImage = ImageUtility.makeGrayBufferedImage(wr);
} else {
tmpImage = imageUtility.convertSpaceToRgb(wr, colourSpace, decode);
}
} else if (colourSpace instanceof Indexed) {
// still some issue here with Chevron.pdf
tmpImage = imageUtility.applyIndexColourModel(wr, colourSpace, bitsPerComponent);
}
} catch (IOException e) {
logger.log(Level.FINE, "Problem loading JPEG2000 image: ", e);
}
return tmpImage;
}
/**
* CCITT fax decode algorithm, decodes the stream into a valid image
* stream that can be used to create a BufferedImage.
*
* @param width of image
* @param height height of image.
* @return decoded stream bytes.
*/
private byte[] ccittFaxDecode(byte[] streamData, int width, int height) {
HashMap decodeParms = library.getDictionary(entries, DECODEPARMS_KEY);
float k = library.getFloat(decodeParms, K_KEY);
// default value is always false
boolean blackIs1 = getBlackIs1(library, decodeParms);
// double check for blackIs1 in the main dictionary.
if (!blackIs1 && CHECK_PARENT_BLACK_IS_1) {
blackIs1 = getBlackIs1(library, entries);
}
// get value of key if it is available.
boolean encodedByteAlign = false;
Object encodedByteAlignObject = library.getObject(decodeParms, ENCODEDBYTEALIGN_KEY);
if (encodedByteAlignObject instanceof Boolean) {
encodedByteAlign = (Boolean) encodedByteAlignObject;
}
int columns = library.getInt(decodeParms, COLUMNS_KEY);
int rows = library.getInt(decodeParms, ROWS_KEY);
if (columns == 0) {
columns = width;
}
if (rows == 0) {
rows = height;
}
int size = rows * ((columns + 7) >> 3);
byte[] decodedStreamData = new byte[size];
CCITTFaxDecoder decoder = new CCITTFaxDecoder(1, columns, rows);
decoder.setAlign(encodedByteAlign);
// pick three three possible fax encoding.
try {
if (k == 0) {
decoder.decodeT41D(decodedStreamData, streamData, 0, rows);
} else if (k > 0) {
decoder.decodeT42D(decodedStreamData, streamData, 0, rows);
} else if (k < 0) {
decoder.decodeT6(decodedStreamData, streamData, 0, rows);
}
} catch (Exception e) {
logger.warning("Error decoding CCITTFax image k: " + k);
// IText 5.03 doesn't correctly assign a k value for the deocde,
// as result we can try one more time using the T6.
decoder.decodeT6(decodedStreamData, streamData, 0, rows);
}
// check the black is value flag, no one likes inverted colours.
if (!blackIs1) {
// toggle the byte data invert colour, not bit operand.
for (int i = 0; i < decodedStreamData.length; i++) {
decodedStreamData[i] = (byte) ~decodedStreamData[i];
}
}
return decodedStreamData;
}
/**
* Parses the image stream and creates a Java Images object based on the
* the given stream and the supporting paramaters.
*
* @param width dimension of new image
* @param height dimension of new image
* @param colorSpace colour space of image
* @param imageMask true if the image has a imageMask, false otherwise
* @param bitsPerColour number of bits used in a colour
* @param decode Decode attribute values from PObject
* @return valid java image from the PDF stream
*/
private BufferedImage parseImage(
int width,
int height,
PColorSpace colorSpace,
boolean imageMask,
GraphicsState graphicsState,
int bitsPerColour,
float[] decode,
byte[] baCCITTFaxData) {
// store for manipulating bits in image
int[] imageBits = new int[width];
// RGB value for colour used as fill for image
int fillRGB = 1;
if (graphicsState != null) {
fillRGB = graphicsState.getFillColor().getRGB();
}
// Number of colour components in image, should be 3 for RGB or 4
// for ARGB.
int colorSpaceCompCount = colorSpace.getNumComponents();
boolean isDeviceRGB = colorSpace instanceof DeviceRGB;
boolean isDeviceGray = colorSpace instanceof DeviceGray;
// Max value used to represent a colour, usually 255, min is 0
int maxColourValue = ((1 << bitsPerColour) - 1);
int f[] = new int[colorSpaceCompCount];
float ff[] = new float[colorSpaceCompCount];
// image mask from
float imageMaskValue = decode[0];
// Create the memory hole where where the buffered image will be written
// too, bit by painful bit.
BufferedImage bim = ImageUtility.createTranslucentCompatibleImage(width, height);
// create the buffer and get the first series of bytes from the cached
// stream
BitStream in;
if (baCCITTFaxData != null) {
in = new BitStream(new ByteArrayInputStream(baCCITTFaxData));
} else {
InputStream dataInput = getDecodedByteArrayInputStream();
if (dataInput == null)
return null;
in = new BitStream(dataInput);
}
try {
// Start encoding bit stream into an image, we work one pixel at
// a time, and grap the need bit information for the images
// colour space and bits per colour
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// if image has mask apply it
if (imageMask) {
int bit = in.getBits(bitsPerColour);
bit = (bit == imageMaskValue) ? fillRGB : 0x00000000;
imageBits[x] = bit;
}
// other wise start colour bit parsing
else {
// set some default values
int red = 255;
int blue = 255;
int green = 255;
int alpha = 255;
// indexed colour
if (colorSpaceCompCount == 1) {
// get value used for this bit
int bit = in.getBits(bitsPerColour);
// check decode array if a colour inversion is needed
// if index 0 > index 1 then we have a need for ainversion
if (decode[0] > decode[1]) {
bit = (bit == maxColourValue) ? 0x00000000 : maxColourValue;
}
if (isDeviceGray) {
if (bitsPerColour == 1)
bit = ImageUtility.GRAY_1_BIT_INDEX_TO_RGB[bit];
else if (bitsPerColour == 2)
bit = ImageUtility.GRAY_2_BIT_INDEX_TO_RGB[bit];
else if (bitsPerColour == 4)
bit = ImageUtility.GRAY_4_BIT_INDEX_TO_RGB[bit];
else if (bitsPerColour == 8) {
bit = ((bit << 24) |
(bit << 16) |
(bit << 8) |
bit);
}
imageBits[x] = bit;
} else {
f[0] = bit;
colorSpace.normaliseComponentsToFloats(f, ff, maxColourValue);
Color color = colorSpace.getColor(ff);
imageBits[x] = color.getRGB();
}
}
// normal RGB colour
else if (colorSpaceCompCount == 3) {
// We can have an ICCBased color space that has 3 components,
// but where we can't assume it's RGB.
// But, when it is DeviceRGB, we don't want the performance hit
// of converting the pixels via the PColorSpace, so we'll
// break this into the two cases
if (isDeviceRGB) {
red = in.getBits(bitsPerColour);
green = in.getBits(bitsPerColour);
blue = in.getBits(bitsPerColour);
// combine the colour together
imageBits[x] = (alpha << 24) | (red << 16) |
(green << 8) | blue;
} else {
for (int i = 0; i < colorSpaceCompCount; i++) {
f[i] = in.getBits(bitsPerColour);
}
PColorSpace.reverseInPlace(f);
colorSpace.normaliseComponentsToFloats(f, ff, maxColourValue);
Color color = colorSpace.getColor(ff);
imageBits[x] = color.getRGB();
}
}
// normal aRGB colour, this could use some more
// work for optimizing.
else if (colorSpaceCompCount == 4 || colorSpace instanceof DeviceN) {
for (int i = 0; i < colorSpaceCompCount; i++) {
f[i] = in.getBits(bitsPerColour);
// apply decode
if (decode[0] > decode[1]) {
f[i] = maxColourValue - f[i];
}
}
PColorSpace.reverseInPlace(f);
colorSpace.normaliseComponentsToFloats(f, ff, maxColourValue);
Color color = colorSpace.getColor(ff);
imageBits[x] = color.getRGB();
}
// else just set pixel with the default values
else {
// compine the colour together
imageBits[x] = (alpha << 24) | (red << 16) |
(green << 8) | blue;
}
}
}
// Assign the new bits for this pixel
bim.setRGB(0, y, width, 1, imageBits, 0, 1);
}
// final clean up.
in.close();
} catch (IOException e) {
logger.log(Level.FINE, "Error parsing image.", e);
}
return bim;
}
/**
* If BlackIs1 was not specified, then return null, instead of the
* default value of false, so we can tell if it was given or not
*/
public boolean getBlackIs1(Library library, HashMap decodeParmsDictionary) {
Object blackIs1Obj = library.getObject(decodeParmsDictionary, BLACKIS1_KEY);
if (blackIs1Obj != null) {
if (blackIs1Obj instanceof Boolean) {
return (Boolean) blackIs1Obj;
} else if (blackIs1Obj instanceof String) {
String blackIs1String = (String) blackIs1Obj;
if (blackIs1String.equalsIgnoreCase("true"))
return true;
else if (blackIs1String.equalsIgnoreCase("t"))
return true;
else if (blackIs1String.equals("1"))
return true;
else if (blackIs1String.equalsIgnoreCase("false"))
return false;
else if (blackIs1String.equalsIgnoreCase("f"))
return false;
else if (blackIs1String.equals("0"))
return false;
}
}
return false;
}
private boolean containsFilter(String[] searchFilterNames) {
List filterNames = getFilterNames();
if (filterNames == null)
return false;
for (Object filterName1 : filterNames) {
String filterName = filterName1.toString();
for (String search : searchFilterNames) {
if (search.equals(filterName)) {
return true;
}
}
}
return false;
}
/**
* Does the image have an ImageMask.
*/
public boolean isImageMask() {
return library.getBoolean(entries, IMAGEMASK_KEY);
}
private boolean shouldUseCCITTFaxDecode() {
return containsFilter(CCITTFAX_DECODE_FILTERS);
}
private boolean shouldUseDCTDecode() {
return containsFilter(DCT_DECODE_FILTERS);
}
private boolean shouldUseJBIG2Decode() {
return containsFilter(JBIG2_DECODE_FILTERS);
}
private boolean shouldUseJPXDecode() {
return containsFilter(JPX_DECODE_FILTERS);
}
/**
* Used to enable/disable the loading of CCITTFax images using JAI library.
* This method can be used in place of the system property
* org.icepdf.core.ccittfax.jai .
*
* @param enable eanb
*/
public static void forceJaiCcittFax(boolean enable) {
forceJaiccittfax = enable;
}
public PColorSpace getColourSpace() {
synchronized (colorSpaceAssignmentLock) {
return colourSpace;
}
}
/**
* Return a string description of the object. Primarily used for debugging.
*/
public String toString() {
StringBuilder sb = new StringBuilder(64);
sb.append("Image stream= ");
sb.append(entries);
if (getPObjectReference() != null) {
sb.append(" ");
sb.append(getPObjectReference());
}
return sb.toString();
}
}