blob: 9f0ce3ff7c699d048531a61f871c58d06c9f780a [file] [log] [blame]
/*
* 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.image;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.ImagingOpException;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
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;
import javax.imageio.ImageIO;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSInputStream;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.filter.DecodeOptions;
import org.apache.pdfbox.filter.DecodeResult;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
import org.apache.pdfbox.util.filetypedetector.FileType;
import org.apache.pdfbox.util.filetypedetector.FileTypeDetector;
/**
* An Image XObject.
*
* @author John Hewson
* @author Ben Litchfield
*/
public final class PDImageXObject extends PDXObject implements PDImage
{
/**
* Log instance.
*/
private static final Logger LOG = LogManager.getLogger(PDImageXObject.class);
private SoftReference<BufferedImage> cachedImage;
private PDColorSpace colorSpace;
// initialize to MAX_VALUE as we prefer lower subsampling when keeping/replacing cache.
private int cachedImageSubsampling = Integer.MAX_VALUE;
// indicates whether this image has an JPX-based filter applied
private boolean hasJPXFilter = false;
// is set to true after reading some values from a JPX-based image
private boolean jpxValuesInitialized = false;
/**
* current resource dictionary (has color spaces)
*/
private final PDResources resources;
/**
* Creates an Image XObject in the given document. This constructor is for internal PDFBox use
* and is not for PDF generation. Users who want to create images should look at {@link #createFromFileByExtension(File, PDDocument)
* }.
*
* @param document the current document
*/
public PDImageXObject(PDDocument document)
{
this(new PDStream(document), null);
}
/**
* Creates an Image XObject in the given document using the given filtered stream. This
* constructor is for internal PDFBox use and is not for PDF generation. Users who want to
* create images should look at {@link #createFromFileByExtension(File, PDDocument) }.
*
* @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);
getCOSObject().setItem(COSName.FILTER, cosFilter);
resources = null;
colorSpace = null;
setBitsPerComponent(bitsPerComponent);
setWidth(width);
setHeight(height);
setColorSpace(initColorSpace);
}
/**
* Creates an Image XObject with the given stream as its contents and current color spaces. This
* constructor is for internal PDFBox use and is not for PDF generation. Users who want to
* create images should look at {@link #createFromFileByExtension(File, PDDocument) }.
*
* @param stream the XObject stream to read
* @param resources the current resources
*/
public PDImageXObject(PDStream stream, PDResources resources)
{
super(stream, COSName.IMAGE);
this.resources = resources;
List<COSName> filters = stream.getFilters();
if (!filters.isEmpty() && COSName.JPX_DECODE.equals(filters.get(filters.size() - 1)))
{
hasJPXFilter = true;
}
}
/**
* Creates a thumbnail Image XObject from the given COSBase and name.
* @param cosStream the COS stream
* @return an XObject
*/
public static PDImageXObject createThumbnail(COSStream cosStream)
{
// thumbnails are special, any non-null subtype is treated as being "Image"
PDStream pdStream = new PDStream(cosStream);
return new PDImageXObject(pdStream, null);
}
/**
* Creates a COS stream from raw (encoded) data.
*/
private static COSStream createRawStream(PDDocument document, InputStream rawInput)
throws IOException
{
COSStream stream = document.getDocument().createCOSStream();
try (OutputStream output = stream.createRawOutputStream())
{
rawInput.transferTo(output);
}
return stream;
}
/**
* Create a PDImageXObject from an image file, see {@link #createFromFileByExtension(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 createFromFileByExtension(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). Starting with 2.0.18, this call will create an image
* directly from a PNG file without decoding it (when possible), which is faster. However the
* result size depends on the compression skill of the software that created the PNG file. If
* file size or bandwidth are important to you or to your clients, then create your PNG files
* with a tool that has implemented the
* <a href="https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/">Zopfli
* algorithm</a>, or use the two-step process mentioned above.
*
* @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 createFromFileByExtension(File file, PDDocument doc) throws IOException
{
String name = file.getName();
int dot = name.lastIndexOf('.');
if (dot == -1)
{
throw new IllegalArgumentException("Image type not supported: " + name);
}
String ext = name.substring(dot + 1).toLowerCase();
if ("jpg".equals(ext) || "jpeg".equals(ext))
{
try (FileInputStream fis = new FileInputStream(file))
{
return JPEGFactory.createFromStream(doc, fis);
}
}
if ("tif".equals(ext) || "tiff".equals(ext))
{
try
{
return CCITTFactory.createFromFile(doc, file);
}
catch (IOException ex)
{
LOG.debug("Reading as TIFF failed, setting fileType to PNG", ex);
// Plan B: try reading with ImageIO
// common exception:
// First image in tiff is not CCITT T4 or T6 compressed
ext = "png";
}
}
if ("gif".equals(ext) || "bmp".equals(ext) || "png".equals(ext))
{
BufferedImage bim = ImageIO.read(file);
return LosslessFactory.createFromImage(doc, bim);
}
throw new IllegalArgumentException("Image type not supported: " + name);
}
/**
* Create a PDImageXObject from an image file. The file format is determined by the file
* content. The following file types 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). Starting with 2.0.18, this call will create an image
* directly from a PNG file without decoding it (when possible), which is faster. However the
* result size depends on the compression skill of the software that created the PNG file. If
* file size or bandwidth are important to you or to your clients, then create your PNG files
* with a tool that has implemented the
* <a href="https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/">Zopfli
* algorithm</a>, or use the two-step process mentioned above.
*
* @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 createFromFileByContent(File file, PDDocument doc) throws IOException
{
FileType fileType = null;
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(file)))
{
fileType = FileTypeDetector.detectFileType(bufferedInputStream);
}
catch (IOException e)
{
throw new IOException("Could not determine file type: " + file.getName(), e);
}
if (fileType == null)
{
throw new IllegalArgumentException("Image type not supported: " + file.getName());
}
if (fileType == FileType.JPEG)
{
try (FileInputStream fis = new FileInputStream(file))
{
return JPEGFactory.createFromStream(doc, fis);
}
}
if (fileType == FileType.TIFF)
{
try
{
return CCITTFactory.createFromFile(doc, file);
}
catch (IOException ex)
{
LOG.debug("Reading as TIFF failed, setting fileType to PNG", ex);
// Plan B: try reading with ImageIO
// common exception:
// First image in tiff is not CCITT T4 or T6 compressed
fileType = FileType.PNG;
}
}
if (fileType == FileType.BMP || fileType == FileType.GIF || fileType == FileType.PNG)
{
BufferedImage bim = ImageIO.read(file);
return LosslessFactory.createFromImage(doc, bim);
}
throw new IllegalArgumentException("Image type " + fileType + " not supported: " + file.getName());
}
/**
* Create a PDImageXObject from bytes of an image file. The file format is determined by the
* file content. The following file types are supported: JPG, JPEG, TIF, TIFF, GIF, BMP and PNG.
* This is a convenience method that calls {@link JPEGFactory#createFromByteArray},
* {@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). Starting with 2.0.18, this call will create an image
* directly from a PNG file without decoding it (when possible), which is faster. However the
* result size depends on the compression skill of the software that created the PNG file. If
* file size or bandwidth are important to you or to your clients, then create your PNG files
* with a tool that has implemented the
* <a href="https://blog.codinghorror.com/zopfli-optimization-literally-free-bandwidth/">Zopfli
* algorithm</a>, or use the two-step process mentioned above.
*
* @param byteArray bytes from an image file.
* @param document the document that shall use this PDImageXObject.
* @param name name of image file for exception messages, can be null.
* @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 createFromByteArray(PDDocument document, byte[] byteArray, String name) throws IOException
{
FileType fileType = FileTypeDetector.detectFileType(byteArray);
if (fileType == null)
{
throw new IllegalArgumentException("Image type not supported: " + name);
}
if (fileType == FileType.JPEG)
{
return JPEGFactory.createFromByteArray(document, byteArray);
}
if (fileType == FileType.PNG)
{
// Try to directly convert the image without recoding it.
PDImageXObject image = PNGConverter.convertPNGImage(document, byteArray);
if (image != null)
{
return image;
}
}
if (fileType == FileType.TIFF)
{
try
{
return CCITTFactory.createFromByteArray(document, byteArray);
}
catch (IOException ex)
{
LOG.debug("Reading as TIFF failed, setting fileType to PNG", ex);
// Plan B: try reading with ImageIO
// common exception:
// First image in tiff is not CCITT T4 or T6 compressed
fileType = FileType.PNG;
}
}
if (fileType == FileType.BMP || fileType == FileType.GIF || fileType == FileType.PNG)
{
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
BufferedImage bim = ImageIO.read(bais);
return LosslessFactory.createFromImage(document, bim);
}
throw new IllegalArgumentException("Image type " + fileType + " not supported: " + name);
}
/**
* 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 = getCOSObject().getCOSStream(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)
{
getCOSObject().setItem(COSName.METADATA, meta);
}
/**
* Returns the key of this XObject in the structural parent tree.
*
* @return this object's key the structural parent tree or -1 if there isn't any.
*/
public int getStructParent()
{
return getCOSObject().getInt(COSName.STRUCT_PARENT);
}
/**
* Sets the key of this XObject in the structural parent tree.
* @param key the new key for this XObject
*/
public void setStructParent(int key)
{
getCOSObject().setInt(COSName.STRUCT_PARENT, key);
}
/**
* {@inheritDoc}
* The returned images are cached via a SoftReference.
*/
@Override
public BufferedImage getImage() throws IOException
{
return getImage(null, 1);
}
/**
* {@inheritDoc}
*/
@Override
public BufferedImage getImage(Rectangle region, int subsampling) throws IOException
{
if (region == null && subsampling == cachedImageSubsampling && cachedImage != null)
{
BufferedImage cached = cachedImage.get();
if (cached != null)
{
return cached;
}
}
initJPXValues();
// get RGB image w/o reference because applyMask might modify it, take long time and a lot of memory.
final BufferedImage image;
final PDImageXObject softMask = getSoftMask();
final PDImageXObject mask = getMask();
// soft mask (overrides explicit mask)
if (softMask != null)
{
image = applyMask(SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask()),
softMask.getOpaqueImage(region, subsampling), softMask.getInterpolate(), true,
extractMatte(softMask));
}
// explicit mask - to be applied only if /ImageMask true
else if (mask != null && mask.isStencil())
{
image = applyMask(SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask()),
mask.getOpaqueImage(region, subsampling), mask.getInterpolate(), false, null);
}
else
{
image = SampledImageReader.getRGBImage(this, region, subsampling, getColorKeyMask());
}
if (region == null && subsampling <= cachedImageSubsampling)
{
// only cache full-image renders, and prefer lower subsampling frequency, as lower
// subsampling means higher quality and longer render times.
cachedImageSubsampling = subsampling;
cachedImage = new SoftReference<>(image);
}
return image;
}
@Override
public BufferedImage getRawImage() throws IOException
{
return getColorSpace().toRawImage(getRawRaster());
}
@Override
public WritableRaster getRawRaster() throws IOException
{
return SampledImageReader.getRawRaster(this);
}
/**
* Extract the matte color from a softmask.
*
* @param softMask
* @return the matte color.
* @throws IOException if the color conversion fails.
*/
private float[] extractMatte(PDImageXObject softMask) throws IOException
{
COSBase base = softMask.getCOSObject().getItem(COSName.MATTE);
float[] matte = null;
if (base instanceof COSArray)
{
// PDFBOX-4267: process /Matte
// see PDF specification 1.7, 11.6.5.3 Soft-Mask Images
matte = ((COSArray) base).toFloatArray();
// convert to RGB
if (matte.length < getColorSpace().getNumberOfComponents())
{
LOG.error("Image /Matte entry not long enough for colorspace, skipped");
return null;
}
matte = getColorSpace().toRGB(matte);
}
return matte;
}
/**
* {@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);
}
/**
* 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 BufferedImage getOpaqueImage() throws IOException
{
return getOpaqueImage(null, 1);
}
/**
* 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.
*
* @param region The region of the source image to get, or null if the entire image is needed. The actual region
* will be clipped to the dimensions of the source image.
*
* @param subsampling The amount of rows and columns to advance for every output pixel, a value of 1 meaning every
* pixel will be read. It must not be larger than the image width or height.
*
* @return the image without any masks applied
* @throws IOException if the image cannot be read
*/
public BufferedImage getOpaqueImage(Rectangle region, int subsampling) throws IOException
{
return SampledImageReader.getRGBImage(this, region, subsampling, null);
}
/**
* @param image The image to apply the mask to as alpha channel.
* @param mask A mask image in 8 bit Gray. Even for a stencil mask image due to
* {@link #getOpaqueImage()} and {@link SampledImageReader}'s {@code from1Bit()} special
* handling of DeviceGray.
* @param interpolateMask interpolation flag of the mask image.
* @param isSoft {@code true} if a soft mask. If not stencil mask, then alpha will be inverted
* by this method.
* @param matte an optional RGB matte if a soft mask.
* @return an ARGB image (can be the altered original image)
*/
private BufferedImage applyMask(BufferedImage image, BufferedImage mask, boolean interpolateMask,
boolean isSoft, float[] matte)
{
if (mask == null)
{
return image;
}
final int width = Math.max(image.getWidth(), mask.getWidth());
final int height = Math.max(image.getHeight(), mask.getHeight());
// scale mask to fit image, or image to fit mask, whichever is larger.
// also make sure that mask is 8 bit gray and image is ARGB as this
// is what needs to be returned.
if (mask.getWidth() < width || mask.getHeight() < height)
{
mask = scaleImage(mask, width, height, BufferedImage.TYPE_BYTE_GRAY, interpolateMask);
}
else if (mask.getType() != BufferedImage.TYPE_BYTE_GRAY)
{
mask = scaleImage(mask, width, height, BufferedImage.TYPE_BYTE_GRAY, false);
}
if (image.getWidth() < width || image.getHeight() < height)
{
image = scaleImage(image, width, height, BufferedImage.TYPE_INT_ARGB, getInterpolate());
}
else if (image.getType() != BufferedImage.TYPE_INT_ARGB)
{
image = scaleImage(image, width, height, BufferedImage.TYPE_INT_ARGB, false);
}
// compose alpha into ARGB image, either:
// - very fast by direct bit combination if not a soft mask and a 8 bit alpha source.
// - fast by letting the sample model do a bulk band operation if no matte is set.
// - slow and complex by matte calculations on individual pixel components.
final WritableRaster raster = image.getRaster();
final WritableRaster alpha = mask.getRaster();
if (!isSoft && raster.getDataBuffer().getSize() == alpha.getDataBuffer().getSize())
{
final DataBuffer dst = raster.getDataBuffer();
final DataBuffer src = alpha.getDataBuffer();
for (int i = 0, c = dst.getSize(); c > 0; i++, c--)
{
dst.setElem(i, dst.getElem(i) & 0xffffff | ~src.getElem(i) << 24);
}
}
else if (matte == null)
{
final int[] samples = new int[width];
for (int y = 0; y < height; y++)
{
alpha.getSamples(0, y, width, 1, 0, samples);
if (!isSoft)
{
for (int x = 0; x < width; x++)
{
samples[x] ^= -1;
}
}
raster.setSamples(0, y, width, 1, 3, samples);
}
}
else
{
final int[] alphas = new int[width];
final int[] pixels = new int[4 * width];
// Original code is to clamp component and alpha to [0f, 1f] as matte is,
// and later expand to [0; 255] again (with rounding).
// component = 255f * ((component / 255f - matte) / (alpha / 255f) + matte)
// = (255 * component - 255 * 255f * matte) / alpha + 255f * matte
// There is a clearly visible factor 255 for most components in above formula,
// i.e. max value is 255 * 255: 16 bits + sign.
// Let's use faster fixed point integer arithmetics with Q16.15,
// introducing neglible errors (0.001%).
// Note: For "correct" rounding we increase the final matte value (m0h, m1h, m2h) by
// a half an integer.
final int fraction = 15;
final int factor = 255 << fraction;
final int m0 = Math.round(factor * matte[0]) * 255;
final int m1 = Math.round(factor * matte[1]) * 255;
final int m2 = Math.round(factor * matte[2]) * 255;
final int m0h = m0 / 255 + (1 << fraction - 1);
final int m1h = m1 / 255 + (1 << fraction - 1);
final int m2h = m2 / 255 + (1 << fraction - 1);
for (int y = 0; y < height; y++)
{
raster.getPixels(0, y, width, 1, pixels);
alpha.getSamples(0, y, width, 1, 0, alphas);
int offset = 0;
for (int x = 0; x < width; x++)
{
int a = alphas[x];
if (a == 0)
{
offset += 3;
}
else
{
pixels[offset] = clampColor(((pixels[offset++] * factor - m0) / a + m0h) >> fraction);
pixels[offset] = clampColor(((pixels[offset++] * factor - m1) / a + m1h) >> fraction);
pixels[offset] = clampColor(((pixels[offset++] * factor - m2) / a + m2h) >> fraction);
}
pixels[offset++] = a;
}
raster.setPixels(0, y, width, 1, pixels);
}
}
return image;
}
private static int clampColor(int color)
{
return color < 0 ? 0 : color > 255 ? 255 : color;
}
private void initJPXValues()
{
if (!hasJPXFilter || jpxValuesInitialized)
{
return;
}
// some of the dictionary values of the COSStream may be overwritten by values which are extracted from the
// image itself, such as
// width and height of the image
// bits per component
// the colorspace of the image is used if the dictionary doesn't provide any value
PDStream stream = getStream();
try (COSInputStream is = stream.createInputStream())
{
DecodeResult decodeResult = is.getDecodeResult();
stream.getCOSObject().addAll(decodeResult.getParameters());
if (colorSpace == null)
{
colorSpace = decodeResult.getJPXColorSpace();
}
jpxValuesInitialized = true;
}
catch (IOException exception)
{
LOG.debug("Can't initialize JPX based values", exception);
}
}
/**
* High-quality image scaling.
*/
private static BufferedImage scaleImage(BufferedImage image, int width, int height, int type, boolean interpolate)
{
final int imgWidth = image.getWidth();
final int imgHeight = image.getHeight();
// largeScale switch is arbitrarily chosen as to where bicubic becomes very slow
boolean largeScale = width * height > 3000 * 3000 * (type == BufferedImage.TYPE_BYTE_GRAY ? 3 : 1);
interpolate &= imgWidth != width || imgHeight != height;
BufferedImage image2 = new BufferedImage(width, height, type);
if (interpolate)
{
AffineTransform af = AffineTransform.getScaleInstance((double) width / imgWidth, (double) height / imgHeight);
AffineTransformOp afo = new AffineTransformOp(af, largeScale ? AffineTransformOp.TYPE_BILINEAR : AffineTransformOp.TYPE_BICUBIC);
try
{
afo.filter(image, image2);
return image2;
}
catch (ImagingOpException e)
{
LOG.warn(e.getMessage(), e);
}
}
Graphics2D g = image2.createGraphics();
if (interpolate)
{
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
largeScale ? RenderingHints.VALUE_INTERPOLATION_BILINEAR : RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_RENDERING,
largeScale ? RenderingHints.VALUE_RENDER_DEFAULT : RenderingHints.VALUE_RENDER_QUALITY);
}
g.drawImage(image, 0, 0, width, height, 0, 0, imgWidth, imgHeight, null);
g.dispose();
return image2;
}
/**
* Returns the Mask Image XObject associated with this image, or null if there is none.
*
* @return Mask Image XObject
* @throws java.io.IOException if the mask data could not be read
*/
public PDImageXObject getMask() throws IOException
{
COSArray mask = getCOSObject().getCOSArray(COSName.MASK);
if (mask != null)
{
// color key mask, no explicit mask to return
return null;
}
else
{
COSStream cosStream = getCOSObject().getCOSStream(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()
{
return getCOSObject().getCOSArray(COSName.MASK);
}
/**
* Returns the Soft Mask Image XObject associated with this image, or null if there is none.
*
* @return the SMask Image XObject, or null.
* @throws java.io.IOException if the soft mask data could not be read
*/
public PDImageXObject getSoftMask() throws IOException
{
COSStream cosStream = getCOSObject().getCOSStream(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
{
initJPXValues();
return getCOSObject().getInt(COSName.BITS_PER_COMPONENT, COSName.BPC);
}
}
@Override
public void setBitsPerComponent(int bpc)
{
getCOSObject().setInt(COSName.BITS_PER_COMPONENT, bpc);
}
@Override
public PDColorSpace getColorSpace() throws IOException
{
if (colorSpace == null)
{
COSBase cosBase = getCOSObject().getItem(COSName.COLORSPACE, COSName.CS);
if (cosBase != null)
{
COSObject indirect = null;
if (cosBase instanceof COSObject &&
resources != null && resources.getResourceCache() != null)
{
// PDFBOX-4022: use the resource cache because several images
// might have the same colorspace indirect object.
indirect = (COSObject) cosBase;
colorSpace = resources.getResourceCache().getColorSpace(indirect);
if (colorSpace != null)
{
return colorSpace;
}
}
colorSpace = PDColorSpace.create(cosBase, resources);
if (indirect != null)
{
resources.getResourceCache().put(indirect, colorSpace);
}
}
else if (isStencil())
{
// stencil mask color space must be gray, it is often missing
colorSpace = PDDeviceGray.INSTANCE;
}
else
{
initJPXValues();
}
if (colorSpace == null)
{
// 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(DecodeOptions options) throws IOException
{
return getStream().createInputStream(options);
}
@Override
public InputStream createInputStream(List<String> stopFilters) throws IOException
{
return getStream().createInputStream(stopFilters);
}
@Override
public boolean isEmpty()
{
return getStream().getCOSObject().getLength() == 0;
}
@Override
public void setColorSpace(PDColorSpace cs)
{
getCOSObject().setItem(COSName.COLORSPACE, cs != null ? cs.getCOSObject() : null);
colorSpace = null;
cachedImage = null;
}
@Override
public int getHeight()
{
initJPXValues();
return getCOSObject().getInt(COSName.HEIGHT);
}
@Override
public void setHeight(int h)
{
getCOSObject().setInt(COSName.HEIGHT, h);
}
@Override
public int getWidth()
{
initJPXValues();
return getCOSObject().getInt(COSName.WIDTH);
}
@Override
public void setWidth(int w)
{
getCOSObject().setInt(COSName.WIDTH, w);
}
@Override
public boolean getInterpolate()
{
return getCOSObject().getBoolean(COSName.INTERPOLATE, false);
}
@Override
public void setInterpolate(boolean value)
{
getCOSObject().setBoolean(COSName.INTERPOLATE, value);
}
@Override
public void setDecode(COSArray decode)
{
getCOSObject().setItem(COSName.DECODE, decode);
}
@Override
public COSArray getDecode()
{
return getCOSObject().getCOSArray(COSName.DECODE);
}
@Override
public boolean isStencil()
{
return getCOSObject().getBoolean(COSName.IMAGE_MASK, false);
}
@Override
public void setStencil(boolean isStencil)
{
getCOSObject().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.isEmpty())
{
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 if (filters.contains(COSName.JBIG2_DECODE))
{
return "jb2";
}
else
{
LOG.warn("getSuffix() returns null, filters: {}", filters);
return null;
}
}
/**
* This will get the optional content group or optional content membership dictionary.
*
* @return The optional content group or optional content membership dictionary or null if there
* is none.
*/
public PDPropertyList getOptionalContent()
{
COSDictionary optionalContent = getCOSObject().getCOSDictionary(COSName.OC);
return optionalContent != null ? PDPropertyList.create(optionalContent) : null;
}
/**
* Sets the optional content group or optional content membership dictionary.
*
* @param oc The optional content group or optional content membership dictionary.
*/
public void setOptionalContent(PDPropertyList oc)
{
getCOSObject().setItem(COSName.OC, oc);
}
}