/* * $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 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); // } // } }