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