package com.tom_roush.pdfbox.pdmodel.graphics.image; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.util.Log; import com.tom_roush.pdfbox.cos.COSArray; import com.tom_roush.pdfbox.cos.COSBase; import com.tom_roush.pdfbox.cos.COSInputStream; import com.tom_roush.pdfbox.cos.COSName; import com.tom_roush.pdfbox.cos.COSStream; import com.tom_roush.pdfbox.io.IOUtils; import com.tom_roush.pdfbox.pdmodel.PDDocument; import com.tom_roush.pdfbox.pdmodel.PDResources; import com.tom_roush.pdfbox.pdmodel.common.PDMetadata; import com.tom_roush.pdfbox.pdmodel.common.PDStream; import com.tom_roush.pdfbox.pdmodel.graphics.PDXObject; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColorSpace; import com.tom_roush.pdfbox.pdmodel.graphics.color.PDDeviceGray; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.SoftReference; import java.util.List; /** * An Image XObject. * * @author John Hewson * @author Ben Litchfield */ public final class PDImageXObject extends PDXObject implements PDImage { private SoftReference<Bitmap> cachedImage; private PDColorSpace colorSpace; private final PDResources resources; // current resource dictionary (has color spaces) /** * Creates a thumbnail Image XObject from the given COSBase and name. * @param cosStream the COS stream * @return an XObject * @throws IOException if there is an error creating the XObject. */ public static PDImageXObject createThumbnail(COSStream cosStream) throws IOException { // thumbnails are special, any non-null subtype is treated as being "Image" PDStream pdStream = new PDStream(cosStream); return new PDImageXObject(pdStream, null); } /** * Creates an Image XObject in the given document. * @param document the current document * @throws java.io.IOException if there is an error creating the XObject. */ public PDImageXObject(PDDocument document) throws IOException { this(new PDStream(document), null); } /** * Creates an Image XObject in the given document using the given filtered stream. * @param document the current document * @param encodedStream an encoded stream of image data * @param cosFilter the filter or a COSArray of filters * @param width the image width * @param height the image height * @param bitsPerComponent the bits per component * @param initColorSpace the color space * @throws IOException if there is an error creating the XObject. */ public PDImageXObject(PDDocument document, InputStream encodedStream, COSBase cosFilter, int width, int height, int bitsPerComponent, PDColorSpace initColorSpace) throws IOException { super(createRawStream(document, encodedStream), COSName.IMAGE); getCOSStream().setItem(COSName.FILTER, cosFilter); resources = null; colorSpace = null; setBitsPerComponent(bitsPerComponent); setWidth(width); setHeight(height); setColorSpace(initColorSpace); } /** * Creates a COS stream from raw (encoded) data. */ private static COSStream createRawStream(PDDocument document, InputStream rawInput) throws IOException { COSStream stream = document.getDocument().createCOSStream(); OutputStream output = null; try { output = stream.createRawOutputStream(); IOUtils.copy(rawInput, output); } finally { if (output != null) { output.close(); } } return stream; } /** * Creates an Image XObject with the given stream as its contents and current color spaces. * @param stream the XObject stream to read * @param resources the current resources * @throws java.io.IOException if there is an error creating the XObject. */ public PDImageXObject(PDStream stream, PDResources resources) throws IOException { this(stream, resources, stream.createInputStream()); } /** * Create a PDImageXObject from an image file, see {@link #createFromFile(File, PDDocument)} for * more details. * * @param imagePath the image file path. * @param doc the document that shall use this PDImageXObject. * @return a PDImageXObject. * @throws IOException if there is an error when reading the file or creating the * PDImageXObject, or if the image type is not supported. */ public static PDImageXObject createFromFile(String imagePath, PDDocument doc) throws IOException { return createFromFile(new File(imagePath), doc); } /** * Create a PDImageXObject from an image file. The file format is determined by the file name * suffix. The following suffixes are supported: jpg, jpeg, tif, tiff, gif, bmp and png. This is * a convenience method that calls {@link JPEGFactory#createFromStream}, * {@link CCITTFactory#createFromFile} or {@link ImageIO#read} combined with * {@link LosslessFactory#createFromImage}. (The later can also be used to create a * PDImageXObject from a BufferedImage). * * @param file the image file. * @param doc the document that shall use this PDImageXObject. * @return a PDImageXObject. * @throws IOException if there is an error when reading the file or creating the * PDImageXObject. * @throws IllegalArgumentException if the image type is not supported. */ public static PDImageXObject createFromFile(File file, PDDocument doc) throws IOException { String name = file.getName(); int dot = file.getName().lastIndexOf('.'); if (dot == -1) { throw new IOException("Image type not supported: " + name); } String ext = name.substring(dot + 1).toLowerCase(); if ("jpg".equals(ext) || "jpeg".equals(ext)) { return JPEGFactory.createFromStream(doc, new FileInputStream(file)); } if ("tif".equals(ext) || "tiff".equals(ext)) { return CCITTFactory.createFromFile(doc, file); } if ("gif".equals(ext) || "bmp".equals(ext) || "png".equals(ext)) { Bitmap bim = BitmapFactory.decodeFile(file.getPath()); return LosslessFactory.createFromImage(doc, bim); } throw new IOException("Image type not supported: " + name); } // repairs parameters using decode result private PDImageXObject(PDStream stream, PDResources resources, COSInputStream input) { super(repair(stream, input), COSName.IMAGE); this.resources = resources; // this.colorSpace = input.getDecodeResult().getJPXColorSpace();TODO: PdfBox-Android } // repairs parameters using decode result private static PDStream repair(PDStream stream, COSInputStream input) { stream.getStream().addAll(input.getDecodeResult().getParameters()); return stream; } /** * Returns the metadata associated with this XObject, or null if there is none. * @return the metadata associated with this object. */ public PDMetadata getMetadata() { COSStream cosStream = (COSStream) getCOSStream().getDictionaryObject(COSName.METADATA); if (cosStream != null) { return new PDMetadata(cosStream); } return null; } /** * Sets the metadata associated with this XObject, or null if there is none. * @param meta the metadata associated with this object */ public void setMetadata(PDMetadata meta) { getCOSStream().setItem(COSName.METADATA, meta); } /** * Returns the key of this XObject in the structural parent tree. * @return this object's key the structural parent tree */ public int getStructParent() { return getCOSStream().getInt(COSName.STRUCT_PARENT, 0); } /** * Sets the key of this XObject in the structural parent tree. * @param key the new key for this XObject */ public void setStructParent(int key) { getCOSStream().setInt(COSName.STRUCT_PARENT, key); } /** * {@inheritDoc} * The returned images are cached via a SoftReference. */ @Override public Bitmap getImage() throws IOException { if (cachedImage != null) { Bitmap cached = cachedImage.get(); if (cached != null) { return cached; } } // get image as RGB Bitmap image = SampledImageReader.getRGBImage(this, getColorKeyMask()); // soft mask (overrides explicit mask) PDImageXObject softMask = getSoftMask(); if (softMask != null) { image = applyMask(image, softMask.getOpaqueImage(), true); } else { // explicit mask PDImageXObject mask = getMask(); if (mask != null) { image = applyMask(image, mask.getOpaqueImage(), false); } } cachedImage = new SoftReference<Bitmap>(image); return image; } /** * {@inheritDoc} * The returned images are not cached. */ // @Override // public BufferedImage getStencilImage(Paint paint) throws IOException // { // if (!isStencil()) // { // throw new IllegalStateException("Image is not a stencil"); // } // return SampledImageReader.getStencilImage(this, paint); // }TODO: PdfBox-Android /** * Returns an RGB buffered image containing the opaque image stream without any masks applied. * If this Image XObject is a mask then the buffered image will contain the raw mask. * @return the image without any masks applied * @throws IOException if the image cannot be read */ public Bitmap getOpaqueImage() throws IOException { return SampledImageReader.getRGBImage(this, null); } // explicit mask: RGB + Binary -> ARGB // soft mask: RGB + Gray -> ARGB private Bitmap applyMask(Bitmap image, Bitmap mask, boolean isSoft) throws IOException { if (mask == null) { return image; } int width = image.getWidth(); int height = image.getHeight(); if (mask.getWidth() < width || mask.getHeight() < height) { mask = Bitmap.createScaledBitmap(mask, width, height, true); } else if (mask.getWidth() > width || mask.getHeight() > height) { width = mask.getWidth(); height = mask.getHeight(); image = Bitmap.createScaledBitmap(image, width, height, true); } // compose to ARGB Bitmap masked = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); // scale mask to fit image if (mask.getWidth() != width || mask.getHeight() != height) { mask = Bitmap.createScaledBitmap(mask, width, height, true); } int alphaPixel; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int color = image.getPixel(x, y); // Greyscale, any rgb component should do alphaPixel = Color.red(mask.getPixel(x, y)); if (!isSoft) { alphaPixel = 255 - alphaPixel; } masked.setPixel(x, y, Color.argb(alphaPixel, Color.red(color), Color.green(color), Color.blue(color))); } } return masked; } /** * Returns the Mask Image XObject associated with this image, or null if there is none. * @return Mask Image XObject */ public PDImageXObject getMask() throws IOException { COSBase mask = getCOSStream().getDictionaryObject(COSName.MASK); if (mask instanceof COSArray) { // color key mask, no explicit mask to return return null; } else { COSStream cosStream = (COSStream)getCOSStream().getDictionaryObject(COSName.MASK); if (cosStream != null) { // always DeviceGray return new PDImageXObject(new PDStream(cosStream), null); } return null; } } /** * Returns the color key mask array associated with this image, or null if there is none. * @return Mask Image XObject */ public COSArray getColorKeyMask() { COSBase mask = getCOSStream().getDictionaryObject(COSName.MASK); if (mask instanceof COSArray) { return (COSArray)mask; } return null; } /** * Returns the Soft Mask Image XObject associated with this image, or null if there is none. * @return the SMask Image XObject, or null. */ public PDImageXObject getSoftMask() throws IOException { COSStream cosStream = (COSStream)getCOSStream().getDictionaryObject(COSName.SMASK); if (cosStream != null) { // always DeviceGray return new PDImageXObject(new PDStream(cosStream), null); } return null; } @Override public int getBitsPerComponent() { if (isStencil()) { return 1; } else { return getCOSStream().getInt(COSName.BITS_PER_COMPONENT, COSName.BPC); } } @Override public void setBitsPerComponent(int bpc) { getCOSStream().setInt(COSName.BITS_PER_COMPONENT, bpc); } @Override public PDColorSpace getColorSpace() throws IOException { if (colorSpace == null) { COSBase cosBase = getCOSStream().getDictionaryObject(COSName.COLORSPACE, COSName.CS); if (cosBase != null) { colorSpace = PDColorSpace.create(cosBase, resources); } else if (isStencil()) { // stencil mask color space must be gray, it is often missing return PDDeviceGray.INSTANCE; } else { // an image without a color space is always broken throw new IOException("could not determine color space"); } } return colorSpace; } @Override public InputStream createInputStream() throws IOException { return getStream().createInputStream(); } @Override public InputStream createInputStream(List<String> stopFilters) throws IOException { return createInputStream(); } @Override public boolean isEmpty() { return getStream().getStream().getLength() == 0; } @Override public void setColorSpace(PDColorSpace cs) { getCOSStream().setItem(COSName.COLORSPACE, cs != null ? cs.getCOSObject() : null); } @Override public int getHeight() { return getCOSStream().getInt(COSName.HEIGHT); } @Override public void setHeight(int h) { getCOSStream().setInt(COSName.HEIGHT, h); } @Override public int getWidth() { return getCOSStream().getInt(COSName.WIDTH); } @Override public void setWidth(int w) { getCOSStream().setInt(COSName.WIDTH, w); } @Override public boolean getInterpolate() { return getCOSStream().getBoolean(COSName.INTERPOLATE, false); } @Override public void setInterpolate(boolean value) { getCOSStream().setBoolean(COSName.INTERPOLATE, value); } @Override public void setDecode(COSArray decode) { getCOSStream().setItem(COSName.DECODE, decode); } @Override public COSArray getDecode() { COSBase decode = getCOSStream().getDictionaryObject(COSName.DECODE); if (decode instanceof COSArray) { return (COSArray) decode; } return null; } @Override public boolean isStencil() { return getCOSStream().getBoolean(COSName.IMAGE_MASK, false); } @Override public void setStencil(boolean isStencil) { getCOSStream().setBoolean(COSName.IMAGE_MASK, isStencil); } /** * This will get the suffix for this image type, e.g. jpg/png. * @return The image suffix or null if not available. */ @Override public String getSuffix() { List<COSName> filters = getStream().getFilters(); if (filters == null) { return "png"; } else if (filters.contains(COSName.DCT_DECODE)) { return "jpg"; } else if (filters.contains(COSName.JPX_DECODE)) { return "jpx"; } else if (filters.contains(COSName.CCITTFAX_DECODE)) { return "tiff"; } else if (filters.contains(COSName.FLATE_DECODE) || filters.contains(COSName.LZW_DECODE) || filters.contains(COSName.RUN_LENGTH_DECODE)) { return "png"; } else { Log.w("PdfBox-Android", "getSuffix() returns null, filters: " + filters); // TODO more... return null; } } }