PDFRenderer.java
/*
* 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.rendering;
import java.awt.Color;
import java.awt.DisplayMode;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter;
/**
* Renders a PDF document to an AWT BufferedImage.
* This class may be overridden in order to perform custom rendering.
*
* @author John Hewson
*/
public class PDFRenderer
{
private static final Log LOG = LogFactory.getLog(PDFRenderer.class);
protected final PDDocument document;
// TODO keep rendering state such as caches here
/**
* Default annotations filter, returns all annotations
*/
private AnnotationFilter annotationFilter = annotation -> true;
private boolean subsamplingAllowed = false;
private RenderDestination defaultDestination;
private RenderingHints renderingHints = null;
private BufferedImage pageImage;
private static boolean kcmsLogged = false;
private float imageDownscalingOptimizationThreshold = 0.5f;
private final PDPageTree pageTree;
/**
* Creates a new PDFRenderer.
* @param document the document to render
*/
public PDFRenderer(PDDocument document)
{
this.document = document;
this.pageTree = document.getPages();
if (!kcmsLogged)
{
suggestKCMS();
kcmsLogged = true;
}
}
/**
* Return the AnnotationFilter.
*
* @return the AnnotationFilter
*/
public AnnotationFilter getAnnotationsFilter()
{
return annotationFilter;
}
/**
* Set the AnnotationFilter.
*
* <p>Allows to only render annotation accepted by the filter.
*
* @param annotationsFilter the AnnotationFilter
*/
public void setAnnotationsFilter(AnnotationFilter annotationsFilter)
{
this.annotationFilter = annotationsFilter;
}
/**
* Value indicating if the renderer is allowed to subsample images before drawing, according to
* image dimensions and requested scale.
*
* Subsampling may be faster and less memory-intensive in some cases, but it may also lead to
* loss of quality, especially in images with high spatial frequency.
*
* @return true if subsampling of images is allowed, false otherwise.
*/
public boolean isSubsamplingAllowed()
{
return subsamplingAllowed;
}
/**
* Sets a value instructing the renderer whether it is allowed to subsample images before
* drawing. The subsampling frequency is determined according to image size and requested scale.
*
* Subsampling may be faster and less memory-intensive in some cases, but it may also lead to
* loss of quality, especially in images with high spatial frequency.
*
* @param subsamplingAllowed The new value indicating if subsampling is allowed.
*/
public void setSubsamplingAllowed(boolean subsamplingAllowed)
{
this.subsamplingAllowed = subsamplingAllowed;
}
/**
* @return the defaultDestination
*/
public RenderDestination getDefaultDestination()
{
return defaultDestination;
}
/**
* @param defaultDestination the defaultDestination to set
*/
public void setDefaultDestination(RenderDestination defaultDestination)
{
this.defaultDestination = defaultDestination;
}
/**
* Get the rendering hints.
*
* @return the rendering hints or null if none are set.
*/
public RenderingHints getRenderingHints()
{
return renderingHints;
}
/**
* Set the rendering hints. Use this to influence rendering quality and speed. If you don't set
* them yourself or pass null, PDFBox will decide <b><u>at runtime</u></b> depending on the
* destination.
*
* @param renderingHints
*/
public void setRenderingHints(RenderingHints renderingHints)
{
this.renderingHints = renderingHints;
}
/**
*
* @return get the image downscaling optimization threshold. See
* {@link #getImageDownscalingOptimizationThreshold()} for details.
*/
public float getImageDownscalingOptimizationThreshold()
{
return imageDownscalingOptimizationThreshold;
}
/**
* Set the image downscaling optimization threshold. This must be a value between 0 and 1. When
* rendering downscaled images and rendering hints are set to bicubic+quality and the scaling is
* smaller than the threshold, a more quality-optimized but slower method will be used. The
* default is 0.5 which is a good compromise.
*
* @param imageDownscalingOptimizationThreshold
*/
public void setImageDownscalingOptimizationThreshold(float imageDownscalingOptimizationThreshold)
{
this.imageDownscalingOptimizationThreshold = imageDownscalingOptimizationThreshold;
}
/**
* Returns the given page as an RGB image at 72 DPI
* @param pageIndex the zero-based index of the page to be converted.
* @return the rendered page image
* @throws IOException if the PDF cannot be read
*/
public BufferedImage renderImage(int pageIndex) throws IOException
{
return renderImage(pageIndex, 1);
}
/**
* Returns the given page as an RGB image at the given scale.
* A scale of 1 will render at 72 DPI.
* @param pageIndex the zero-based index of the page to be converted
* @param scale the scaling factor, where 1 = 72 DPI
* @return the rendered page image
* @throws IOException if the PDF cannot be read
*/
public BufferedImage renderImage(int pageIndex, float scale) throws IOException
{
return renderImage(pageIndex, scale, ImageType.RGB);
}
/**
* Returns the given page as an RGB image at the given DPI.
* @param pageIndex the zero-based index of the page to be converted
* @param dpi the DPI (dots per inch) to render at
* @return the rendered page image
* @throws IOException if the PDF cannot be read
*/
public BufferedImage renderImageWithDPI(int pageIndex, float dpi) throws IOException
{
return renderImage(pageIndex, dpi / 72f, ImageType.RGB);
}
/**
* Returns the given page as an RGB image at the given DPI.
* @param pageIndex the zero-based index of the page to be converted
* @param dpi the DPI (dots per inch) to render at
* @param imageType the type of image to return
* @return the rendered page image
* @throws IOException if the PDF cannot be read
*/
public BufferedImage renderImageWithDPI(int pageIndex, float dpi, ImageType imageType)
throws IOException
{
return renderImage(pageIndex, dpi / 72f, imageType);
}
/**
* Returns the given page as an RGB or ARGB image at the given scale.
* @param pageIndex the zero-based index of the page to be converted
* @param scale the scaling factor, where 1 = 72 DPI
* @param imageType the type of image to return
* @return the rendered page image
* @throws IOException if the PDF cannot be read
*/
public BufferedImage renderImage(int pageIndex, float scale, ImageType imageType)
throws IOException
{
return renderImage(pageIndex, scale, imageType,
defaultDestination == null ? RenderDestination.EXPORT : defaultDestination);
}
/**
* Returns the given page as an RGB or ARGB image at the given scale.
* @param pageIndex the zero-based index of the page to be converted
* @param scale the scaling factor, where 1 = 72 DPI
* @param imageType the type of image to return
* @param destination controlling visibility of optional content groups
* @return the rendered page image
* @throws IOException if the PDF cannot be read
*/
public BufferedImage renderImage(int pageIndex, float scale, ImageType imageType, RenderDestination destination)
throws IOException
{
PDPage page = pageTree.get(pageIndex);
PDRectangle cropBox = page.getCropBox();
float widthPt = cropBox.getWidth();
float heightPt = cropBox.getHeight();
// PDFBOX-4306 avoid single blank pixel line on the right or on the bottom
int widthPx = (int) Math.max(Math.floor(widthPt * scale), 1);
int heightPx = (int) Math.max(Math.floor(heightPt * scale), 1);
// PDFBOX-4518 the maximum size (w*h) of a buffered image is limited to Integer.MAX_VALUE
if ((long) widthPx * (long) heightPx > Integer.MAX_VALUE)
{
throw new IOException("Maximum size of image exceeded (w * h * scale ^ 2) = "//
+ widthPt + " * " + heightPt + " * " + scale + " ^ 2 > " + Integer.MAX_VALUE);
}
int rotationAngle = page.getRotation();
int bimType;
if (imageType != ImageType.ARGB && hasBlendMode(page))
{
// PDFBOX-4095: if the PDF has blending on the top level, draw on transparent background
// Inspired from PDF.js: if a PDF page uses any blend modes other than Normal,
// PDF.js renders everything on a fully transparent RGBA canvas.
// Finally when the page has been rendered, PDF.js draws the RGBA canvas on a white canvas.
bimType = BufferedImage.TYPE_INT_ARGB;
}
else
{
bimType = imageType.toBufferedImageType();
}
// swap width and height
BufferedImage image;
if (rotationAngle == 90 || rotationAngle == 270)
{
image = new BufferedImage(heightPx, widthPx, bimType);
}
else
{
image = new BufferedImage(widthPx, heightPx, bimType);
}
pageImage = image;
// use a transparent background if the image type supports alpha
Graphics2D g = image.createGraphics();
if (image.getType() == BufferedImage.TYPE_INT_ARGB)
{
g.setBackground(new Color(0, 0, 0, 0));
}
else
{
g.setBackground(Color.WHITE);
}
g.clearRect(0, 0, image.getWidth(), image.getHeight());
transform(g, page.getRotation(), cropBox, scale, scale);
// the end-user may provide a custom PageDrawer
RenderingHints actualRenderingHints =
renderingHints == null ? createDefaultRenderingHints(g) : renderingHints;
PageDrawerParameters parameters =
new PageDrawerParameters(this, page, subsamplingAllowed, destination,
actualRenderingHints, imageDownscalingOptimizationThreshold);
PageDrawer drawer = createPageDrawer(parameters);
drawer.drawPage(g, cropBox);
g.dispose();
if (image.getType() != imageType.toBufferedImageType())
{
// PDFBOX-4095: draw temporary transparent image on white background
BufferedImage newImage =
new BufferedImage(image.getWidth(), image.getHeight(), imageType.toBufferedImageType());
Graphics2D dstGraphics = newImage.createGraphics();
dstGraphics.setBackground(Color.WHITE);
dstGraphics.clearRect(0, 0, image.getWidth(), image.getHeight());
dstGraphics.drawImage(image, 0, 0, null);
dstGraphics.dispose();
image = newImage;
}
return image;
}
/**
* Renders a given page to an AWT Graphics2D instance at 72 DPI.
* <p>
* Read {@link #renderPageToGraphics(int, java.awt.Graphics2D, float, float, org.apache.pdfbox.rendering.RenderDestination) renderPageToGraphics(int, Graphics2D, float, float, RenderDestination)}
* before using this.
*
* @param pageIndex the zero-based index of the page to be converted
* @param graphics the Graphics2D on which to draw the page
* @throws IOException if the PDF cannot be read
*/
public void renderPageToGraphics(int pageIndex, Graphics2D graphics) throws IOException
{
renderPageToGraphics(pageIndex, graphics, 1);
}
/**
* Renders a given page to an AWT Graphics2D instance.
* <p>
* Read {@link #renderPageToGraphics(int, java.awt.Graphics2D, float, float, org.apache.pdfbox.rendering.RenderDestination) renderPageToGraphics(int, Graphics2D, float, float, RenderDestination)}
* before using this.
*
* @param pageIndex the zero-based index of the page to be converted
* @param graphics the Graphics2D on which to draw the page
* @param scale the scaling factor, where 1 = 72 DPI
* @throws IOException if the PDF cannot be read
*/
public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scale)
throws IOException
{
renderPageToGraphics(pageIndex, graphics, scale, scale);
}
/**
* Renders a given page to an AWT Graphics2D instance.
* <p>
* Read {@link #renderPageToGraphics(int, java.awt.Graphics2D, float, float, org.apache.pdfbox.rendering.RenderDestination) renderPageToGraphics(int, Graphics2D, float, float, RenderDestination)}
* before using this.
*
* @param pageIndex the zero-based index of the page to be converted
* @param graphics the Graphics2D on which to draw the page
* @param scaleX the scale to draw the page at for the x-axis, where 1 = 72 DPI
* @param scaleY the scale to draw the page at for the y-axis, where 1 = 72 DPI
* @throws IOException if the PDF cannot be read
*/
public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scaleX, float scaleY)
throws IOException
{
renderPageToGraphics(pageIndex, graphics, scaleX, scaleY,
defaultDestination == null ? RenderDestination.VIEW : defaultDestination);
}
/**
* Renders a given page to an AWT Graphics2D instance.
* <p>
* Known problems:
* <ul>
* <li>rendering of PDF files with transparencies is not supported on Ubuntu, see
* <a href="https://issues.apache.org/jira/browse/PDFBOX-4581">PDFBOX-4581</a> and
* <a href="https://bugs.openjdk.java.net/browse/JDK-6689349">JDK-6689349</a>. Rendering will
* not abort, but the pages will be rendered incorrectly.</li>
* <li>Clipping the Graphics2D will not work properly, see
* <a href="https://issues.apache.org/jira/browse/PDFBOX-4583">PDFBOX-4583</a>.</li>
* </ul>
* If you encounter these problems, then you should render into an image by using the
* {@link #renderImage(int) renderImage} methods.
*
* @param pageIndex the zero-based index of the page to be converted
* @param graphics the Graphics2D on which to draw the page
* @param scaleX the scale to draw the page at for the x-axis, where 1 = 72 DPI
* @param scaleY the scale to draw the page at for the y-axis, where 1 = 72 DPI
* @param destination controlling visibility of optional content groups
* @throws IOException if the PDF cannot be read
*/
public void renderPageToGraphics(int pageIndex, Graphics2D graphics, float scaleX, float scaleY, RenderDestination destination)
throws IOException
{
PDPage page = pageTree.get(pageIndex);
// TODO need width/height calculations? should these be in PageDrawer?
PDRectangle cropBox = page.getCropBox();
transform(graphics, page.getRotation(), cropBox, scaleX, scaleY);
graphics.clearRect(0, 0, (int) cropBox.getWidth(), (int) cropBox.getHeight());
// the end-user may provide a custom PageDrawer
RenderingHints actualRenderingHints =
renderingHints == null ? createDefaultRenderingHints(graphics) : renderingHints;
PageDrawerParameters parameters =
new PageDrawerParameters(this, page, subsamplingAllowed, destination,
actualRenderingHints, imageDownscalingOptimizationThreshold);
PageDrawer drawer = createPageDrawer(parameters);
drawer.drawPage(graphics, cropBox);
}
/**
* Indicates whether an optional content group is enabled.
* @param group the group
* @return true if the group is enabled
*/
public boolean isGroupEnabled(PDOptionalContentGroup group)
{
PDOptionalContentProperties ocProperties = document.getDocumentCatalog().getOCProperties();
return ocProperties == null || ocProperties.isGroupEnabled(group);
}
// scale rotate translate
private void transform(Graphics2D graphics, int rotationAngle, PDRectangle cropBox, float scaleX, float scaleY)
{
graphics.scale(scaleX, scaleY);
// TODO should we be passing the scale to PageDrawer rather than messing with Graphics?
if (rotationAngle != 0)
{
float translateX = 0;
float translateY = 0;
switch (rotationAngle)
{
case 90:
translateX = cropBox.getHeight();
break;
case 270:
translateY = cropBox.getWidth();
break;
case 180:
translateX = cropBox.getWidth();
translateY = cropBox.getHeight();
break;
default:
break;
}
graphics.translate(translateX, translateY);
graphics.rotate(Math.toRadians(rotationAngle));
}
}
private boolean isBitonal(Graphics2D graphics)
{
GraphicsConfiguration deviceConfiguration = graphics.getDeviceConfiguration();
if (deviceConfiguration == null)
{
return false;
}
GraphicsDevice device = deviceConfiguration.getDevice();
if (device == null)
{
return false;
}
DisplayMode displayMode = device.getDisplayMode();
if (displayMode == null)
{
return false;
}
return displayMode.getBitDepth() == 1;
}
private RenderingHints createDefaultRenderingHints(Graphics2D graphics)
{
boolean isBitonal = isBitonal(graphics);
RenderingHints r = new RenderingHints(null);
r.put(RenderingHints.KEY_INTERPOLATION, isBitonal ?
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR :
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
r.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
r.put(RenderingHints.KEY_ANTIALIASING, isBitonal ?
RenderingHints.VALUE_ANTIALIAS_OFF :
RenderingHints.VALUE_ANTIALIAS_ON);
return r;
}
/**
* Returns a new PageDrawer instance, using the given parameters. May be overridden.
*/
protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException
{
PageDrawer pageDrawer = new PageDrawer(parameters);
pageDrawer.setAnnotationFilter(annotationFilter);
return pageDrawer;
}
private boolean hasBlendMode(PDPage page)
{
// check the current resources for blend modes
PDResources resources = page.getResources();
if (resources == null)
{
return false;
}
for (COSName name : resources.getExtGStateNames())
{
PDExtendedGraphicsState extGState = resources.getExtGState(name);
if (extGState != null)
{
// extGState null can happen if key exists but no value
// see PDFBOX-3950-23EGDHXSBBYQLKYOKGZUOVYVNE675PRD.pdf
BlendMode blendMode = extGState.getBlendMode();
if (blendMode != BlendMode.NORMAL)
{
return true;
}
}
}
return false;
}
/**
* Returns the image to which the current page is being rendered.
* May be null if the page is rendered to a Graphics2D object
* instead of a BufferedImage.
*/
BufferedImage getPageImage()
{
return pageImage;
}
private static void suggestKCMS()
{
String cmmProperty = System.getProperty("sun.java2d.cmm");
if (!"sun.java2d.cmm.kcms.KcmsServiceProvider".equals(cmmProperty))
{
try
{
// Make sure that class exists
Class.forName("sun.java2d.cmm.kcms.KcmsServiceProvider");
String version = System.getProperty("java.version");
if (version == null ||
isGoodVersion(version, "1.8.0_(\\d+)", 191) ||
isGoodVersion(version, "9.0.(\\d+)", 4))
{
return;
}
LOG.info("Your current java version is: " + version);
LOG.info("To get higher rendering speed on old java 1.8 or 9 versions,");
LOG.info(" update to the latest 1.8 or 9 version (>= 1.8.0_191 or >= 9.0.4),");
LOG.info(" or");
LOG.info(" use the option -Dsun.java2d.cmm=sun.java2d.cmm.kcms.KcmsServiceProvider");
LOG.info(" or call System.setProperty(\"sun.java2d.cmm\", \"sun.java2d.cmm.kcms.KcmsServiceProvider\")");
}
catch (ClassNotFoundException e)
{
// KCMS not available
}
}
}
private static boolean isGoodVersion(String version, String regex, int min)
{
Matcher matcher = Pattern.compile(regex).matcher(version);
if (matcher.matches() && matcher.groupCount() >= 1)
{
try
{
int v = Integer.parseInt(matcher.group(1));
if (v >= min)
{
// LCMS no longer bad
return true;
}
}
catch (NumberFormatException ex)
{
return true;
}
}
return false;
}
}