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.COSNumber;
import com.tom_roush.pdfbox.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* Reads a sampled image from a PDF file.
* @author John Hewson
*/
final class SampledImageReader
{
private SampledImageReader()
{
}
/**
* Returns an ARGB image filled with the given paint and using the given image as a mask.
* @param paint the paint to fill the visible portions of the image with
* @return a masked image filled with the given paint
* @throws IOException if the image cannot be read
* @throws IllegalStateException if the image is not a stencil.
*/
// public static BufferedImage getStencilImage(PDImage pdImage, Paint paint) throws IOException
// {
// // get mask (this image)
// BufferedImage mask = getRGBImage(pdImage, null);
//
// // compose to ARGB
// BufferedImage masked = new BufferedImage(mask.getWidth(), mask.getHeight(),
// BufferedImage.TYPE_INT_ARGB);
// Graphics2D g = masked.createGraphics();
//
// // draw the mask
// //g.drawImage(mask, 0, 0, null);
//
// // fill with paint using src-in
// //g.setComposite(AlphaComposite.SrcIn);
// g.setPaint(paint);
// g.fillRect(0, 0, mask.getWidth(), mask.getHeight());
// g.dispose();
//
// // set the alpha
// int width = masked.getWidth();
// int height = masked.getHeight();
// WritableRaster raster = masked.getRaster();
// WritableRaster alpha = mask.getRaster();
//
// final float[] transparent = new float[4];
// float[] alphaPixel = null;
// for (int y = 0; y < height; y++)
// {
// for (int x = 0; x < width; x++)
// {
// alphaPixel = alpha.getPixel(x, y, alphaPixel);
// if (alphaPixel[0] == 255)
// {
// raster.setPixel(x, y, transparent);
// }
// }
// }
//
// return masked;
// }TODO: PdfBox-Android
/**
* Returns the content of the given image as an AWT buffered image with an RGB color space.
* If a color key mask is provided then an ARGB image is returned instead.
* This method never returns null.
* @param pdImage the image to read
* @param colorKey an optional color key mask
* @return content of this image as an RGB buffered image
* @throws IOException if the image cannot be read
*/
public static Bitmap getRGBImage(PDImage pdImage, COSArray colorKey) throws IOException
{
if (pdImage.isEmpty())
{
throw new IOException("Image stream is empty");
}
// get parameters, they must be valid or have been repaired
// final PDColorSpace colorSpace = pdImage.getColorSpace();
// final int numComponents = colorSpace.getNumberOfComponents();
final int width = pdImage.getWidth();
final int height = pdImage.getHeight();
final int bitsPerComponent = pdImage.getBitsPerComponent();
final float[] decode = getDecodeArray(pdImage);
//
// An AWT raster must use 8/16/32 bits per component. Images with < 8bpc
// will be unpacked into a byte-backed raster. Images with 16bpc will be reduced
// in depth to 8bpc as they will be drawn to TYPE_INT_RGB images anyway. All code
// in PDColorSpace#toRGBImage expects and 8-bit range, i.e. 0-255.
//
Bitmap raster = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
// convert image, faster path for non-decoded, non-colormasked 8-bit images
final float[] defaultDecode = pdImage.getColorSpace().getDefaultDecode(8);
if(pdImage.getSuffix().equals("jpg")) {
return BitmapFactory.decodeStream(pdImage.getStream().createInputStream());
}
else if (bitsPerComponent == 8 && Arrays.equals(decode, defaultDecode) && colorKey == null)
{
return from8bit(pdImage, raster);
}
else if (bitsPerComponent == 1 && colorKey == null)
{
// return from1Bit(pdImage, raster);
return from8bit(pdImage, raster);
}
else
{
Log.e("PdfBox-Android", "Trying to create other-bit image not supported");
// return fromAny(pdImage, raster, colorKey);
return from8bit(pdImage, raster);
}
}
// private static BufferedImage from1Bit(PDImage pdImage, WritableRaster raster)
// throws IOException
// {
// final PDColorSpace colorSpace = pdImage.getColorSpace();
// final int width = pdImage.getWidth();
// final int height = pdImage.getHeight();
// final float[] decode = getDecodeArray(pdImage);
// byte[] output = ((DataBufferByte) raster.getDataBuffer()).getData();
//
// // read bit stream
// InputStream iis = null;
// try
// {
// // create stream
// iis = pdImage.createInputStream();
// final boolean isIndexed = colorSpace instanceof PDIndexed;
//
// int rowLen = width / 8;
// if (width % 8 > 0)
// {
// rowLen++;
// }
//
// // read stream
// byte value0;
// byte value1;
// if (isIndexed || decode[0] < decode[1])
// {
// value0 = 0;
// value1 = (byte) 255;
// }
// else
// {
// value0 = (byte) 255;
// value1 = 0;
// }
// byte[] buff = new byte[rowLen];
// int idx = 0;
// for (int y = 0; y < height; y++)
// {
// int x = 0;
// int readLen = iis.read(buff);
// for (int r = 0; r < rowLen && r < readLen; r++)
// {
// int value = buff[r];
// int mask = 128;
// for (int i = 0; i < 8; i++)
// {
// int bit = value & mask;
// mask >>= 1;
// output[idx++] = bit == 0 ? value0 : value1;
// x++;
// if (x == width)
// {
// break;
// }
// }
// }
// }
// if (readLen != rowLen)
// {
// Log.w("PdfBox-Android", "premature EOF, image will be incomplete");
// break;
// }
//
// // use the color space to convert the image to RGB
// BufferedImage rgbImage = colorSpace.toRGBImage(raster);
//
// return rgbImage;
// }
// finally
// {
// if (iis != null)
// {
// iis.close();
// }
// }
// }TODO: PdfBox-Android
// faster, 8-bit non-decoded, non-colormasked image conversion
private static Bitmap from8bit(PDImage pdImage, Bitmap raster)
throws IOException
{
InputStream input = pdImage.createInputStream();
try
{
// get the raster's underlying byte buffer
// byte[][] banks = ((DataBufferByte) raster.getDataBuffer()).getBankData();
final int width = pdImage.getWidth();
final int height = pdImage.getHeight();
final int numComponents = pdImage.getColorSpace().getNumberOfComponents();
// int max = width * height;
/*
byte[] tempBytes = new byte[numComponents];
for (int i = 0; i < max; i++)
{
input.read(tempBytes);
for (int c = 0; c < numComponents; c++)
{
banks[c][i] = tempBytes[0+c];
}
}
*/
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
if(numComponents == 1) {
int in = input.read();
raster.setPixel(x, y, Color.argb(255, in, in, in));
} else {
raster.setPixel(x, y, Color.argb(255, input.read(), input.read(), input.read()));
}
}
}
// // use the color space to convert the image to RGB
// return pdImage.getColorSpace().toRGBImage(raster);
return raster;
}
finally
{
IOUtils.closeQuietly(input);
}
}
// slower, general-purpose image conversion from any image format
// private static BufferedImage fromAny(PDImage pdImage, WritableRaster raster, COSArray colorKey)
// throws IOException
// {
// final PDColorSpace colorSpace = pdImage.getColorSpace();
// final int numComponents = colorSpace.getNumberOfComponents();
// final int width = pdImage.getWidth();
// final int height = pdImage.getHeight();
// final int bitsPerComponent = pdImage.getBitsPerComponent();
// final float[] decode = getDecodeArray(pdImage);
//
// // read bit stream
// ImageInputStream iis = null;
// try
// {
// // create stream
// iis = new MemoryCacheImageInputStream(pdImage.createInputStream());
// final float sampleMax = (float)Math.pow(2, bitsPerComponent) - 1f;
// final boolean isIndexed = colorSpace instanceof PDIndexed;
//
// // init color key mask
// float[] colorKeyRanges = null;
// BufferedImage colorKeyMask = null;
// if (colorKey != null)
// {
// colorKeyRanges = colorKey.toFloatArray();
// colorKeyMask = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
// }
//
// // calculate row padding
// int padding = 0;
// if (width * numComponents * bitsPerComponent % 8 > 0)
// {
// padding = 8 - (width * numComponents * bitsPerComponent % 8);
// }
//
// // read stream
// byte[] srcColorValues = new byte[numComponents];
// byte[] alpha = new byte[1];
// for (int y = 0; y < height; y++)
// {
// for (int x = 0; x < width; x++)
// {
// boolean isMasked = true;
// for (int c = 0; c < numComponents; c++)
// {
// int value = (int)iis.readBits(bitsPerComponent);
//
// // color key mask requires values before they are decoded
// if (colorKeyRanges != null)
// {
// isMasked &= value >= colorKeyRanges[c * 2] &&
// value <= colorKeyRanges[c * 2 + 1];
// }
//
// // decode array
// final float dMin = decode[c * 2];
// final float dMax = decode[(c * 2) + 1];
//
// // interpolate to domain
// float output = dMin + (value * ((dMax - dMin) / sampleMax));
//
// if (isIndexed)
// {
// // indexed color spaces get the raw value, because the TYPE_BYTE
// // below cannot be reversed by the color space without it having
// // knowledge of the number of bits per component
// srcColorValues[c] = (byte)Math.round(output);
// }
// else
// {
// // interpolate to TYPE_BYTE
// int outputByte = Math.round(((output - Math.min(dMin, dMax)) /
// Math.abs(dMax - dMin)) * 255f);
//
// srcColorValues[c] = (byte)outputByte;
// }
// }
// raster.setDataElements(x, y, srcColorValues);
//
// // set alpha channel in color key mask, if any
// if (colorKeyMask != null)
// {
// alpha[0] = (byte)(isMasked ? 255 : 0);
// colorKeyMask.getRaster().setDataElements(x, y, alpha);
// }
// }
//
// // rows are padded to the nearest byte
// iis.readBits(padding);
// }
//
// // use the color space to convert the image to RGB
// BufferedImage rgbImage = colorSpace.toRGBImage(raster);
//
// // apply color mask, if any
// if (colorKeyMask != null)
// {
// return applyColorKeyMask(rgbImage, colorKeyMask);
// }
// else
// {
// return rgbImage;
// }
// }
// finally
// {
// if (iis != null)
// {
// iis.close();
// }
// }
// }TODO: PdfBox-Android
// color key mask: RGB + Binary -> ARGB
// private static BufferedImage applyColorKeyMask(BufferedImage image, BufferedImage mask)
// throws IOException
// {
// int width = image.getWidth();
// int height = image.getHeight();
//
// // compose to ARGB
// BufferedImage masked = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
//
// WritableRaster src = image.getRaster();
// WritableRaster dest = masked.getRaster();
// WritableRaster alpha = mask.getRaster();
//
// float[] rgb = new float[3];
// float[] rgba = new float[4];
// float[] alphaPixel = null;
// for (int y = 0; y < height; y++)
// {
// for (int x = 0; x < width; x++)
// {
// src.getPixel(x, y, rgb);
//
// rgba[0] = rgb[0];
// rgba[1] = rgb[1];
// rgba[2] = rgb[2];
// alphaPixel = alpha.getPixel(x, y, alphaPixel);
// rgba[3] = 255 - alphaPixel[0];
//
// dest.setPixel(x, y, rgba);
// }
// }
//
// return masked;
// }TODO: PdfBox-Android
// gets decode array from dictionary or returns default
private static float[] getDecodeArray(PDImage pdImage) throws IOException
{
final COSArray cosDecode = pdImage.getDecode();
float[] decode = null;
if (cosDecode != null)
{
int numberOfComponents = pdImage.getColorSpace().getNumberOfComponents();
if (cosDecode.size() != numberOfComponents * 2)
{
if (pdImage.isStencil() && cosDecode.size() >= 2
&& cosDecode.get(0) instanceof COSNumber
&& cosDecode.get(1) instanceof COSNumber)
{
float decode0 = ((COSNumber) cosDecode.get(0)).floatValue();
float decode1 = ((COSNumber) cosDecode.get(1)).floatValue();
if (decode0 >= 0 && decode0 <= 1 && decode1 >= 0 && decode1 <= 1)
{
Log.w("PdfBox-Android", "decode array " + cosDecode
+ " not compatible with color space, using the first two entries");
return new float[]
{
decode0, decode1
};
}
}
Log.e("PdfBox-Android", "decode array " + cosDecode
+ " not compatible with color space, using default");
}
else
{
decode = cosDecode.toFloatArray();
}
}
// use color space default
if (decode == null)
{
return pdImage.getColorSpace().getDefaultDecode(pdImage.getBitsPerComponent());
}
return decode;
}
}