/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.pdfbox.pdmodel.graphics.xobject;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.ImageIO;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.common.function.PDFunction;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceN;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
import org.apache.pdfbox.pdmodel.graphics.color.PDSeparation;
import org.apache.pdfbox.util.ImageIOUtil;
/**
* An image class for JPegs.
*
* @author mathiak
* @version $Revision: 1.5 $
*/
public class PDJpeg extends PDXObjectImage
{
private BufferedImage image = null;
private static final String JPG = "jpg";
private static final List<String> DCT_FILTERS = new ArrayList<String>();
private static final float DEFAULT_COMPRESSION_LEVEL = 0.75f;
static
{
DCT_FILTERS.add( COSName.DCT_DECODE.getName() );
DCT_FILTERS.add( COSName.DCT_DECODE_ABBREVIATION.getName() );
}
/**
* Standard constructor.
*
* @param jpeg The COSStream from which to extract the JPeg
*/
public PDJpeg(PDStream jpeg)
{
super(jpeg, JPG);
}
/**
* Construct from a stream.
*
* @param doc The document to create the image as part of.
* @param is The stream that contains the jpeg data.
* @throws IOException If there is an error reading the jpeg data.
*/
public PDJpeg( PDDocument doc, InputStream is ) throws IOException
{
super( new PDStream( doc, is, true ), JPG);
COSDictionary dic = getCOSStream();
dic.setItem( COSName.FILTER, COSName.DCT_DECODE );
dic.setItem( COSName.SUBTYPE, COSName.IMAGE);
dic.setItem( COSName.TYPE, COSName.XOBJECT );
getRGBImage();
if (image != null)
{
setBitsPerComponent( 8 );
setColorSpace( PDDeviceRGB.INSTANCE );
setHeight( image.getHeight() );
setWidth( image.getWidth() );
}
}
/**
* Construct from a buffered image.
* The default compression level of 0.75 will be used.
*
* @param doc The document to create the image as part of.
* @param bi The image to convert to a jpeg
* @throws IOException If there is an error processing the jpeg data.
*/
public PDJpeg( PDDocument doc, BufferedImage bi ) throws IOException
{
super( new PDStream( doc ) , JPG);
createImageStream(doc, bi, DEFAULT_COMPRESSION_LEVEL);
}
/**
* Construct from a buffered image.
*
* @param doc The document to create the image as part of.
* @param bi The image to convert to a jpeg
* @param compressionQuality The quality level which is used to compress the image
* @throws IOException If there is an error processing the jpeg data.
*/
public PDJpeg( PDDocument doc, BufferedImage bi, float compressionQuality ) throws IOException
{
super( new PDStream( doc ), JPG);
createImageStream(doc, bi, compressionQuality);
}
private void createImageStream(PDDocument doc, BufferedImage bi, float compressionQuality) throws IOException
{
BufferedImage alpha = null;
if (bi.getColorModel().hasAlpha())
{
// extract the alpha information
WritableRaster alphaRaster = bi.getAlphaRaster();
ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY),
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
alpha = new BufferedImage(cm, alphaRaster, false, null);
// create a RGB image without alpha
image = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setComposite(AlphaComposite.Src);
g.drawImage(bi, 0, 0, null);
bi = image;
}
java.io.OutputStream os = getCOSStream().createFilteredStream();
try
{
ImageIOUtil.writeImage(bi, JPG, os);
COSDictionary dic = getCOSStream();
dic.setItem( COSName.FILTER, COSName.DCT_DECODE );
dic.setItem( COSName.SUBTYPE, COSName.IMAGE);
dic.setItem( COSName.TYPE, COSName.XOBJECT );
PDXObjectImage alphaPdImage = null;
if(alpha != null)
{
alphaPdImage = new PDJpeg(doc, alpha, compressionQuality);
dic.setItem(COSName.SMASK, alphaPdImage);
}
setBitsPerComponent( 8 );
if (bi.getColorModel().getNumComponents() == 3)
{
setColorSpace( PDDeviceRGB.INSTANCE );
}
else
{
if (bi.getColorModel().getNumComponents() == 1)
{
setColorSpace( new PDDeviceGray() );
}
else
{
throw new IllegalStateException();
}
}
setHeight( bi.getHeight() );
setWidth( bi.getWidth() );
}
finally
{
os.close();
}
}
/**
* Returns an image of the JPeg, or null if JPegs are not supported. (They should be. )
* {@inheritDoc}
*/
public BufferedImage getRGBImage() throws IOException
{
if (image != null)
{
return image;
}
BufferedImage bi = null;
boolean readError = false;
ByteArrayOutputStream os = new ByteArrayOutputStream();
removeAllFiltersButDCT(os);
os.close();
byte[] img = os.toByteArray();
PDColorSpace cs = getColorSpace();
try
{
if (cs instanceof PDDeviceCMYK
|| (cs instanceof PDICCBased && cs.getNumberOfComponents() == 4))
{
// create BufferedImage based on the converted color values
bi = convertCMYK2RGB(readImage(img), cs);
}
else if (cs instanceof PDSeparation)
{
// create BufferedImage based on the converted color values
bi = processTintTransformation(readImage(img),
((PDSeparation)cs).getTintTransform(), cs.getJavaColorSpace());
}
else if (cs instanceof PDDeviceN)
{
// create BufferedImage based on the converted color values
bi = processTintTransformation(readImage(img),
((PDDeviceN)cs).getTintTransform(), cs.getJavaColorSpace());
}
else
{
ByteArrayInputStream bai = new ByteArrayInputStream(img);
bi = ImageIO.read(bai);
}
}
catch(IIOException exception)
{
readError = true;
}
// 2. try to read jpeg again. some jpegs have some strange header containing
// "Adobe " at some place. so just replace the header with a valid jpeg header.
// TODO : not sure if it works for all cases
if (bi == null && readError)
{
byte[] newImage = replaceHeader(img);
ByteArrayInputStream bai = new ByteArrayInputStream(newImage);
bi = ImageIO.read(bai);
}
// If there is a 'soft mask' image then we use that as a transparency mask.
PDXObjectImage smask = getSMaskImage();
if (smask != null)
{
BufferedImage smaskBI = smask.getRGBImage();
COSArray decodeArray = smask.getDecode();
CompositeImage compositeImage = new CompositeImage(bi, smaskBI);
BufferedImage rgbImage = compositeImage.createMaskedImage(decodeArray);
image = rgbImage;
}
else
{
// But if there is no soft mask, use the unaltered image.
image = bi;
}
return image;
}
/**
* This writes the JPeg to out.
* {@inheritDoc}
*/
public void write2OutputStream(OutputStream out) throws IOException
{
getRGBImage();
if (image != null)
{
ImageIOUtil.writeImage(image, JPG, out);
}
}
private void removeAllFiltersButDCT(OutputStream out) throws IOException
{
InputStream data = getPDStream().getPartiallyFilteredStream( DCT_FILTERS );
byte[] buf = new byte[1024];
int amountRead = -1;
while( (amountRead = data.read( buf )) != -1 )
{
out.write( buf, 0, amountRead );
}
}
private int getHeaderEndPos(byte[] imageAsBytes)
{
for (int i = 0; i < imageAsBytes.length; i++)
{
byte b = imageAsBytes[i];
if (b == (byte) 0xDB)
{
// TODO : check for ff db
return i -2;
}
}
return 0;
}
private byte[] replaceHeader(byte[] imageAsBytes)
{
// get end position of wrong header respectively startposition of "real jpeg data"
int pos = getHeaderEndPos(imageAsBytes);
// simple correct header
byte[] header = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, (byte) 0x00,
(byte) 0x10, (byte) 0x4A, (byte) 0x46, (byte) 0x49, (byte) 0x46, (byte) 0x00, (byte) 0x01,
(byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x60, (byte) 0x00, (byte) 0x60, (byte) 0x00, (byte) 0x00};
// concat
byte[] newImage = new byte[imageAsBytes.length - pos + header.length - 1];
System.arraycopy(header, 0, newImage, 0, header.length);
System.arraycopy(imageAsBytes, pos + 1, newImage, header.length, imageAsBytes.length - pos - 1);
return newImage;
}
private Raster readImage(byte[] bytes) throws IOException
{
ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes));
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (readers == null || !readers.hasNext())
{
throw new RuntimeException("No ImageReaders found");
}
// read the raster information only
// avoid to access the meta information
ImageReader reader = (ImageReader) readers.next();
reader.setInput(input);
Raster raster = reader.readRaster(0, reader.getDefaultReadParam());
if (input != null)
{
input.close();
}
reader.dispose();
return raster;
}
// CMYK jpegs are not supported by JAI, so that we have to do the conversion on our own
private BufferedImage convertCMYK2RGB(Raster raster, PDColorSpace colorspace) throws IOException
{
// create a java color space to be used for conversion
ColorSpace cs = colorspace.getJavaColorSpace();
int width = raster.getWidth();
int height = raster.getHeight();
byte[] rgb = new byte[width * height * 3];
int rgbIndex = 0;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// get the source color values
float[] srcColorValues = raster.getPixel(j,i, (float[])null);
// convert values from 0..255 to 0..1
for (int k = 0; k < 4; k++)
{
srcColorValues[k] /= 255f;
}
// convert CMYK to RGB
float[] rgbValues = cs.toRGB(srcColorValues);
// convert values from 0..1 to 0..255
for (int k = 0; k < 3; k++)
{
rgb[rgbIndex+k] = (byte)(rgbValues[k] * 255);
}
rgbIndex +=3;
}
}
// create a RGB color model
ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB),
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
// create the target raster
WritableRaster writeableRaster = cm.createCompatibleWritableRaster(width, height);
// get the data buffer of the raster
DataBufferByte buffer = (DataBufferByte)writeableRaster.getDataBuffer();
byte[] bufferData = buffer.getData();
// copy all the converted data to the raster buffer
System.arraycopy( rgb, 0,bufferData, 0,rgb.length );
// create an image using the converted color values
return new BufferedImage(cm, writeableRaster, true, null);
}
// Separation and DeviceN colorspaces are using a tint transform function to convert color values
private BufferedImage processTintTransformation(Raster raster, PDFunction function, ColorSpace colorspace)
throws IOException
{
int numberOfInputValues = function.getNumberOfInputParameters();
int numberOfOutputValues = function.getNumberOfOutputParameters();
int width = raster.getWidth();
int height = raster.getHeight();
byte[] sourceBuffer = new byte[width * height * numberOfOutputValues];
int bufferIndex = 0;
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
// get the source color values
float[] srcColorValues = raster.getPixel(j,i, (float[])null);
// convert values from 0..255 to 0..1
for (int k = 0; k < numberOfInputValues; k++)
{
srcColorValues[k] /= 255f;
}
// transform the color values using the tint function
float[] convertedValues = function.eval(srcColorValues);
// convert values from 0..1 to 0..255
for (int k = 0; k < numberOfOutputValues; k++)
{
sourceBuffer[bufferIndex+k] = (byte)(convertedValues[k] * 255);
}
bufferIndex +=numberOfOutputValues;
}
}
// create a target color model
ColorModel cm = new ComponentColorModel(colorspace,
false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
// create the target raster
WritableRaster writeableRaster = cm.createCompatibleWritableRaster(width, height);
// get the data buffer of the raster
DataBufferByte buffer = (DataBufferByte)writeableRaster.getDataBuffer();
byte[] bufferData = buffer.getData();
// copy all the converted data to the raster buffer
System.arraycopy( sourceBuffer, 0,bufferData, 0,sourceBuffer.length );
// create an image using the converted color values
return new BufferedImage(cm, writeableRaster, true, null);
}
}