Android-Pdf-Viewer-Library/src/com/sun/pdfview/PDFImage.java

711 lines
24 KiB
Java

/*
* $Id: PDFImage.java,v 1.9 2009/03/12 13:23:54 tomoke 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 java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import com.sun.pdfview.colorspace.PDFColorSpace;
import com.sun.pdfview.function.FunctionType0;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Bitmap.Config;
import android.util.Log;
/**
* Encapsulates a PDF Image
*/
public class PDFImage {
private static final String TAG = "AWTPDF.pdfimage";
public static boolean sShowImages;
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);
}
/** 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 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:
int[] 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 int[]{Color.WHITE, Color.BLACK};
}
}
// TODO [FHe]: support for indexed colorspace
image.setColorSpace(PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_GRAY));
// 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 Bitmap getImage() {
try {
Bitmap bi = (Bitmap) imageObj.getCache();
if (bi == null) {
if (!sShowImages)
throw new UnsupportedOperationException("do not show images");
byte[] imgBytes = imageObj.getStream();
bi = parseData(imgBytes);
// TODO [FHe]: is the cache useful on Android?
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;
} catch (OutOfMemoryError e) {
// fix for too large images
Log.e(TAG, "image too large (OutOfMemoryError)");
int size = 15;
int max = size-1;
int half = size/2-1;
Bitmap bi = Bitmap.createBitmap(size, size, Config.RGB_565);
Canvas c = new Canvas(bi);
c.drawColor(Color.RED);
Paint p = new Paint();
p.setColor(Color.WHITE);
c.drawLine(0, 0, max, max, p);
c.drawLine(0, max, max, 0, p);
c.drawLine(half, 0, half, max, p);
c.drawLine(0, half, max, half, p);
return bi;
}
}
private Bitmap parseData(byte[] imgBytes) {
Bitmap bi;
long startTime = System.currentTimeMillis();
// parse the stream data into an actual image
Log.i(TAG, "Creating Image width="+getWidth() + ", Height="+getHeight()+", bpc="+getBitsPerComponent()+",cs="+colorSpace);
if (colorSpace == null) {
throw new UnsupportedOperationException("image without colorspace");
} else if (colorSpace.getType() == PDFColorSpace.COLORSPACE_RGB) {
int maxH = getHeight();
int maxW = getWidth();
if (imgBytes.length == 2*maxW*maxH) {
// decoded JPEG as RGB565
bi = Bitmap.createBitmap(maxW, maxH, Config.RGB_565);
bi.copyPixelsFromBuffer(ByteBuffer.wrap(imgBytes));
}
else {
// create RGB image
bi = Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888);
int[] line = new int[maxW];
int n=0;
for (int h = 0; h<maxH; h++) {
for (int w = 0; w<getWidth(); w++) {
line[w] = ((0xff&(int)imgBytes[n])<<8|(0xff&(int)imgBytes[n+1]))<<8|(0xff&(int)imgBytes[n+2])|0xFF000000;
// line[w] = Color.rgb(0xff&(int)imgBytes[n], 0xff&(int)imgBytes[n+1],0xff&(int)imgBytes[n+2]);
n+=3;
}
bi.setPixels(line, 0, maxW, 0, h, maxW, 1);
}
}
}
else if (colorSpace.getType() == PDFColorSpace.COLORSPACE_GRAY) {
// create gray image
bi = Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888);
int maxH = getHeight();
int maxW = getWidth();
int[] line = new int[maxW];
int n=0;
for (int h = 0; h<maxH; h++) {
for (int w = 0; w<getWidth(); w++) {
int gray = 0xff&(int)imgBytes[n];
line[w] = (gray<<8|gray)<<8|gray|0xFF000000;
n+=1;
}
bi.setPixels(line, 0, maxW, 0, h, maxW, 1);
}
}
else if (colorSpace.getType() == PDFColorSpace.COLORSPACE_INDEXED) {
// create indexed image
bi = Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888);
int maxH = getHeight();
int maxW = getWidth();
int[] line = new int[maxW];
int[] comps = new int[1];
int n=0;
for (int h = 0; h<maxH; h++) {
for (int w = 0; w<getWidth(); w++) {
comps[0] = imgBytes[n]&0xff;
line[w] = colorSpace.toColor(comps);
n+=1;
}
bi.setPixels(line, 0, maxW, 0, h, maxW, 1);
}
}
else {
throw new UnsupportedOperationException("image with unsupported colorspace "+colorSpace);
}
long stopTime = System.currentTimeMillis();
Log.i(TAG, "millis for converting image="+(stopTime-startTime));
return bi;
}
// /**
// * <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
// */
// protected Bitmap parseData(byte[] data) {
//// 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();
//// }
// // 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
// ColorModel 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 () *
// getBitsPerComponent () / 8;
// 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 (?)
// */
// BufferedImage 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 {
// bi = new BufferedImage(cm, raster, true, null);
// }
//
// // hack to avoid *very* slow conversion
// ColorSpace cs = cm.getColorSpace();
// ColorSpace rgbCS = ColorSpace.getInstance(ColorSpace.CS_sRGB);
//
// // 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);
// }
/**
* 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) {
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 (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 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) {
// 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);
// }
// }
}