diff --git a/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java b/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java index 92366f0113d..53e7e6c0448 100644 --- a/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java +++ b/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java @@ -1,2307 +1,2309 @@ -/* - * 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.BasicStroke; -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.GraphicsConfiguration; -import java.awt.GraphicsDevice; -import java.awt.Image; -import java.awt.Paint; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.Shape; -import java.awt.Stroke; -import java.awt.TexturePaint; -import java.awt.Transparency; -import java.awt.color.ColorSpace; -import java.awt.geom.AffineTransform; -import static java.awt.geom.AffineTransform.TYPE_FLIP; -import static java.awt.geom.AffineTransform.TYPE_MASK_SCALE; -import static java.awt.geom.AffineTransform.TYPE_TRANSLATION; -import java.awt.geom.Area; -import java.awt.geom.GeneralPath; -import java.awt.geom.Path2D; -import java.awt.geom.PathIterator; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.awt.image.ByteLookupTable; -import java.awt.image.ColorModel; -import java.awt.image.ComponentColorModel; -import java.awt.image.DataBuffer; -import java.awt.image.DataBufferByte; -import java.awt.image.LookupOp; -import java.awt.image.LookupTable; -import java.awt.image.Raster; -import java.awt.image.WritableRaster; -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; -import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.pdmodel.PDResources; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.function.PDFunction; -import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.font.PDType3Font; -import org.apache.pdfbox.pdmodel.font.PDVectorFont; -import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern; -import org.apache.pdfbox.pdmodel.graphics.PDXObject; -import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode; -import org.apache.pdfbox.pdmodel.graphics.color.PDColor; -import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; -import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; -import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; -import org.apache.pdfbox.pdmodel.graphics.color.PDPattern; -import org.apache.pdfbox.pdmodel.graphics.color.PDSeparation; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; -import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; -import org.apache.pdfbox.pdmodel.graphics.image.PDImage; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; -import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup.RenderState; -import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentMembershipDictionary; -import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern; -import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern; -import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; -import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; -import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; -import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState; -import org.apache.pdfbox.pdmodel.graphics.state.PDSoftMask; -import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode; -import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationUnknown; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; -import org.apache.pdfbox.util.Matrix; -import org.apache.pdfbox.util.Vector; - -/** - * Paints a page in a PDF document to a Graphics context. May be subclassed to provide custom - * rendering. - * - *

- * If you want to do custom graphics processing rather than Graphics2D rendering, then you should - * subclass {@link PDFGraphicsStreamEngine} instead. Subclassing PageDrawer is only suitable for - * cases where the goal is to render onto a {@link Graphics2D} surface. In that case you'll also - * have to subclass {@link PDFRenderer} and override - * {@link PDFRenderer#createPageDrawer(PageDrawerParameters)}. See the OpaquePDFRenderer.java - * example in the source code download on how to do this. - * - * @author Ben Litchfield - */ -public class PageDrawer extends PDFGraphicsStreamEngine -{ - private static final Logger LOG = LogManager.getLogger(PageDrawer.class); - - private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); - private static final boolean IS_WINDOWS = OS_NAME.startsWith("windows"); - private static final boolean IS_LINUX = OS_NAME.startsWith("linux"); - - // parent document renderer - note: this is needed for not-yet-implemented resource caching - private final PDFRenderer renderer; - - private final boolean subsamplingAllowed; - - // the graphics device to draw to, xform is the initial transform of the device (i.e. DPI) - private Graphics2D graphics; - private AffineTransform xform; - private float xformScalingFactorX; - private float xformScalingFactorY; - - // the page box to draw (usually the crop box but may be another) - private PDRectangle pageSize; - - // whether image of a transparency group must be flipped - // needed when in a tiling pattern - private boolean flipTG = false; - - // clipping winding rule used for the clipping path - private int clipWindingRule = -1; - private GeneralPath linePath = new GeneralPath(); - - // last clipping path - private List lastClips; - - // clip when drawPage() is called, can be null, must be intersected when clipping - private Shape initialClip; - - // shapes of glyphs being drawn to be used for clipping - private List textClippings; - - // glyph caches - private final Map glyphCaches = new HashMap<>(); - - private final TilingPaintFactory tilingPaintFactory = new TilingPaintFactory(this); - - private final Deque transparencyGroupStack = new ArrayDeque<>(); - - // if greater zero the content is hidden and will not be rendered - private int nestedHiddenOCGCount; - - private final RenderDestination destination; - private final RenderingHints renderingHints; - private final float imageDownscalingOptimizationThreshold; - private LookupTable invTable = null; - private final Map blendModeMap = new HashMap<>(); - - /** - * Default annotations filter, returns all annotations - */ - private AnnotationFilter annotationFilter = annotation -> true; - - /** - * Constructor. - * - * @param parameters Parameters for page drawing. - * @throws IOException If there is an error loading properties from the file. - */ - public PageDrawer(PageDrawerParameters parameters) throws IOException - { - super(parameters.getPage()); - this.renderer = parameters.getRenderer(); - this.subsamplingAllowed = parameters.isSubsamplingAllowed(); - this.destination = parameters.getDestination(); - this.renderingHints = parameters.getRenderingHints(); - this.imageDownscalingOptimizationThreshold = - parameters.getImageDownscalingOptimizationThreshold(); - } - - /** - * Return the AnnotationFilter. - * - * @return the AnnotationFilter - */ - public AnnotationFilter getAnnotationFilter() - { - return annotationFilter; - } - - /** - * Set the AnnotationFilter. - * - *

Allows to only render annotation accepted by the filter. - * - * @param annotationFilter the AnnotationFilter - */ - public void setAnnotationFilter(AnnotationFilter annotationFilter) - { - this.annotationFilter = annotationFilter; - } - - /** - * Returns the parent renderer. - * - * @return the parent renderer - */ - public final PDFRenderer getRenderer() - { - return renderer; - } - - /** - * Returns the underlying Graphics2D. May be null if drawPage has not yet been called. - * - * @return the underlying Graphics2D - */ - protected final Graphics2D getGraphics() - { - return graphics; - } - - /** - * Returns the current line path. This is reset to empty after each fill/stroke. - * - * @return the current line path - */ - protected final GeneralPath getLinePath() - { - return linePath; - } - - /** - * Sets high-quality rendering hints on the current Graphics2D. - */ - private void setRenderingHints() - { - graphics.addRenderingHints(renderingHints); - } - - /** - * Draws the page to the requested context. - * - * @param g The graphics context to draw onto. - * @param pageSize The size of the page to draw. - * @throws IOException If there is an IO error while drawing the page. - */ - public void drawPage(Graphics2D g, PDRectangle pageSize) throws IOException - { - graphics = g; - xform = graphics.getTransform(); - Matrix m = new Matrix(xform); - xformScalingFactorX = Math.abs(m.getScalingFactorX()); - xformScalingFactorY = Math.abs(m.getScalingFactorY()); - initialClip = graphics.getClip(); - this.pageSize = pageSize; - - setRenderingHints(); - - graphics.translate(0, pageSize.getHeight()); - graphics.scale(1, -1); - - // adjust for non-(0,0) crop box - graphics.translate(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY()); - - processPage(getPage()); - - for (PDAnnotation annotation : getPage().getAnnotations(annotationFilter)) - { - showAnnotation(annotation); - } - - graphics = null; - } - - /** - * Draws the pattern stream to the requested context. - * - * @param g The graphics context to draw onto. - * @param pattern The tiling pattern to be used. - * @param colorSpace color space for this tiling. - * @param color color for this tiling. - * @param patternMatrix the pattern matrix - * @throws IOException If there is an IO error while drawing the page. - */ - void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace colorSpace, - PDColor color, Matrix patternMatrix) throws IOException - { - Graphics2D savedGraphics = graphics; - graphics = g; - - GeneralPath savedLinePath = linePath; - linePath = new GeneralPath(); - int savedClipWindingRule = clipWindingRule; - clipWindingRule = -1; - - List savedLastClips = lastClips; - lastClips = null; - Shape savedInitialClip = initialClip; - initialClip = null; - - boolean savedFlipTG = flipTG; - flipTG = true; - - setRenderingHints(); - processTilingPattern(pattern, color, colorSpace, patternMatrix); - - flipTG = savedFlipTG; - graphics = savedGraphics; - linePath = savedLinePath; - lastClips = savedLastClips; - initialClip = savedInitialClip; - clipWindingRule = savedClipWindingRule; - } - - private float clampColor(float color) - { - return color < 0 ? 0 : (color > 1 ? 1 : color); - } - - /** - * Returns an AWT paint for the given PDColor. - * - * @param color The color to get a paint for. This can be an actual color or a pattern. - * @return an AWT paint for the given PDColor - * - * @throws IOException if the AWT paint could not be created - */ - protected Paint getPaint(PDColor color) throws IOException - { - PDColorSpace colorSpace = color.getColorSpace(); - if (colorSpace == null) // PDFBOX-5782 - { - LOG.error("colorSpace is null, will be rendered as transparency"); - return new Color(0, 0, 0, 0); - } - else if (colorSpace instanceof PDSeparation && - "None".equals(((PDSeparation) colorSpace).getColorantName())) - { - // PDFBOX-4900: "The special colorant name None shall not produce any visible output" - //TODO better solution needs to be found for all occurences where toRGB is called - return new Color(0, 0, 0, 0); - } - else if (!(colorSpace instanceof PDPattern)) - { - float[] rgb = colorSpace.toRGB(color.getComponents()); - return new Color(clampColor(rgb[0]), clampColor(rgb[1]), clampColor(rgb[2])); - } - else - { - PDPattern patternSpace = (PDPattern)colorSpace; - PDAbstractPattern pattern = patternSpace.getPattern(color); - if (pattern instanceof PDTilingPattern) - { - PDTilingPattern tilingPattern = (PDTilingPattern) pattern; - - if (tilingPattern.getPaintType() == PDTilingPattern.PAINT_COLORED) - { - // colored tiling pattern - return tilingPaintFactory.create(tilingPattern, null, null, xform); - } - else - { - // uncolored tiling pattern - return tilingPaintFactory.create(tilingPattern, - patternSpace.getUnderlyingColorSpace(), color, xform); - } - } - else - { - PDShadingPattern shadingPattern = (PDShadingPattern)pattern; - PDShading shading = shadingPattern.getShading(); - if (shading == null) - { - LOG.error("shadingPattern is null, will be filled with transparency"); - return new Color(0,0,0,0); - } - return shading.toPaint(Matrix.concatenate(getInitialMatrix(), - shadingPattern.getMatrix())); - } - } - } - - /** - * Sets the clipping path using caching for performance. We track lastClip manually because - * {@link Graphics2D#getClip()} returns a new object instead of the same one passed to - * {@link Graphics2D#setClip(java.awt.Shape) setClip()}. You may need to call this if you override - * {@link #showGlyph(Matrix, PDFont, int, Vector) showGlyph()}. See - * PDFBOX-5093 for more. - */ - protected final void setClip() - { - List clippingPaths = getGraphicsState().getCurrentClippingPaths(); - if (clippingPaths != lastClips) - { - transferClip(graphics); - if (initialClip != null) - { - // apply the remembered initial clip, but transform it first - //TODO see PDFBOX-4583 - } - lastClips = clippingPaths; - } - } - - /** - * Transfer clip to the destination device. Override this if you want to avoid to do slow - * intersecting operations but want the destination device to do this (e.g. SVG). You can get - * the individual clippings via {@link PDGraphicsState#getCurrentClippingPaths()}. See - * PDFBOX-5258 for sample code. - * - * @param graphics graphics device - */ - protected void transferClip(Graphics2D graphics) - { - Area clippingPath = getGraphicsState().getCurrentClippingPath(); - if (clippingPath.getPathIterator(null).isDone()) - { - // PDFBOX-4821: avoid bug with java printing that empty clipping path is ignored by - // replacing with empty rectangle, works because this is not an empty path - graphics.setClip(new Rectangle()); - } - else - { - graphics.setClip(clippingPath); - } - } - - @Override - public void beginText() throws IOException - { - setClip(); - beginTextClip(); - } - - @Override - public void endText() throws IOException - { - endTextClip(); - } - - /** - * Begin buffering the text clipping path, if any. - */ - private void beginTextClip() - { - // buffer the text clippings because they represents a single clipping area - textClippings = new ArrayList<>(); - } - - /** - * End buffering the text clipping path, if any. - */ - private void endTextClip() - { - PDGraphicsState state = getGraphicsState(); - RenderingMode renderingMode = state.getTextState().getRenderingMode(); - - // apply the buffered clip as one area - if (renderingMode.isClip() && !textClippings.isEmpty()) - { - // PDFBOX-4150: this is much faster than using textClippingArea.add(new Area(glyph)) - // https://stackoverflow.com/questions/21519007/fast-union-of-shapes-in-java - GeneralPath path = new GeneralPath(Path2D.WIND_NON_ZERO, textClippings.size()); - textClippings.forEach(shape -> path.append(shape, false)); - state.intersectClippingPath(path); - textClippings = new ArrayList<>(); - - // PDFBOX-3681: lastClip needs to be reset, because after intersection it is still the same - // object, thus setClip() would believe that it is cached. - lastClips = null; - } - } - - @Override - protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, - Vector displacement) throws IOException - { - AffineTransform at = textRenderingMatrix.createAffineTransform(); - at.concatenate(font.getFontMatrix().createAffineTransform()); - - // create cache if it does not exist - PDVectorFont vectorFont = (PDVectorFont) font; - GlyphCache cache = glyphCaches.get(font); - if (cache == null) - { - cache = new GlyphCache(vectorFont); - glyphCaches.put(font, cache); - } - - GeneralPath path = cache.getPathForCharacterCode(code); - drawGlyph(path, font, code, displacement, at); - } - - /** - * Renders a glyph. - * - * @param path the GeneralPath for the glyph - * @param font the font - * @param code character code - * @param displacement the glyph's displacement (advance) - * @param at the transformation - * @throws IOException if something went wrong - */ - private void drawGlyph(GeneralPath path, PDFont font, int code, Vector displacement, AffineTransform at) throws IOException - { - PDGraphicsState state = getGraphicsState(); - RenderingMode renderingMode = state.getTextState().getRenderingMode(); - - if (path != null) - { - // Stretch non-embedded glyph if it does not match the height/width contained in the PDF. - // Vertical fonts have zero X displacement, so the following code scales to 0 if we don't skip it. - // TODO: How should vertical fonts be handled? - if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code)) - { - float fontWidth = font.getWidthFromFont(code); - if (displacement.getX() > 0 && // PDFBOX-5611: ignore zero widths - fontWidth > 0 && // ignore spaces - Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001) - { - float pdfWidth = displacement.getX() * 1000; - at.scale(pdfWidth / fontWidth, 1); - } - } - - // render glyph - Shape glyph = at.createTransformedShape(path); - - if (isContentRendered()) - { - if (renderingMode.isFill()) - { - graphics.setComposite(state.getNonStrokingJavaComposite()); - graphics.setPaint(getNonStrokingPaint()); - setClip(); - graphics.fill(glyph); - } - - if (renderingMode.isStroke()) - { - graphics.setComposite(state.getStrokingJavaComposite()); - graphics.setPaint(getStrokingPaint()); - graphics.setStroke(getStroke()); - setClip(); - graphics.draw(glyph); - } - } - - if (renderingMode.isClip()) - { - textClippings.add(glyph); - } - } - } - - @Override - protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int code, - Vector displacement) throws IOException - { - PDGraphicsState state = getGraphicsState(); - RenderingMode renderingMode = state.getTextState().getRenderingMode(); - if (RenderingMode.NEITHER != renderingMode) - { - super.showType3Glyph(textRenderingMatrix, font, code, displacement); - } - } - - @Override - public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) - { - // to ensure that the path is created in the right direction, we have to create - // it by combining single lines instead of creating a simple rectangle - linePath.moveTo((float) p0.getX(), (float) p0.getY()); - linePath.lineTo((float) p1.getX(), (float) p1.getY()); - linePath.lineTo((float) p2.getX(), (float) p2.getY()); - linePath.lineTo((float) p3.getX(), (float) p3.getY()); - - // close the subpath instead of adding the last line so that a possible set line - // cap style isn't taken into account at the "beginning" of the rectangle - linePath.closePath(); - } - - private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException - { - if (softMask == null || softMask.getGroup() == null) - { - return parentPaint; - } - PDColor backdropColor = null; - if (COSName.LUMINOSITY.equals(softMask.getSubType())) - { - COSArray backdropColorArray = softMask.getBackdropColor(); - if (backdropColorArray != null) - { - PDTransparencyGroup form = softMask.getGroup(); - PDColorSpace colorSpace = form.getGroup().getColorSpace(form.getResources()); - if (colorSpace != null && - colorSpace.getNumberOfComponents() == backdropColorArray.size()) // PDFBOX-5795 - { - backdropColor = new PDColor(backdropColorArray, colorSpace); - } - } - } - TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true, - softMask.getInitialTransformationMatrix(), backdropColor); - BufferedImage image = transparencyGroup.getImage(); - if (image == null) - { - // Adobe Reader ignores empty softmasks instead of using bc color - // sample file: PDFJS-6967_reduced_outside_softmask.pdf - return parentPaint; - } - BufferedImage gray = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - if (COSName.ALPHA.equals(softMask.getSubType())) - { - gray.setData(image.getAlphaRaster()); - } - else if (COSName.LUMINOSITY.equals(softMask.getSubType())) - { - Graphics g = gray.getGraphics(); - g.drawImage(image, 0, 0, null); - g.dispose(); - } - else - { - throw new IOException("Invalid soft mask subtype."); - } - gray = adjustImage(gray); - - Rectangle2D tpgBounds = transparencyGroup.getBounds(); - return new SoftMask(parentPaint, gray, tpgBounds, backdropColor, softMask.getTransferFunction()); - } - - // returns the image adjusted for applySoftMaskToPaint(). - private BufferedImage adjustImage(BufferedImage gray) - { - AffineTransform at = new AffineTransform(xform); - at.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); - - Rectangle originalBounds = new Rectangle(gray.getWidth(), gray.getHeight()); - Rectangle2D transformedBounds = at.createTransformedShape(originalBounds).getBounds2D(); - at.preConcatenate(AffineTransform.getTranslateInstance(-transformedBounds.getMinX(), - -transformedBounds.getMinY())); - - int width = (int) Math.ceil(transformedBounds.getWidth()); - int height = (int) Math.ceil(transformedBounds.getHeight()); - - if (width == gray.getWidth() && height == gray.getHeight() && at.isIdentity()) - { - return gray; - } - - BufferedImage transformedGray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); - Graphics2D g2 = (Graphics2D) transformedGray.getGraphics(); - g2.drawImage(gray, at, null); - g2.dispose(); - return transformedGray; - } - - // returns the stroking AWT Paint - private Paint getStrokingPaint() throws IOException - { - PDGraphicsState graphicsState = getGraphicsState(); - return applySoftMaskToPaint( - getPaint(graphicsState.getStrokingColor()), graphicsState.getSoftMask()); - } - - /** - * Returns the non-stroking AWT Paint. You may need to call this if you override - * {@link #showGlyph(Matrix, PDFont, int, Vector) showGlyph()}. See - * PDFBOX-5093 for more. - * - * @return The non-stroking AWT Paint. - * @throws IOException if the non-stroking AWT Paint could not be created - */ - protected final Paint getNonStrokingPaint() throws IOException - { - PDGraphicsState graphicsState = getGraphicsState(); - return applySoftMaskToPaint( - getPaint(graphicsState.getNonStrokingColor()), graphicsState.getSoftMask()); - } - - // create a new stroke based on the current CTM and the current stroke - private Stroke getStroke() - { - PDGraphicsState state = getGraphicsState(); - - // apply the CTM - float lineWidth = transformWidth(state.getLineWidth()); - - // minimum line width as used by Adobe Reader - if (lineWidth < 0.25) - { - lineWidth = 0.25f; - } - - PDLineDashPattern dashPattern = state.getLineDashPattern(); - // PDFBOX-5168: show an all-zero dash array line invisible like Adobe does - // must do it here because getDashArray() sets minimum width because of JVM bugs - float[] dashArray = dashPattern.getDashArray(); - if (isAllZeroDash(dashArray)) - { - return (Shape p) -> new Area(); - } - float phaseStart = dashPattern.getPhase(); - dashArray = getDashArray(dashPattern); - phaseStart = transformWidth(phaseStart); - - int lineCap = Math.min(2, Math.max(0, state.getLineCap())); // legal values 0..2 - int lineJoin = Math.min(2, Math.max(0, state.getLineJoin())); - float miterLimit = state.getMiterLimit(); - if (miterLimit < 1) - { - LOG.warn("Miter limit must be >= 1, value {} is ignored", miterLimit); - miterLimit = 10; - } - return new BasicStroke(lineWidth, lineCap, lineJoin, - miterLimit, dashArray, phaseStart); - } - - private boolean isAllZeroDash(float[] dashArray) - { - if (dashArray.length > 0) - { - for (int i = 0; i < dashArray.length; ++i) - { - if (dashArray[i] != 0) - { - return false; - } - } - return true; - } - return false; - } - - private float[] getDashArray(PDLineDashPattern dashPattern) - { - float[] dashArray = dashPattern.getDashArray(); - // avoid empty, infinite and NaN values (PDFBOX-3360) - if (dashArray.length == 0) - { - return null; - } - for (int i = 0; i < dashArray.length; ++i) - { - if (Float.isInfinite(dashArray[i]) || Float.isNaN(dashArray[i])) - { - return null; - } - } - for (int i = 0; i < dashArray.length; ++i) - { - // apply the CTM - float w = transformWidth(dashArray[i]); - // minimum line dash width avoids JVM crash, - // see PDFBOX-2373, PDFBOX-2929, PDFBOX-3204, PDFBOX-3813 - // also avoid 0 in array like "[ 0 1000 ] 0 d", see PDFBOX-3724 - if (xformScalingFactorX < 0.5f) - { - // PDFBOX-4492 - dashArray[i] = Math.max(w, 0.2f); - } - else - { - dashArray[i] = Math.max(w, 0.062f); - } - } - return dashArray; - } - - @Override - public void strokePath() throws IOException - { - if (isContentRendered()) - { - graphics.setComposite(getGraphicsState().getStrokingJavaComposite()); - graphics.setPaint(getStrokingPaint()); - graphics.setStroke(getStroke()); - setClip(); - graphics.draw(linePath); - } - linePath.reset(); - } - - @Override - public void fillPath(int windingRule) throws IOException - { - PDGraphicsState graphicsState = getGraphicsState(); - graphics.setComposite(graphicsState.getNonStrokingJavaComposite()); - setClip(); - linePath.setWindingRule(windingRule); - - // disable anti-aliasing for rectangular paths, this is a workaround to avoid small stripes - // which occur when solid fills are used to simulate piecewise gradients, see PDFBOX-2302 - // note that we ignore paths with a width/height under 1 as these are fills used as strokes, - // see PDFBOX-1658 for an example - Rectangle2D bounds = linePath.getBounds2D(); - boolean noAntiAlias = isRectangular(linePath) && bounds.getWidth() > 1 && - bounds.getHeight() > 1; - if (noAntiAlias) - { - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_OFF); - } - - Shape shape; - if (graphicsState.getNonStrokingColorSpace() instanceof PDPattern) - { - // apply clip to path to avoid oversized device bounds in shading contexts (PDFBOX-2901) - Area area = new Area(linePath); - Shape clip = graphics.getClip(); - if (clip != null) - { - area.intersect(new Area(clip)); - } - intersectShadingBBox(graphicsState.getNonStrokingColor(), area); - shape = area; - } - else - { - shape = linePath; - } - if (isContentRendered() && !shape.getPathIterator(null).isDone()) - { - // creating Paint is sometimes a costly operation, so avoid if possible - graphics.setPaint(getNonStrokingPaint()); - graphics.fill(shape); - } - - linePath.reset(); - - if (noAntiAlias) - { - // JDK 1.7 has a bug where rendering hints are reset by the above call to - // the setRenderingHint method, so we re-set all hints, see PDFBOX-2302 - setRenderingHints(); - } - } - - // checks whether this is a shading pattern and if yes, - // get the transformed BBox and intersect with current paint area - // need to do it here and not in shading getRaster() because it may have been rotated - private void intersectShadingBBox(PDColor color, Area area) throws IOException - { - if (color.getColorSpace() instanceof PDPattern) - { - PDColorSpace colorSpace = color.getColorSpace(); - PDAbstractPattern pat = ((PDPattern) colorSpace).getPattern(color); - if (pat instanceof PDShadingPattern) - { - PDShading shading = ((PDShadingPattern) pat).getShading(); - PDRectangle bbox = shading.getBBox(); - if (bbox != null) - { - Matrix m = Matrix.concatenate(getInitialMatrix(), pat.getMatrix()); - Area bboxArea = new Area(bbox.transform(m)); - area.intersect(bboxArea); - } - } - } - } - - /** - * Returns true if the given path is rectangular. - */ - private boolean isRectangular(GeneralPath path) - { - PathIterator iter = path.getPathIterator(null); - double[] coords = new double[6]; - int count = 0; - int[] xs = new int[4]; - int[] ys = new int[4]; - while (!iter.isDone()) - { - switch(iter.currentSegment(coords)) - { - case PathIterator.SEG_MOVETO: - if (count == 0) - { - xs[count] = (int)Math.floor(coords[0]); - ys[count] = (int)Math.floor(coords[1]); - } - else - { - return false; - } - count++; - break; - - case PathIterator.SEG_LINETO: - if (count < 4) - { - xs[count] = (int)Math.floor(coords[0]); - ys[count] = (int)Math.floor(coords[1]); - } - else - { - return false; - } - count++; - break; - - case PathIterator.SEG_CUBICTO: - return false; - - default: - break; - } - iter.next(); - } - - if (count == 4) - { - return xs[0] == xs[1] || xs[0] == xs[2] || - ys[0] == ys[1] || ys[0] == ys[3]; - } - return false; - } - - /** - * Fills and then strokes the path. - * - * @param windingRule The winding rule this path will use. - * @throws IOException If there is an IO error while filling the path. - */ - @Override - public void fillAndStrokePath(int windingRule) throws IOException - { - // Cloning needed because fillPath() resets linePath - GeneralPath path = (GeneralPath)linePath.clone(); - fillPath(windingRule); - linePath = path; - strokePath(); - } - - @Override - public void clip(int windingRule) - { - // the clipping path will not be updated until the succeeding painting operator is called - clipWindingRule = windingRule; - if (clipWindingRule != -1) - { - linePath.setWindingRule(clipWindingRule); - - if (!linePath.getPathIterator(null).isDone()) - { - // PDFBOX-4949 / PDF.js 12306: don't clip if "W n" only - getGraphicsState().intersectClippingPath(adjustClip(linePath)); - } - - // PDFBOX-3836: lastClip needs to be reset, because after intersection it is still the same - // object, thus setClip() would believe that it is cached. - lastClips = null; - - clipWindingRule = -1; - } - } - - @Override - public void moveTo(float x, float y) - { - linePath.moveTo(x, y); - } - - @Override - public void lineTo(float x, float y) - { - linePath.lineTo(x, y); - } - - @Override - public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) - { - linePath.curveTo(x1, y1, x2, y2, x3, y3); - } - - @Override - public Point2D getCurrentPoint() - { - return linePath.getCurrentPoint(); - } - - @Override - public void closePath() - { - linePath.closePath(); - } - - @Override - public void endPath() - { - linePath.reset(); - } - - /** - * PDFBOX-5715 / PR#73: This was added to fix a problem with missing fine lines when printing - * on MacOS. Lines vanish because CPrinterJob sets graphics scale to 1 for Printable so after - * scaling lines often have a width smaller than 1 after scaling and clipping. This change - * enlarges the clip bounds to cover at least 1 point plus 0.5 on one and another side in the - * device space to allow to draw the linePath inside the clip. The linePath can consists from - * different lines but when its bounds width or height is less than 1.0 it seems safe to use a - * rectangle as a clip instead of the real path. A more detailed explanation can be read - * here. - * - * @param linePath - * @return - */ - private GeneralPath adjustClip(GeneralPath linePath) - { - AffineTransform tx = graphics.getTransform(); - int type = tx.getType(); - - if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP)) == 0) - { - return linePath; - } - else if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP | TYPE_MASK_SCALE)) == 0) - { - double sx = Math.abs(tx.getScaleX()); - double sy = Math.abs(tx.getScaleY()); - if (sx > 1.0 && sy > 1.0) - { - return linePath; - } - - Rectangle2D bounds = linePath.getBounds(); - double w = bounds.getWidth(); - double h = bounds.getHeight(); - double sw = sx * w; - double sh = sy * h; - final double minSize = 2.0; - if (sw < minSize || sh < minSize) - { - double x = bounds.getX(); - double y = bounds.getY(); - if (sw < minSize) - { - w = minSize / sx; - x = bounds.getCenterX() - w / 2; - } - if (sh < minSize) - { - h = minSize / sy; - y = bounds.getCenterY() - h / 2; - } - return new GeneralPath(new Rectangle2D.Double(x, y, w, h)); - } - } - return linePath; - } - - @Override - public void drawImage(PDImage pdImage) throws IOException - { - if (pdImage instanceof PDImageXObject && - isHiddenOCG(((PDImageXObject) pdImage).getOptionalContent())) - { - return; - } - if (!isContentRendered()) - { - return; - } - Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); - AffineTransform at = ctm.createAffineTransform(); - - if (!pdImage.getInterpolate()) - { - // if the image is scaled down, we use smooth interpolation, eg PDFBOX-2364 - // only when scaled up do we use nearest neighbour, eg PDFBOX-2302 / mori-cvpr01.pdf - // PDFBOX-4930: we use the sizes of the ARGB image. These can be different - // than the original sizes of the base image, when the mask is bigger. - // PDFBOX-5091: also consider subsampling, the sizes are different too. - BufferedImage bim; - if (subsamplingAllowed) - { - bim = pdImage.getImage(null, getSubsampling(pdImage, at)); - } - else - { - bim = pdImage.getImage(); - } - boolean isScaledUp = - bim.getWidth() <= Math.abs(Math.round(ctm.getScalingFactorX() * xformScalingFactorX)) || - bim.getHeight() <= Math.abs(Math.round(ctm.getScalingFactorY() * xformScalingFactorY)); - if (isScaledUp) - { - graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - } - } - - graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); - setClip(); - - if (pdImage.isStencil()) - { - if (getGraphicsState().getNonStrokingColor().getColorSpace() instanceof PDPattern) - { - // The earlier code for stencils (see "else") doesn't work with patterns because the - // CTM is not taken into consideration. - // this code is based on the fact that it is easily possible to draw the mask and - // the paint at the correct place with the existing code, but not in one step. - // Thus what we do is to draw both in separate images, then combine the two and draw - // the result. - // Note that the device scale is not used. In theory, some patterns can get better - // at higher resolutions but the stencil would become more and more "blocky". - // If anybody wants to do this, have a look at the code in showTransparencyGroup(). - - // draw the paint - Paint paint = getNonStrokingPaint(); - Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1); - Rectangle2D bounds = at.createTransformedShape(unitRect).getBounds2D(); - int w = (int) Math.ceil(bounds.getWidth()); - int h = (int) Math.ceil(bounds.getHeight()); - BufferedImage renderedPaint = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = (Graphics2D) renderedPaint.getGraphics(); - g.translate(-bounds.getMinX(), -bounds.getMinY()); - g.setPaint(paint); - g.setRenderingHints(graphics.getRenderingHints()); - g.fill(bounds); - g.dispose(); - - // draw the mask - BufferedImage mask = pdImage.getImage(); - AffineTransform imageTransform = new AffineTransform(at); - imageTransform.scale(1.0 / mask.getWidth(), -1.0 / mask.getHeight()); - imageTransform.translate(0, -mask.getHeight()); - AffineTransform full = new AffineTransform(g.getTransform()); - full.concatenate(imageTransform); - Matrix m = new Matrix(full); - double scaleX = Math.abs(m.getScalingFactorX()); - double scaleY = Math.abs(m.getScalingFactorY()); - - boolean smallMask = mask.getWidth() <= 8 && mask.getHeight() <= 8; - if (mask.getWidth() == 1 && mask.getHeight() == 1) - { - // PDFBOX-5802: force usage of the lookup table if it is only 1 pixel - // (See the comment for PDFBOX-5403 that it isn't done for some - // cases based purely on the rendering result of one file!) - smallMask = false; - } - if (!smallMask) - { - // PDFBOX-5403: - // The mask is copied to RGB because this supports a smooth scaling, so we - // get a mask with 255 values instead of just 0 and 255. - // Inverting is done because when we don't do it, the getScaledInstance() call - // produces a black line in many masks. With the inversion we have a white line - // which is neutral. Because of the inversion we don't have to substract from 255 - // in the "apply the mask" segment when rasterPixel[3] is assigned. - - // The inversion is not done for very small ones, because of - // PDFBOX-2171-002-002710-p14.pdf where the "New Harmony Consolidated" and - // "Sailor Springs" patterns became almost invisible. - // (We may have to decide this differently in the future, e.g. on b/w relationship) - BufferedImage tmp = new BufferedImage(mask.getWidth(), mask.getHeight(), BufferedImage.TYPE_INT_RGB); - mask = new LookupOp(getInvLookupTable(), graphics.getRenderingHints()).filter(mask, tmp); - } - - BufferedImage renderedMask = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - g = (Graphics2D) renderedMask.getGraphics(); - g.translate(-bounds.getMinX(), -bounds.getMinY()); - g.setRenderingHints(graphics.getRenderingHints()); - - if (smallMask) - { - g.drawImage(mask, imageTransform, null); - } - else if (scaleX != 0 && scaleY != 0) - { - while (scaleX < 0.25 || Math.round(mask.getWidth() * scaleX) < 1) - { - scaleX *= 2.0; - } - while (scaleY < 0.25 || Math.round(mask.getHeight() * scaleY) < 1) - { - scaleY *= 2.0; - } - int w2 = (int) Math.round(mask.getWidth() * scaleX); - int h2 = (int) Math.round(mask.getHeight() * scaleY); - - Image scaledMask = mask.getScaledInstance(w2, h2, Image.SCALE_SMOOTH); - imageTransform.scale(1f / Math.abs(scaleX), 1f / Math.abs(scaleY)); - g.drawImage(scaledMask, imageTransform, null); - } - g.dispose(); - - // apply the mask - int[] alphaPixel = null; - int[] rasterPixel = null; - WritableRaster raster = renderedPaint.getRaster(); - WritableRaster alpha = renderedMask.getRaster(); - for (int y = 0; y < h; y++) - { - for (int x = 0; x < w; x++) - { - alphaPixel = alpha.getPixel(x, y, alphaPixel); - rasterPixel = raster.getPixel(x, y, rasterPixel); - rasterPixel[3] = alphaPixel[0]; - raster.setPixel(x, y, rasterPixel); - } - } - - // draw the image - graphics.drawImage(renderedPaint, - AffineTransform.getTranslateInstance(bounds.getMinX(), bounds.getMinY()), - null); - } - else - { - // fill the image with stenciled paint - BufferedImage image = pdImage.getStencilImage(getNonStrokingPaint()); - - // draw the image - drawBufferedImage(pdImage, image, at); - } - } - else - { - if (subsamplingAllowed) - { - int subsampling = getSubsampling(pdImage, at); - // draw the subsampled image - drawBufferedImage(pdImage, pdImage.getImage(null, subsampling), at); - } - else - { - // subsampling not allowed, draw the image - drawBufferedImage(pdImage, pdImage.getImage(), at); - } - } - - if (!pdImage.getInterpolate()) - { - // JDK 1.7 has a bug where rendering hints are reset by the above call to - // the setRenderingHint method, so we re-set all hints, see PDFBOX-2302 - setRenderingHints(); - } - } - - /** - * Calculates the subsampling frequency for a given PDImage based on the current transformation - * and its calculated transform. Extend this method if you want to use your own strategy. - * - * @param pdImage PDImage to be drawn - * @param at Transform that will be applied to the image when drawing - * @return The rounded-down ratio of image pixels to drawn pixels. Returned value will always be - * >=1. - */ - protected int getSubsampling(PDImage pdImage, AffineTransform at) - { - // calculate subsampling according to the resulting image size - double scale = Math.abs(at.getDeterminant() * xform.getDeterminant()); - - int subsampling = (int) Math.floor(Math.sqrt(pdImage.getWidth() * pdImage.getHeight() / scale)); - if (subsampling > 8) - { - subsampling = 8; - } - if (subsampling < 1) - { - subsampling = 1; - } - if (subsampling > pdImage.getWidth() || subsampling > pdImage.getHeight()) - { - // For very small images it is possible that the subsampling would imply 0 size. - // To avoid problems, the subsampling is set to no less than the smallest dimension. - subsampling = Math.min(pdImage.getWidth(), pdImage.getHeight()); - } - return subsampling; - } - - private void drawBufferedImage(PDImage pdImage, BufferedImage image, AffineTransform at) throws IOException - { - AffineTransform originalTransform = graphics.getTransform(); - AffineTransform imageTransform = new AffineTransform(at); - int width = image.getWidth(); - int height = image.getHeight(); - imageTransform.scale(1.0 / width, -1.0 / height); - imageTransform.translate(0, -height); - - PDSoftMask softMask = getGraphicsState().getSoftMask(); - - // PDFBOX-5307 / PDF.js PR#19269 - // From section 11.6.4.3 Mask Shape and Opacity in the PDF specification: - // "Either form of mask in the image dictionary shall override the current soft mask - // in the graphics state" - boolean hasImageMask = pdImage.getCOSObject().containsKey(COSName.MASK) || - pdImage.getCOSObject().containsKey(COSName.SMASK); - - if (softMask != null && !hasImageMask) - { - Rectangle2D rectangle = new Rectangle2D.Float(0, 0, width, height); - Paint awtPaint = new TexturePaint(image, rectangle); - awtPaint = applySoftMaskToPaint(awtPaint, softMask); - graphics.setPaint(awtPaint); - graphics.transform(imageTransform); - graphics.fill(rectangle); - graphics.setTransform(originalTransform); - } - else - { - COSBase transfer = getGraphicsState().getTransfer(); - if (transfer instanceof COSArray || transfer instanceof COSDictionary) - { - image = applyTransferFunction(image, transfer); - } - - // PDFBOX-4516, PDFBOX-4527, PDFBOX-4815, PDFBOX-4886, PDFBOX-4863: - // graphics.drawImage() has terrible quality when scaling down, even when - // RenderingHints.VALUE_INTERPOLATION_BICUBIC, VALUE_ALPHA_INTERPOLATION_QUALITY, - // VALUE_COLOR_RENDER_QUALITY and VALUE_RENDER_QUALITY are all set. - // A workaround is to get a pre-scaled image with Image.getScaledInstance() - // and then draw that one. To reduce differences in testing - // (partly because the method needs integer parameters), only smaller scalings - // will trigger the workaround. Because of the slowness we only do it if the user - // expects quality rendering and interpolation. - Matrix imageTransformMatrix = new Matrix(imageTransform); - Matrix graphicsTransformMatrix = new Matrix(originalTransform); - float scaleX = Math.abs(imageTransformMatrix.getScalingFactorX() * graphicsTransformMatrix.getScalingFactorX()); - float scaleY = Math.abs(imageTransformMatrix.getScalingFactorY() * graphicsTransformMatrix.getScalingFactorY()); - - if ((scaleX < imageDownscalingOptimizationThreshold || scaleY < imageDownscalingOptimizationThreshold) && - RenderingHints.VALUE_RENDER_QUALITY.equals(graphics.getRenderingHint(RenderingHints.KEY_RENDERING)) && - RenderingHints.VALUE_INTERPOLATION_BICUBIC.equals(graphics.getRenderingHint(RenderingHints.KEY_INTERPOLATION))) - { - int w = Math.round(image.getWidth() * scaleX); - int h = Math.round(image.getHeight() * scaleY); - if (w < 1 || h < 1) - { - graphics.drawImage(image, imageTransform, null); - return; - } - Image imageToDraw = image.getScaledInstance(w, h, Image.SCALE_SMOOTH); - // remove the scale (extracted from w and h, to have it from the rounded values - // hoping to reverse the rounding: without this, we get an horizontal line - // when rendering PDFJS-8860-Pattern-Size1.pdf at 100% ) - imageTransform.scale(1f / w * image.getWidth(), 1f / h * image.getHeight()); - imageTransform.preConcatenate(originalTransform); - graphics.setTransform(new AffineTransform()); - graphics.drawImage(imageToDraw, imageTransform, null); - graphics.setTransform(originalTransform); - } - else - { - GraphicsConfiguration graphicsConfiguration = graphics.getDeviceConfiguration(); - int deviceType = GraphicsDevice.TYPE_RASTER_SCREEN; - if (graphicsConfiguration != null) - { - GraphicsDevice graphicsDevice = graphicsConfiguration.getDevice(); - if (graphicsDevice != null) - { - deviceType = graphicsDevice.getType(); - } - } - if (deviceType == GraphicsDevice.TYPE_PRINTER && - image.getType() != BufferedImage.TYPE_4BYTE_ABGR && - (IS_WINDOWS || IS_LINUX)) - { - // PDFBOX-5601, PDFBOX-4010, JDK-8308099, JDK-8191800: - // workaround to avoid terrible / missing output on printer unless TYPE_4BYTE_ABGR - BufferedImage bim = new BufferedImage( - image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); - Graphics g = bim.getGraphics(); - g.drawImage(image, 0, 0, null); - g.dispose(); - image = bim; - } - graphics.drawImage(image, imageTransform, null); - } - } - } - - private BufferedImage applyTransferFunction(BufferedImage image, COSBase transfer) throws IOException - { - BufferedImage bim; - if (image.getColorModel().hasAlpha()) - { - bim = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); - } - else - { - bim = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); - } - - // prepare transfer functions (either one per color or one for all) - // and maps (actually arrays[256] to be faster) to avoid calculating values several times - Integer[] rMap; - Integer[] gMap; - Integer[] bMap; - PDFunction rf; - PDFunction gf; - PDFunction bf; - if (transfer instanceof COSArray) - { - COSArray ar = (COSArray) transfer; - rf = PDFunction.create(ar.getObject(0)); - gf = PDFunction.create(ar.getObject(1)); - bf = PDFunction.create(ar.getObject(2)); - rMap = new Integer[256]; - gMap = new Integer[256]; - bMap = new Integer[256]; - } - else - { - rf = PDFunction.create(transfer); - gf = rf; - bf = rf; - rMap = new Integer[256]; - gMap = rMap; - bMap = rMap; - } - - // apply the transfer function to each color, but keep alpha - float[] input = new float[1]; - for (int x = 0; x < image.getWidth(); ++x) - { - for (int y = 0; y < image.getHeight(); ++y) - { - int rgb = image.getRGB(x, y); - int ri = (rgb >> 16) & 0xFF; - int gi = (rgb >> 8) & 0xFF; - int bi = rgb & 0xFF; - int ro; - int go; - int bo; - if (rMap[ri] != null) - { - ro = rMap[ri]; - } - else - { - input[0] = (ri & 0xFF) / 255f; - ro = (int) (rf.eval(input)[0] * 255); - rMap[ri] = ro; - } - if (gMap[gi] != null) - { - go = gMap[gi]; - } - else - { - input[0] = (gi & 0xFF) / 255f; - go = (int) (gf.eval(input)[0] * 255); - gMap[gi] = go; - } - if (bMap[bi] != null) - { - bo = bMap[bi]; - } - else - { - input[0] = (bi & 0xFF) / 255f; - bo = (int) (bf.eval(input)[0] * 255); - bMap[bi] = bo; - } - bim.setRGB(x, y, (rgb & 0xFF000000) | (ro << 16) | (go << 8) | bo); - } - } - return bim; - } - - @Override - public void shadingFill(COSName shadingName) throws IOException - { - if (!isContentRendered()) - { - return; - } - PDShading shading = getResources().getShading(shadingName); - if (shading == null) - { - LOG.error("shading {} does not exist in resources dictionary", shadingName); - return; - } - Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); - - graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); - Shape savedClip = graphics.getClip(); - graphics.setClip(null); - lastClips = null; - - // get the transformed BBox and intersect with current clipping path - // need to do it here and not in shading getRaster() because it may have been rotated - PDRectangle bbox = shading.getBBox(); - Area area; - if (bbox != null) - { - area = new Area(bbox.transform(ctm)); - area.intersect(getGraphicsState().getCurrentClippingPath()); - } - else - { - Rectangle2D bounds = shading.getBounds(new AffineTransform(), ctm); - if (bounds != null) - { - bounds.add(new Point2D.Double(Math.floor(bounds.getMinX() - 1), - Math.floor(bounds.getMinY() - 1))); - bounds.add(new Point2D.Double(Math.ceil(bounds.getMaxX() + 1), - Math.ceil(bounds.getMaxY() + 1))); - area = new Area(bounds); - area.intersect(getGraphicsState().getCurrentClippingPath()); - } - else - { - area = getGraphicsState().getCurrentClippingPath(); - } - } - if (!area.isEmpty()) - { - // creating Paint is sometimes a costly operation, so avoid if possible - Paint paint = shading.toPaint(ctm); - paint = applySoftMaskToPaint(paint, getGraphicsState().getSoftMask()); - graphics.setPaint(paint); - graphics.fill(area); - } - graphics.setClip(savedClip); - } - - @Override - public void showAnnotation(PDAnnotation annotation) throws IOException - { - lastClips = null; - - if (shouldSkipAnnotation(annotation)) - { - return; - } - - //TODO support NoZoom, example can be found in p5 of PDFBOX-2348 - PDAppearanceDictionary appearance = annotation.getAppearance(); - if (appearance == null || appearance.getNormalAppearance() == null) - { - annotation.constructAppearances(renderer.document); - } - if (annotation.isNoRotate() && getCurrentPage().getRotation() != 0) - { - appearance = annotation.getAppearance(); - if (appearance != null) - { - PDAppearanceEntry appearanceEntry = appearance.getNormalAppearance(); - if (appearanceEntry != null && appearanceEntry.isStream() && - hasTransparency(appearanceEntry.getAppearanceStream())) - { - // PDFBOX-4744: avoid appearances with transparency groups until we have fixed - // the rendering. A real solution should probably be - // in PDFStreamEngine.processAnnotation(). - annotation.constructAppearances(); - } - } - PDRectangle rect = annotation.getRectangle(); - AffineTransform savedTransform = graphics.getTransform(); - // "The upper-left corner of the annotation remains at the same point in - // default user space; the annotation pivots around that point." - graphics.rotate(Math.toRadians(getCurrentPage().getRotation()), - rect.getLowerLeftX(), rect.getUpperRightY()); - super.showAnnotation(annotation); - graphics.setTransform(savedTransform); - annotation.setAppearance(appearance); // restore - } - else - { - super.showAnnotation(annotation); - } - } - - private boolean shouldSkipAnnotation(PDAnnotation annotation) - { - if (destination == RenderDestination.PRINT && !annotation.isPrinted()) - { - return true; - } - if ((destination == RenderDestination.VIEW || destination == RenderDestination.EXPORT) && - annotation.isNoView()) - { - return true; - } - if (annotation.isHidden()) - { - return true; - } - if (annotation.isInvisible() && annotation instanceof PDAnnotationUnknown) - { - // "If set, do not display the annotation if it does not belong to one - // of the standard annotation types and no annotation handler is available." - return true; - } - return isHiddenOCG(annotation.getOptionalContent()); - } - - private boolean hasTransparency(PDFormXObject form) throws IOException - { - if (form == null) - { - return false; - } - PDResources resources = form.getResources(); - if (resources == null) - { - return false; - } - for (COSName name : resources.getXObjectNames()) - { - PDXObject xObject = resources.getXObject(name); - if (xObject instanceof PDTransparencyGroup) - { - return true; - } - if (xObject instanceof PDFormXObject && hasTransparency((PDFormXObject) xObject)) - { - return true; - } - } - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public void showForm(PDFormXObject form) throws IOException - { - if (isHiddenOCG(form.getOptionalContent())) - { - return; - } - if (isContentRendered()) - { - GeneralPath savedLinePath = linePath; - linePath = new GeneralPath(); - super.showForm(form); - linePath = savedLinePath; - } - } - - @Override - public void showTransparencyGroup(PDTransparencyGroup form) throws IOException - { - showTransparencyGroupOnGraphics(form, graphics); - } - - /** - * For advanced users, to extract the transparency group into a separate graphics device. - * - * @param form the transparency group to be extracted - * @param graphics the target graphics device - * @throws IOException if the transparency group could not be extracted - */ - protected void showTransparencyGroupOnGraphics(PDTransparencyGroup form, Graphics2D graphics) - throws IOException - { - if (isHiddenOCG(form.getOptionalContent())) - { - return; - } - if (!isContentRendered()) - { - return; - } - TransparencyGroup group - = new TransparencyGroup(form, false, getGraphicsState().getCurrentTransformationMatrix(), null); - BufferedImage image = group.getImage(); - if (image == null) - { - // image is empty, don't bother - return; - } - - graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); - setClip(); - - // both the DPI xform and the CTM were already applied to the group, so all we do - // here is draw it directly onto the Graphics2D device at the appropriate position - AffineTransform savedTransform = graphics.getTransform(); - AffineTransform transform = new AffineTransform(xform); - transform.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); - graphics.setTransform(transform); - - // adjust bbox (x,y) position at the initial scale + cropbox - PDRectangle bbox = group.getBBox(); - float x = bbox.getLowerLeftX() - pageSize.getLowerLeftX(); - float y = pageSize.getUpperRightY() - bbox.getUpperRightY(); - - if (flipTG) - { - graphics.translate(0, image.getHeight()); - graphics.scale(1, -1); - } - else - { - graphics.translate(x * xformScalingFactorX, y * xformScalingFactorY); - } - - PDSoftMask softMask = getGraphicsState().getSoftMask(); - if (softMask != null) - { - Paint awtPaint = new TexturePaint(image, - new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight())); - awtPaint = applySoftMaskToPaint(awtPaint, softMask); - graphics.setPaint(awtPaint); - graphics.fill( - new Rectangle2D.Float(0, 0, bbox.getWidth() * xformScalingFactorX, bbox.getHeight() * xformScalingFactorY)); - } - else - { - try - { - graphics.drawImage(image, null, null); - } - catch (InternalError ie) - { - LOG.error("Exception drawing image, see JDK-6689349, " + - "try rendering into a BufferedImage instead", ie); - } - } - - graphics.setTransform(savedTransform); - } - - /** - * Transparency group. - **/ - private final class TransparencyGroup - { - private final BufferedImage image; - private final PDRectangle bbox; - - private final int minX; - private final int minY; - private final int maxX; - private final int maxY; - private final int width; - private final int height; - - /** - * Creates a buffered image for a transparency group result. - * - * @param form the transparency group of the form or soft mask. - * @param isSoftMask true if this is a soft mask. - * @param ctm the relevant current transformation matrix. For soft masks, this is the CTM at - * the time the soft mask is set (not at the time the soft mask is used for fill/stroke!), - * for forms, this is the CTM at the time the form is invoked. - * @param backdropColor the color according to the /bc entry to be used for luminosity soft - * masks. - * @throws IOException - */ - private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix ctm, - PDColor backdropColor) throws IOException - { - Graphics2D savedGraphics = graphics; - List savedLastClips = lastClips; - Shape savedInitialClip = initialClip; - - // get the CTM x Form Matrix transform - Matrix transform = Matrix.concatenate(ctm, form.getMatrix()); - - // transform the bbox - PDRectangle formBBox = form.getBBox(); - if (formBBox == null) - { - // PDFBOX-5471 - // check done here and not in caller to avoid getBBox() creating rectangle twice - LOG.warn("transparency group ignored because BBox is null"); - formBBox = new PDRectangle(); - } - GeneralPath transformedBox = formBBox.transform(transform); - - // clip the bbox to prevent giant bboxes from consuming all memory - Area transformed = new Area(transformedBox); - transformed.intersect(getGraphicsState().getCurrentClippingPath()); - Rectangle2D clipRect = transformed.getBounds2D(); - if (clipRect.isEmpty()) - { - image = null; - bbox = null; - minX = 0; - minY = 0; - maxX = 0; - maxY = 0; - width = 0; - height = 0; - return; - } - this.bbox = new PDRectangle((float)clipRect.getX(), (float)clipRect.getY(), - (float)clipRect.getWidth(), (float)clipRect.getHeight()); - - // apply the underlying Graphics2D device's DPI transform - AffineTransform xformOriginal = xform; - xform = AffineTransform.getScaleInstance(xformScalingFactorX, xformScalingFactorY); - Rectangle2D bounds = xform.createTransformedShape(clipRect).getBounds2D(); - - minX = (int) Math.floor(bounds.getMinX()); - minY = (int) Math.floor(bounds.getMinY()); - maxX = (int) Math.floor(bounds.getMaxX()) + 1; - maxY = (int) Math.floor(bounds.getMaxY()) + 1; - - width = maxX - minX; - height = maxY - minY; - - // FIXME - color space - if (isGray(form.getGroup().getColorSpace(form.getResources()))) - { - image = create2ByteGrayAlphaImage(width, height); - } - else - { - image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - } - - boolean needsBackdrop = !isSoftMask && !form.getGroup().isIsolated() && - hasBlendMode(form, new HashSet<>()); - BufferedImage backdropImage = null; - // Position of this group in parent group's coordinates - int backdropX = 0; - int backdropY = 0; - if (needsBackdrop) - { - if (transparencyGroupStack.isEmpty()) - { - // Use the current page as the parent group. - backdropImage = renderer.getPageImage(); - if (backdropImage == null) - { - needsBackdrop = false; - } - else - { - backdropX = minX; - backdropY = backdropImage.getHeight() - maxY; - } - } - else - { - TransparencyGroup parentGroup = transparencyGroupStack.peek(); - backdropImage = parentGroup.image; - backdropX = minX - parentGroup.minX; - backdropY = parentGroup.maxY - maxY; - } - } - - Graphics2D g = image.createGraphics(); - if (needsBackdrop) - { - // backdropImage must be included in group image but not in group alpha. - g.drawImage(backdropImage, 0, 0, width, height, - backdropX, backdropY, backdropX + width, backdropY + height, null); - g = new GroupGraphics(image, g); - } - if (isSoftMask && backdropColor != null) - { - // "If the subtype is Luminosity, the transparency group XObject G shall be - // composited with a fully opaque backdrop whose colour is everywhere defined - // by the soft-mask dictionary's BC entry." - g.setBackground(new Color(backdropColor.toRGB())); - g.clearRect(0, 0, width, height); - } - - // flip y-axis - g.translate(0, image.getHeight()); - g.scale(1, -1); - - boolean savedFlipTG = flipTG; - flipTG = false; - - // apply device transform (DPI) - // the initial translation is ignored, because we're not writing into the initial graphics device - g.transform(xform); - - PDRectangle pageSizeOriginal = pageSize; - pageSize = new PDRectangle(minX / xformScalingFactorX, - minY / xformScalingFactorY, - (float) (bounds.getWidth() / xformScalingFactorX), - (float) (bounds.getHeight() / xformScalingFactorY)); - int clipWindingRuleOriginal = clipWindingRule; - clipWindingRule = -1; - GeneralPath linePathOriginal = linePath; - linePath = new GeneralPath(); - - // adjust the origin - g.translate(-clipRect.getX(), -clipRect.getY()); - - graphics = g; - setRenderingHints(); - try - { - if (isSoftMask) - { - processSoftMask(form); - } - else - { - transparencyGroupStack.push(this); - processTransparencyGroup(form); - if (!transparencyGroupStack.isEmpty()) - { - transparencyGroupStack.pop(); - } - } - - if (needsBackdrop) - { - ((GroupGraphics) graphics).removeBackdrop(backdropImage, backdropX, backdropY); - } - } - finally - { - flipTG = savedFlipTG; - lastClips = savedLastClips; - graphics.dispose(); - graphics = savedGraphics; - initialClip = savedInitialClip; - clipWindingRule = clipWindingRuleOriginal; - linePath = linePathOriginal; - pageSize = pageSizeOriginal; - xform = xformOriginal; - } - } - - // http://stackoverflow.com/a/21181943/535646 - private BufferedImage create2ByteGrayAlphaImage(int width, int height) - { - // gray + alpha - int[] bandOffsets = {1, 0}; - int bands = bandOffsets.length; - - // Color Model used for raw GRAY + ALPHA - final ColorModel CM_GRAY_ALPHA - = new ComponentColorModel( - ColorSpace.getInstance(ColorSpace.CS_GRAY), - true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); - - // Init data buffer of type byte - DataBuffer buffer = new DataBufferByte(width * height * bands); - - // Wrap the data buffer in a raster - WritableRaster raster = - Raster.createInterleavedRaster(buffer, width, height, - width * bands, bands, bandOffsets, new Point(0, 0)); - - // Create a custom BufferedImage with the raster and a suitable color model - return new BufferedImage(CM_GRAY_ALPHA, raster, false, null); - } - - private boolean isGray(PDColorSpace colorSpace) - { - if (colorSpace instanceof PDDeviceGray) - { - return true; - } - if (colorSpace instanceof PDICCBased) - { - try - { - return ((PDICCBased) colorSpace).getAlternateColorSpace() instanceof PDDeviceGray; - } - catch (IOException ex) - { - LOG.debug("Couldn't get an alternate ColorSpace", ex); - return false; - } - } - return false; - } - - BufferedImage getImage() - { - return image; - } - - PDRectangle getBBox() - { - return bbox; - } - - Rectangle2D getBounds() - { - Rectangle2D r; - if (flipTG) - { - // Fixes PDFBOX-5966 and PDFBOX-5251, but not pdfium 1317, which has similar PDF code. - // https://bugs.chromium.org/p/pdfium/issues/detail?id=1317 - r = new Rectangle2D.Double(minX, minY, width, height); - } - else - { - // y-axis flip - r = new Rectangle2D.Double( - minX - pageSize.getLowerLeftX() * xformScalingFactorX, - (pageSize.getLowerLeftY() + pageSize.getHeight()) * xformScalingFactorY - minY - height, - width, - height); - } - // apply the underlying Graphics2D device's DPI transform - // this adjusts the rectangle to the rotated image to put the soft mask at the correct position - //TODO - // 1. change transparencyGroup.getBounds() to getOrigin(), because size isn't used in SoftMask, - // 2. Is it possible to create the softmask and transparency group in the correct rotation? - // (needs rendering identity testing before committing!) - AffineTransform adjustedTransform = new AffineTransform(xform); - adjustedTransform.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); - return adjustedTransform.createTransformedShape(r).getBounds2D(); - } - } - - private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) - { - if (groupsDone.contains(group.getCOSObject())) - { - // The group is being processed. Avoid endless recursion. - return false; - } - groupsDone.add(group.getCOSObject()); - - Boolean val = blendModeMap.get(group.getCOSObject()); - if (val != null) - { - return val; - } - - PDResources resources = group.getResources(); - if (resources == null) - { - blendModeMap.put(group.getCOSObject(), false); - return false; - } - for (COSName name : resources.getExtGStateNames()) - { - PDExtendedGraphicsState extGState = resources.getExtGState(name); - if (extGState == null) - { - continue; - } - BlendMode blendMode = extGState.getBlendMode(); - if (blendMode != BlendMode.NORMAL) - { - blendModeMap.put(group.getCOSObject(), true); - return true; - } - } - - // Recursively process nested transparency groups - for (COSName name : resources.getXObjectNames()) - { - PDXObject xObject; - try - { - xObject = resources.getXObject(name); - } - catch (IOException ex) - { - continue; - } - if (xObject instanceof PDTransparencyGroup && - hasBlendMode((PDTransparencyGroup)xObject, groupsDone)) - { - blendModeMap.put(group.getCOSObject(), true); - return true; - } - } - - blendModeMap.put(group.getCOSObject(), false); - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public void beginMarkedContentSequence(COSName tag, COSDictionary properties) - { - if (nestedHiddenOCGCount > 0) - { - nestedHiddenOCGCount++; - return; - } - if (properties == null) - { - return; - } - if (isHiddenOCG(PDPropertyList.create(properties))) - { - nestedHiddenOCGCount = 1; - } - } - - /** - * {@inheritDoc} - */ - @Override - public void endMarkedContentSequence() - { - if (nestedHiddenOCGCount > 0) - { - nestedHiddenOCGCount--; - } - } - - private boolean isContentRendered() - { - return nestedHiddenOCGCount <= 0; - } - - private boolean isHiddenOCG(PDPropertyList propertyList) - { - if (propertyList instanceof PDOptionalContentGroup) - { - PDOptionalContentGroup group = (PDOptionalContentGroup) propertyList; - RenderState printState = group.getRenderState(destination); - if (printState == null) - { - if (!getRenderer().isGroupEnabled(group)) - { - return true; - } - } - else if (RenderState.OFF == printState) - { - return true; - } - } - else if (propertyList instanceof PDOptionalContentMembershipDictionary) - { - return isHiddenOCMD((PDOptionalContentMembershipDictionary) propertyList); - } - return false; - } - - private boolean isHiddenOCMD(PDOptionalContentMembershipDictionary ocmd) - { - COSArray veArray = ocmd.getCOSObject().getCOSArray(COSName.VE); - if (veArray != null && !veArray.isEmpty()) - { - return isHiddenVisibilityExpression(veArray); - } - List oCGs = ocmd.getOCGs(); - if (oCGs.isEmpty()) - { - return false; - } - List visibles = new ArrayList<>(); - oCGs.forEach(prop -> visibles.add(!isHiddenOCG(prop))); - COSName visibilityPolicy = ocmd.getVisibilityPolicy(); - - // visible if any of the entries in OCGs are OFF - if (COSName.ANY_OFF.equals(visibilityPolicy)) - { - return visibles.stream().allMatch(v -> v); - } - - // visible only if all of the entries in OCGs are ON - if (COSName.ALL_ON.equals(visibilityPolicy)) - { - return visibles.stream().anyMatch(v -> !v); - } - - // visible only if all of the entries in OCGs are OFF - if (COSName.ALL_OFF.equals(visibilityPolicy)) - { - return visibles.stream().anyMatch(v -> v); - } - - // visible if any of the entries in OCGs are ON - // AnyOn is default - return visibles.stream().noneMatch(v -> v); - } - - private boolean isHiddenVisibilityExpression(COSArray veArray) - { - if (veArray.isEmpty()) - { - return false; - } - String op = veArray.getName(0); - if (op == null) - { - return false; - } - switch (op) - { - case "And": - return isHiddenAndVisibilityExpression(veArray); - case "Or": - return isHiddenOrVisibilityExpression(veArray); - case "Not": - return isHiddenNotVisibilityExpression(veArray); - default: - return false; - } - } - - private boolean isHiddenAndVisibilityExpression(COSArray veArray) - { - // hidden if at least one isn't visible - for (int i = 1; i < veArray.size(); ++i) - { - COSBase base = veArray.getObject(i); - if (base instanceof COSArray) - { - // Another VE - boolean isHidden = isHiddenVisibilityExpression((COSArray) base); - if (isHidden) - { - return true; - } - } - else if (base instanceof COSDictionary) - { - // Another OCG - PDPropertyList prop = PDPropertyList.create((COSDictionary) base); - boolean isHidden = isHiddenOCG(prop); - if (isHidden) - { - return true; - } - } - } - return false; - } - - private boolean isHiddenOrVisibilityExpression(COSArray veArray) - { - // hidden only if all are hidden - for (int i = 1; i < veArray.size(); ++i) - { - COSBase base = veArray.getObject(i); - if (base instanceof COSArray) - { - // Another VE - boolean isHidden = isHiddenVisibilityExpression((COSArray) base); - if (!isHidden) - { - return false; - } - } - else if (base instanceof COSDictionary) - { - // Another OCG - PDPropertyList prop = PDPropertyList.create((COSDictionary) base); - boolean isHidden = isHiddenOCG(prop); - if (!isHidden) - { - return false; - } - } - } - return true; - } - - private boolean isHiddenNotVisibilityExpression(COSArray veArray) - { - if (veArray.size() != 2) - { - return false; - } - COSBase base = veArray.getObject(1); - if (base instanceof COSArray) - { - // Another VE - return !isHiddenVisibilityExpression((COSArray) base); - } - else if (base instanceof COSDictionary) - { - // Another OCG - PDPropertyList prop = PDPropertyList.create((COSDictionary) base); - return !isHiddenOCG(prop); - } - return false; - } - - private LookupTable getInvLookupTable() - { - if (invTable == null) - { - byte[] inv = new byte[256]; - for (int i = 0; i < inv.length; i++) - { - inv[i] = (byte) (255 - i); - } - invTable = new ByteLookupTable(0, inv); - } - return invTable; - } -} +/* + * 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.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.TexturePaint; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import static java.awt.geom.AffineTransform.TYPE_FLIP; +import static java.awt.geom.AffineTransform.TYPE_MASK_SCALE; +import static java.awt.geom.AffineTransform.TYPE_TRANSLATION; +import java.awt.geom.Area; +import java.awt.geom.GeneralPath; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ByteLookupTable; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.LookupOp; +import java.awt.image.LookupTable; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine; +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.function.PDFunction; +import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType3Font; +import org.apache.pdfbox.pdmodel.font.PDVectorFont; +import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; +import org.apache.pdfbox.pdmodel.graphics.color.PDPattern; +import org.apache.pdfbox.pdmodel.graphics.color.PDSeparation; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup; +import org.apache.pdfbox.pdmodel.graphics.image.PDImage; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup.RenderState; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentMembershipDictionary; +import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern; +import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern; +import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; +import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState; +import org.apache.pdfbox.pdmodel.graphics.state.PDSoftMask; +import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode; +import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationUnknown; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.util.Vector; + +/** + * Paints a page in a PDF document to a Graphics context. May be subclassed to provide custom + * rendering. + * + *

+ * If you want to do custom graphics processing rather than Graphics2D rendering, then you should + * subclass {@link PDFGraphicsStreamEngine} instead. Subclassing PageDrawer is only suitable for + * cases where the goal is to render onto a {@link Graphics2D} surface. In that case you'll also + * have to subclass {@link PDFRenderer} and override + * {@link PDFRenderer#createPageDrawer(PageDrawerParameters)}. See the OpaquePDFRenderer.java + * example in the source code download on how to do this. + * + * @author Ben Litchfield + */ +public class PageDrawer extends PDFGraphicsStreamEngine +{ + private static final Logger LOG = LogManager.getLogger(PageDrawer.class); + + private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); + private static final boolean IS_WINDOWS = OS_NAME.startsWith("windows"); + private static final boolean IS_LINUX = OS_NAME.startsWith("linux"); + + // parent document renderer - note: this is needed for not-yet-implemented resource caching + private final PDFRenderer renderer; + + private final boolean subsamplingAllowed; + + // the graphics device to draw to, xform is the initial transform of the device (i.e. DPI) + private Graphics2D graphics; + private AffineTransform xform; + private float xformScalingFactorX; + private float xformScalingFactorY; + + // the page box to draw (usually the crop box but may be another) + private PDRectangle pageSize; + + // whether image of a transparency group must be flipped + // needed when in a tiling pattern + private boolean flipTG = false; + + // clipping winding rule used for the clipping path + private int clipWindingRule = -1; + private GeneralPath linePath = new GeneralPath(); + + // last clipping path + private List lastClips; + + // clip when drawPage() is called, can be null, must be intersected when clipping + private Shape initialClip; + + // shapes of glyphs being drawn to be used for clipping + private List textClippings; + + // glyph caches + private final Map glyphCaches = new HashMap<>(); + + private final TilingPaintFactory tilingPaintFactory = new TilingPaintFactory(this); + + private final Deque transparencyGroupStack = new ArrayDeque<>(); + + // if greater zero the content is hidden and will not be rendered + private int nestedHiddenOCGCount; + + private final RenderDestination destination; + private final RenderingHints renderingHints; + private final float imageDownscalingOptimizationThreshold; + private LookupTable invTable = null; + private final Map blendModeMap = new HashMap<>(); + + /** + * Default annotations filter, returns all annotations + */ + private AnnotationFilter annotationFilter = annotation -> true; + + /** + * Constructor. + * + * @param parameters Parameters for page drawing. + * @throws IOException If there is an error loading properties from the file. + */ + public PageDrawer(PageDrawerParameters parameters) throws IOException + { + super(parameters.getPage()); + this.renderer = parameters.getRenderer(); + this.subsamplingAllowed = parameters.isSubsamplingAllowed(); + this.destination = parameters.getDestination(); + this.renderingHints = parameters.getRenderingHints(); + this.imageDownscalingOptimizationThreshold = + parameters.getImageDownscalingOptimizationThreshold(); + } + + /** + * Return the AnnotationFilter. + * + * @return the AnnotationFilter + */ + public AnnotationFilter getAnnotationFilter() + { + return annotationFilter; + } + + /** + * Set the AnnotationFilter. + * + *

Allows to only render annotation accepted by the filter. + * + * @param annotationFilter the AnnotationFilter + */ + public void setAnnotationFilter(AnnotationFilter annotationFilter) + { + this.annotationFilter = annotationFilter; + } + + /** + * Returns the parent renderer. + * + * @return the parent renderer + */ + public final PDFRenderer getRenderer() + { + return renderer; + } + + /** + * Returns the underlying Graphics2D. May be null if drawPage has not yet been called. + * + * @return the underlying Graphics2D + */ + protected final Graphics2D getGraphics() + { + return graphics; + } + + /** + * Returns the current line path. This is reset to empty after each fill/stroke. + * + * @return the current line path + */ + protected final GeneralPath getLinePath() + { + return linePath; + } + + /** + * Sets high-quality rendering hints on the current Graphics2D. + */ + private void setRenderingHints() + { + graphics.addRenderingHints(renderingHints); + } + + /** + * Draws the page to the requested context. + * + * @param g The graphics context to draw onto. + * @param pageSize The size of the page to draw. + * @throws IOException If there is an IO error while drawing the page. + */ + public void drawPage(Graphics2D g, PDRectangle pageSize) throws IOException + { + graphics = g; + xform = graphics.getTransform(); + Matrix m = new Matrix(xform); + xformScalingFactorX = Math.abs(m.getScalingFactorX()); + xformScalingFactorY = Math.abs(m.getScalingFactorY()); + initialClip = graphics.getClip(); + this.pageSize = pageSize; + + setRenderingHints(); + + graphics.translate(0, pageSize.getHeight()); + graphics.scale(1, -1); + + // adjust for non-(0,0) crop box + graphics.translate(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY()); + + processPage(getPage()); + + for (PDAnnotation annotation : getPage().getAnnotations(annotationFilter)) + { + showAnnotation(annotation); + } + + graphics = null; + } + + /** + * Draws the pattern stream to the requested context. + * + * @param g The graphics context to draw onto. + * @param pattern The tiling pattern to be used. + * @param colorSpace color space for this tiling. + * @param color color for this tiling. + * @param patternMatrix the pattern matrix + * @throws IOException If there is an IO error while drawing the page. + */ + void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace colorSpace, + PDColor color, Matrix patternMatrix) throws IOException + { + Graphics2D savedGraphics = graphics; + graphics = g; + + GeneralPath savedLinePath = linePath; + linePath = new GeneralPath(); + int savedClipWindingRule = clipWindingRule; + clipWindingRule = -1; + + List savedLastClips = lastClips; + lastClips = null; + Shape savedInitialClip = initialClip; + initialClip = null; + + boolean savedFlipTG = flipTG; + flipTG = true; + + setRenderingHints(); + processTilingPattern(pattern, color, colorSpace, patternMatrix); + + flipTG = savedFlipTG; + graphics = savedGraphics; + linePath = savedLinePath; + lastClips = savedLastClips; + initialClip = savedInitialClip; + clipWindingRule = savedClipWindingRule; + } + + private float clampColor(float color) + { + return color < 0 ? 0 : (color > 1 ? 1 : color); + } + + /** + * Returns an AWT paint for the given PDColor. + * + * @param color The color to get a paint for. This can be an actual color or a pattern. + * @return an AWT paint for the given PDColor + * + * @throws IOException if the AWT paint could not be created + */ + protected Paint getPaint(PDColor color) throws IOException + { + PDColorSpace colorSpace = color.getColorSpace(); + if (colorSpace == null) // PDFBOX-5782 + { + LOG.error("colorSpace is null, will be rendered as transparency"); + return new Color(0, 0, 0, 0); + } + else if (colorSpace instanceof PDSeparation && + "None".equals(((PDSeparation) colorSpace).getColorantName())) + { + // PDFBOX-4900: "The special colorant name None shall not produce any visible output" + //TODO better solution needs to be found for all occurences where toRGB is called + return new Color(0, 0, 0, 0); + } + else if (!(colorSpace instanceof PDPattern)) + { + float[] rgb = colorSpace.toRGB(color.getComponents()); + return new Color(clampColor(rgb[0]), clampColor(rgb[1]), clampColor(rgb[2])); + } + else + { + PDPattern patternSpace = (PDPattern)colorSpace; + PDAbstractPattern pattern = patternSpace.getPattern(color); + if (pattern instanceof PDTilingPattern) + { + PDTilingPattern tilingPattern = (PDTilingPattern) pattern; + + if (tilingPattern.getPaintType() == PDTilingPattern.PAINT_COLORED) + { + // colored tiling pattern + return tilingPaintFactory.create(tilingPattern, null, null, xform); + } + else + { + // uncolored tiling pattern + return tilingPaintFactory.create(tilingPattern, + patternSpace.getUnderlyingColorSpace(), color, xform); + } + } + else + { + PDShadingPattern shadingPattern = (PDShadingPattern)pattern; + PDShading shading = shadingPattern.getShading(); + if (shading == null) + { + LOG.error("shadingPattern is null, will be filled with transparency"); + return new Color(0,0,0,0); + } + return shading.toPaint(Matrix.concatenate(getInitialMatrix(), + shadingPattern.getMatrix())); + } + } + } + + /** + * Sets the clipping path using caching for performance. We track lastClip manually because + * {@link Graphics2D#getClip()} returns a new object instead of the same one passed to + * {@link Graphics2D#setClip(java.awt.Shape) setClip()}. You may need to call this if you override + * {@link #showGlyph(Matrix, PDFont, int, Vector) showGlyph()}. See + * PDFBOX-5093 for more. + */ + protected final void setClip() + { + List clippingPaths = getGraphicsState().getCurrentClippingPaths(); + if (clippingPaths != lastClips) + { + transferClip(graphics); + if (initialClip != null) + { + // apply the remembered initial clip, but transform it first + //TODO see PDFBOX-4583 + } + lastClips = clippingPaths; + } + } + + /** + * Transfer clip to the destination device. Override this if you want to avoid to do slow + * intersecting operations but want the destination device to do this (e.g. SVG). You can get + * the individual clippings via {@link PDGraphicsState#getCurrentClippingPaths()}. See + * PDFBOX-5258 for sample code. + * + * @param graphics graphics device + */ + protected void transferClip(Graphics2D graphics) + { + Area clippingPath = getGraphicsState().getCurrentClippingPath(); + if (clippingPath.getPathIterator(null).isDone()) + { + // PDFBOX-4821: avoid bug with java printing that empty clipping path is ignored by + // replacing with empty rectangle, works because this is not an empty path + graphics.setClip(new Rectangle()); + } + else + { + graphics.setClip(clippingPath); + } + } + + @Override + public void beginText() throws IOException + { + setClip(); + beginTextClip(); + } + + @Override + public void endText() throws IOException + { + endTextClip(); + } + + /** + * Begin buffering the text clipping path, if any. + */ + private void beginTextClip() + { + // buffer the text clippings because they represents a single clipping area + textClippings = new ArrayList<>(); + } + + /** + * End buffering the text clipping path, if any. + */ + private void endTextClip() + { + PDGraphicsState state = getGraphicsState(); + RenderingMode renderingMode = state.getTextState().getRenderingMode(); + + // apply the buffered clip as one area + if (renderingMode.isClip() && !textClippings.isEmpty()) + { + // PDFBOX-4150: this is much faster than using textClippingArea.add(new Area(glyph)) + // https://stackoverflow.com/questions/21519007/fast-union-of-shapes-in-java + GeneralPath path = new GeneralPath(Path2D.WIND_NON_ZERO, textClippings.size()); + textClippings.forEach(shape -> path.append(shape, false)); + state.intersectClippingPath(path); + textClippings = new ArrayList<>(); + + // PDFBOX-3681: lastClip needs to be reset, because after intersection it is still the same + // object, thus setClip() would believe that it is cached. + lastClips = null; + } + } + + @Override + protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, + Vector displacement) throws IOException + { + AffineTransform at = textRenderingMatrix.createAffineTransform(); + at.concatenate(font.getFontMatrix().createAffineTransform()); + + // create cache if it does not exist + PDVectorFont vectorFont = (PDVectorFont) font; + GlyphCache cache = glyphCaches.get(font); + if (cache == null) + { + cache = new GlyphCache(vectorFont); + glyphCaches.put(font, cache); + } + + GeneralPath path = cache.getPathForCharacterCode(code); + drawGlyph(path, font, code, displacement, at); + } + + /** + * Renders a glyph. + * + * @param path the GeneralPath for the glyph + * @param font the font + * @param code character code + * @param displacement the glyph's displacement (advance) + * @param at the transformation + * @throws IOException if something went wrong + */ + private void drawGlyph(GeneralPath path, PDFont font, int code, Vector displacement, AffineTransform at) throws IOException + { + PDGraphicsState state = getGraphicsState(); + RenderingMode renderingMode = state.getTextState().getRenderingMode(); + + if (path != null) + { + // Stretch non-embedded glyph if it does not match the height/width contained in the PDF. + // Vertical fonts have zero X displacement, so the following code scales to 0 if we don't skip it. + // TODO: How should vertical fonts be handled? + if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code)) + { + float fontWidth = font.getWidthFromFont(code); + if (displacement.getX() > 0 && // PDFBOX-5611: ignore zero widths + fontWidth > 0 && // ignore spaces + Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001) + { + float pdfWidth = displacement.getX() * 1000; + at.scale(pdfWidth / fontWidth, 1); + } + } + + // render glyph + Shape glyph = at.createTransformedShape(path); + + if (isContentRendered()) + { + if (renderingMode.isFill()) + { + graphics.setComposite(state.getNonStrokingJavaComposite()); + graphics.setPaint(getNonStrokingPaint()); + setClip(); + graphics.fill(glyph); + } + + if (renderingMode.isStroke()) + { + graphics.setComposite(state.getStrokingJavaComposite()); + graphics.setPaint(getStrokingPaint()); + graphics.setStroke(getStroke()); + setClip(); + graphics.draw(glyph); + } + } + + if (renderingMode.isClip()) + { + textClippings.add(glyph); + } + } + } + + @Override + protected void showType3Glyph(Matrix textRenderingMatrix, PDType3Font font, int code, + Vector displacement) throws IOException + { + PDGraphicsState state = getGraphicsState(); + RenderingMode renderingMode = state.getTextState().getRenderingMode(); + if (RenderingMode.NEITHER != renderingMode) + { + super.showType3Glyph(textRenderingMatrix, font, code, displacement); + } + } + + @Override + public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) + { + // to ensure that the path is created in the right direction, we have to create + // it by combining single lines instead of creating a simple rectangle + linePath.moveTo((float) p0.getX(), (float) p0.getY()); + linePath.lineTo((float) p1.getX(), (float) p1.getY()); + linePath.lineTo((float) p2.getX(), (float) p2.getY()); + linePath.lineTo((float) p3.getX(), (float) p3.getY()); + + // close the subpath instead of adding the last line so that a possible set line + // cap style isn't taken into account at the "beginning" of the rectangle + linePath.closePath(); + } + + private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException + { + if (softMask == null || softMask.getGroup() == null) + { + return parentPaint; + } + PDColor backdropColor = null; + if (COSName.LUMINOSITY.equals(softMask.getSubType())) + { + COSArray backdropColorArray = softMask.getBackdropColor(); + if (backdropColorArray != null) + { + PDTransparencyGroup form = softMask.getGroup(); + PDColorSpace colorSpace = form.getGroup().getColorSpace(form.getResources()); + if (colorSpace != null && + colorSpace.getNumberOfComponents() == backdropColorArray.size()) // PDFBOX-5795 + { + backdropColor = new PDColor(backdropColorArray, colorSpace); + } + } + } + TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true, + softMask.getInitialTransformationMatrix(), backdropColor); + BufferedImage image = transparencyGroup.getImage(); + if (image == null) + { + // Adobe Reader ignores empty softmasks instead of using bc color + // sample file: PDFJS-6967_reduced_outside_softmask.pdf + return parentPaint; + } + BufferedImage gray = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + if (COSName.ALPHA.equals(softMask.getSubType())) + { + gray.setData(image.getAlphaRaster()); + } + else if (COSName.LUMINOSITY.equals(softMask.getSubType())) + { + Graphics g = gray.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + } + else + { + throw new IOException("Invalid soft mask subtype."); + } + gray = adjustImage(gray); + + Rectangle2D tpgBounds = transparencyGroup.getBounds(); + return new SoftMask(parentPaint, gray, tpgBounds, backdropColor, softMask.getTransferFunction()); + } + + // returns the image adjusted for applySoftMaskToPaint(). + private BufferedImage adjustImage(BufferedImage gray) + { + AffineTransform at = new AffineTransform(xform); + at.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); + + Rectangle originalBounds = new Rectangle(gray.getWidth(), gray.getHeight()); + Rectangle2D transformedBounds = at.createTransformedShape(originalBounds).getBounds2D(); + at.preConcatenate(AffineTransform.getTranslateInstance(-transformedBounds.getMinX(), + -transformedBounds.getMinY())); + + int width = (int) Math.ceil(transformedBounds.getWidth()); + int height = (int) Math.ceil(transformedBounds.getHeight()); + + if (width == gray.getWidth() && height == gray.getHeight() && at.isIdentity()) + { + return gray; + } + + BufferedImage transformedGray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2 = (Graphics2D) transformedGray.getGraphics(); + g2.drawImage(gray, at, null); + g2.dispose(); + return transformedGray; + } + + // returns the stroking AWT Paint + private Paint getStrokingPaint() throws IOException + { + PDGraphicsState graphicsState = getGraphicsState(); + return applySoftMaskToPaint( + getPaint(graphicsState.getStrokingColor()), graphicsState.getSoftMask()); + } + + /** + * Returns the non-stroking AWT Paint. You may need to call this if you override + * {@link #showGlyph(Matrix, PDFont, int, Vector) showGlyph()}. See + * PDFBOX-5093 for more. + * + * @return The non-stroking AWT Paint. + * @throws IOException if the non-stroking AWT Paint could not be created + */ + protected final Paint getNonStrokingPaint() throws IOException + { + PDGraphicsState graphicsState = getGraphicsState(); + return applySoftMaskToPaint( + getPaint(graphicsState.getNonStrokingColor()), graphicsState.getSoftMask()); + } + + // create a new stroke based on the current CTM and the current stroke + private Stroke getStroke() + { + PDGraphicsState state = getGraphicsState(); + + // apply the CTM + float lineWidth = transformWidth(state.getLineWidth()); + + // minimum line width as used by Adobe Reader + if (lineWidth < 0.25) + { + lineWidth = 0.25f; + } + + PDLineDashPattern dashPattern = state.getLineDashPattern(); + // PDFBOX-5168: show an all-zero dash array line invisible like Adobe does + // must do it here because getDashArray() sets minimum width because of JVM bugs + float[] dashArray = dashPattern.getDashArray(); + if (isAllZeroDash(dashArray)) + { + return (Shape p) -> new Area(); + } + float phaseStart = dashPattern.getPhase(); + dashArray = getDashArray(dashPattern); + phaseStart = transformWidth(phaseStart); + + int lineCap = Math.min(2, Math.max(0, state.getLineCap())); // legal values 0..2 + int lineJoin = Math.min(2, Math.max(0, state.getLineJoin())); + float miterLimit = state.getMiterLimit(); + if (miterLimit < 1) + { + LOG.warn("Miter limit must be >= 1, value {} is ignored", miterLimit); + miterLimit = 10; + } + return new BasicStroke(lineWidth, lineCap, lineJoin, + miterLimit, dashArray, phaseStart); + } + + private boolean isAllZeroDash(float[] dashArray) + { + if (dashArray.length > 0) + { + for (int i = 0; i < dashArray.length; ++i) + { + if (dashArray[i] != 0) + { + return false; + } + } + return true; + } + return false; + } + + private float[] getDashArray(PDLineDashPattern dashPattern) + { + float[] dashArray = dashPattern.getDashArray(); + // avoid empty, infinite and NaN values (PDFBOX-3360) + if (dashArray.length == 0) + { + return null; + } + for (int i = 0; i < dashArray.length; ++i) + { + if (Float.isInfinite(dashArray[i]) || Float.isNaN(dashArray[i])) + { + return null; + } + } + for (int i = 0; i < dashArray.length; ++i) + { + // apply the CTM + float w = transformWidth(dashArray[i]); + // minimum line dash width avoids JVM crash, + // see PDFBOX-2373, PDFBOX-2929, PDFBOX-3204, PDFBOX-3813 + // also avoid 0 in array like "[ 0 1000 ] 0 d", see PDFBOX-3724 + if (xformScalingFactorX < 0.5f) + { + // PDFBOX-4492 + dashArray[i] = Math.max(w, 0.2f); + } + else + { + dashArray[i] = Math.max(w, 0.062f); + } + } + return dashArray; + } + + @Override + public void strokePath() throws IOException + { + if (isContentRendered()) + { + graphics.setComposite(getGraphicsState().getStrokingJavaComposite()); + graphics.setPaint(getStrokingPaint()); + graphics.setStroke(getStroke()); + setClip(); + graphics.draw(linePath); + } + linePath.reset(); + } + + @Override + public void fillPath(int windingRule) throws IOException + { + PDGraphicsState graphicsState = getGraphicsState(); + graphics.setComposite(graphicsState.getNonStrokingJavaComposite()); + setClip(); + linePath.setWindingRule(windingRule); + + // disable anti-aliasing for rectangular paths, this is a workaround to avoid small stripes + // which occur when solid fills are used to simulate piecewise gradients, see PDFBOX-2302 + // note that we ignore paths with a width/height under 1 as these are fills used as strokes, + // see PDFBOX-1658 for an example + Rectangle2D bounds = linePath.getBounds2D(); + boolean noAntiAlias = isRectangular(linePath) && bounds.getWidth() > 1 && + bounds.getHeight() > 1; + if (noAntiAlias) + { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_OFF); + } + + Shape shape; + if (graphicsState.getNonStrokingColorSpace() instanceof PDPattern) + { + // apply clip to path to avoid oversized device bounds in shading contexts (PDFBOX-2901) + Area area = new Area(linePath); + Shape clip = graphics.getClip(); + if (clip != null) + { + area.intersect(new Area(clip)); + } + intersectShadingBBox(graphicsState.getNonStrokingColor(), area); + shape = area; + } + else + { + shape = linePath; + } + if (isContentRendered() && !shape.getPathIterator(null).isDone()) + { + // creating Paint is sometimes a costly operation, so avoid if possible + graphics.setPaint(getNonStrokingPaint()); + graphics.fill(shape); + } + + linePath.reset(); + + if (noAntiAlias) + { + // JDK 1.7 has a bug where rendering hints are reset by the above call to + // the setRenderingHint method, so we re-set all hints, see PDFBOX-2302 + setRenderingHints(); + } + } + + // checks whether this is a shading pattern and if yes, + // get the transformed BBox and intersect with current paint area + // need to do it here and not in shading getRaster() because it may have been rotated + private void intersectShadingBBox(PDColor color, Area area) throws IOException + { + if (color.getColorSpace() instanceof PDPattern) + { + PDColorSpace colorSpace = color.getColorSpace(); + PDAbstractPattern pat = ((PDPattern) colorSpace).getPattern(color); + if (pat instanceof PDShadingPattern) + { + PDShading shading = ((PDShadingPattern) pat).getShading(); + PDRectangle bbox = shading.getBBox(); + if (bbox != null) + { + Matrix m = Matrix.concatenate(getInitialMatrix(), pat.getMatrix()); + Area bboxArea = new Area(bbox.transform(m)); + area.intersect(bboxArea); + } + } + } + } + + /** + * Returns true if the given path is rectangular. + */ + private boolean isRectangular(GeneralPath path) + { + PathIterator iter = path.getPathIterator(null); + double[] coords = new double[6]; + int count = 0; + int[] xs = new int[4]; + int[] ys = new int[4]; + while (!iter.isDone()) + { + switch(iter.currentSegment(coords)) + { + case PathIterator.SEG_MOVETO: + if (count == 0) + { + xs[count] = (int)Math.floor(coords[0]); + ys[count] = (int)Math.floor(coords[1]); + } + else + { + return false; + } + count++; + break; + + case PathIterator.SEG_LINETO: + if (count < 4) + { + xs[count] = (int)Math.floor(coords[0]); + ys[count] = (int)Math.floor(coords[1]); + } + else + { + return false; + } + count++; + break; + + case PathIterator.SEG_CUBICTO: + return false; + + default: + break; + } + iter.next(); + } + + if (count == 4) + { + return xs[0] == xs[1] || xs[0] == xs[2] || + ys[0] == ys[1] || ys[0] == ys[3]; + } + return false; + } + + /** + * Fills and then strokes the path. + * + * @param windingRule The winding rule this path will use. + * @throws IOException If there is an IO error while filling the path. + */ + @Override + public void fillAndStrokePath(int windingRule) throws IOException + { + // Cloning needed because fillPath() resets linePath + GeneralPath path = (GeneralPath)linePath.clone(); + fillPath(windingRule); + linePath = path; + strokePath(); + } + + @Override + public void clip(int windingRule) + { + // the clipping path will not be updated until the succeeding painting operator is called + clipWindingRule = windingRule; + if (clipWindingRule != -1) + { + linePath.setWindingRule(clipWindingRule); + + if (!linePath.getPathIterator(null).isDone()) + { + // PDFBOX-4949 / PDF.js 12306: don't clip if "W n" only + getGraphicsState().intersectClippingPath(adjustClip(linePath)); + } + + // PDFBOX-3836: lastClip needs to be reset, because after intersection it is still the same + // object, thus setClip() would believe that it is cached. + lastClips = null; + + clipWindingRule = -1; + } + } + + @Override + public void moveTo(float x, float y) + { + linePath.moveTo(x, y); + } + + @Override + public void lineTo(float x, float y) + { + linePath.lineTo(x, y); + } + + @Override + public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) + { + linePath.curveTo(x1, y1, x2, y2, x3, y3); + } + + @Override + public Point2D getCurrentPoint() + { + return linePath.getCurrentPoint(); + } + + @Override + public void closePath() + { + linePath.closePath(); + } + + @Override + public void endPath() + { + linePath.reset(); + } + + /** + * PDFBOX-5715 / PR#73: This was added to fix a problem with missing fine lines when printing + * on MacOS. Lines vanish because CPrinterJob sets graphics scale to 1 for Printable so after + * scaling lines often have a width smaller than 1 after scaling and clipping. This change + * enlarges the clip bounds to cover at least 1 point plus 0.5 on one and another side in the + * device space to allow to draw the linePath inside the clip. The linePath can consists from + * different lines but when its bounds width or height is less than 1.0 it seems safe to use a + * rectangle as a clip instead of the real path. A more detailed explanation can be read + * here. + * + * @param linePath + * @return + */ + private GeneralPath adjustClip(GeneralPath linePath) + { + AffineTransform tx = graphics.getTransform(); + int type = tx.getType(); + + if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP)) == 0) + { + return linePath; + } + else if ((type & ~(TYPE_TRANSLATION | TYPE_FLIP | TYPE_MASK_SCALE)) == 0) + { + double sx = Math.abs(tx.getScaleX()); + double sy = Math.abs(tx.getScaleY()); + if (sx > 1.0 && sy > 1.0) + { + return linePath; + } + + Rectangle2D bounds = linePath.getBounds(); + double w = bounds.getWidth(); + double h = bounds.getHeight(); + double sw = sx * w; + double sh = sy * h; + final double minSize = 2.0; + if (sw < minSize || sh < minSize) + { + double x = bounds.getX(); + double y = bounds.getY(); + if (sw < minSize) + { + w = minSize / sx; + x = bounds.getCenterX() - w / 2; + } + if (sh < minSize) + { + h = minSize / sy; + y = bounds.getCenterY() - h / 2; + } + return new GeneralPath(new Rectangle2D.Double(x, y, w, h)); + } + } + return linePath; + } + + @Override + public void drawImage(PDImage pdImage) throws IOException + { + if (pdImage instanceof PDImageXObject && + isHiddenOCG(((PDImageXObject) pdImage).getOptionalContent())) + { + return; + } + if (!isContentRendered()) + { + return; + } + Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); + AffineTransform at = ctm.createAffineTransform(); + + if (!pdImage.getInterpolate()) + { + // if the image is scaled down, we use smooth interpolation, eg PDFBOX-2364 + // only when scaled up do we use nearest neighbour, eg PDFBOX-2302 / mori-cvpr01.pdf + // PDFBOX-4930: we use the sizes of the ARGB image. These can be different + // than the original sizes of the base image, when the mask is bigger. + // PDFBOX-5091: also consider subsampling, the sizes are different too. + BufferedImage bim; + if (subsamplingAllowed) + { + bim = pdImage.getImage(null, getSubsampling(pdImage, at)); + } + else + { + bim = pdImage.getImage(); + } + boolean isScaledUp = + bim.getWidth() <= Math.abs(Math.round(ctm.getScalingFactorX() * xformScalingFactorX)) || + bim.getHeight() <= Math.abs(Math.round(ctm.getScalingFactorY() * xformScalingFactorY)); + if (isScaledUp) + { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + } + } + + graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); + setClip(); + + if (pdImage.isStencil()) + { + if (getGraphicsState().getNonStrokingColor().getColorSpace() instanceof PDPattern) + { + // The earlier code for stencils (see "else") doesn't work with patterns because the + // CTM is not taken into consideration. + // this code is based on the fact that it is easily possible to draw the mask and + // the paint at the correct place with the existing code, but not in one step. + // Thus what we do is to draw both in separate images, then combine the two and draw + // the result. + // Note that the device scale is not used. In theory, some patterns can get better + // at higher resolutions but the stencil would become more and more "blocky". + // If anybody wants to do this, have a look at the code in showTransparencyGroup(). + + // draw the paint + Paint paint = getNonStrokingPaint(); + Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1); + Rectangle2D bounds = at.createTransformedShape(unitRect).getBounds2D(); + int w = (int) Math.ceil(bounds.getWidth()); + int h = (int) Math.ceil(bounds.getHeight()); + BufferedImage renderedPaint = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = (Graphics2D) renderedPaint.getGraphics(); + g.translate(-bounds.getMinX(), -bounds.getMinY()); + g.setPaint(paint); + g.setRenderingHints(graphics.getRenderingHints()); + g.fill(bounds); + g.dispose(); + + // draw the mask + BufferedImage mask = pdImage.getImage(); + AffineTransform imageTransform = new AffineTransform(at); + imageTransform.scale(1.0 / mask.getWidth(), -1.0 / mask.getHeight()); + imageTransform.translate(0, -mask.getHeight()); + AffineTransform full = new AffineTransform(g.getTransform()); + full.concatenate(imageTransform); + Matrix m = new Matrix(full); + double scaleX = Math.abs(m.getScalingFactorX()); + double scaleY = Math.abs(m.getScalingFactorY()); + + boolean smallMask = mask.getWidth() <= 8 && mask.getHeight() <= 8; + if (mask.getWidth() == 1 && mask.getHeight() == 1) + { + // PDFBOX-5802: force usage of the lookup table if it is only 1 pixel + // (See the comment for PDFBOX-5403 that it isn't done for some + // cases based purely on the rendering result of one file!) + smallMask = false; + } + if (!smallMask) + { + // PDFBOX-5403: + // The mask is copied to RGB because this supports a smooth scaling, so we + // get a mask with 255 values instead of just 0 and 255. + // Inverting is done because when we don't do it, the getScaledInstance() call + // produces a black line in many masks. With the inversion we have a white line + // which is neutral. Because of the inversion we don't have to substract from 255 + // in the "apply the mask" segment when rasterPixel[3] is assigned. + + // The inversion is not done for very small ones, because of + // PDFBOX-2171-002-002710-p14.pdf where the "New Harmony Consolidated" and + // "Sailor Springs" patterns became almost invisible. + // (We may have to decide this differently in the future, e.g. on b/w relationship) + BufferedImage tmp = new BufferedImage(mask.getWidth(), mask.getHeight(), BufferedImage.TYPE_INT_RGB); + mask = new LookupOp(getInvLookupTable(), graphics.getRenderingHints()).filter(mask, tmp); + } + + BufferedImage renderedMask = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + g = (Graphics2D) renderedMask.getGraphics(); + g.translate(-bounds.getMinX(), -bounds.getMinY()); + g.setRenderingHints(graphics.getRenderingHints()); + + if (smallMask) + { + g.drawImage(mask, imageTransform, null); + } + else if (scaleX != 0 && scaleY != 0) + { + while (scaleX < 0.25 || Math.round(mask.getWidth() * scaleX) < 1) + { + scaleX *= 2.0; + } + while (scaleY < 0.25 || Math.round(mask.getHeight() * scaleY) < 1) + { + scaleY *= 2.0; + } + int w2 = (int) Math.round(mask.getWidth() * scaleX); + int h2 = (int) Math.round(mask.getHeight() * scaleY); + + Image scaledMask = mask.getScaledInstance(w2, h2, Image.SCALE_SMOOTH); + imageTransform.scale(1f / Math.abs(scaleX), 1f / Math.abs(scaleY)); + g.drawImage(scaledMask, imageTransform, null); + } + g.dispose(); + + // apply the mask + int[] alphaPixel = null; + int[] rasterPixel = null; + WritableRaster raster = renderedPaint.getRaster(); + WritableRaster alpha = renderedMask.getRaster(); + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + alphaPixel = alpha.getPixel(x, y, alphaPixel); + rasterPixel = raster.getPixel(x, y, rasterPixel); + rasterPixel[3] = alphaPixel[0]; + raster.setPixel(x, y, rasterPixel); + } + } + + // draw the image + graphics.drawImage(renderedPaint, + AffineTransform.getTranslateInstance(bounds.getMinX(), bounds.getMinY()), + null); + } + else + { + // fill the image with stenciled paint + BufferedImage image = pdImage.getStencilImage(getNonStrokingPaint()); + + // draw the image + drawBufferedImage(pdImage, image, at); + } + } + else + { + if (subsamplingAllowed) + { + int subsampling = getSubsampling(pdImage, at); + // draw the subsampled image + drawBufferedImage(pdImage, pdImage.getImage(null, subsampling), at); + } + else + { + // subsampling not allowed, draw the image + drawBufferedImage(pdImage, pdImage.getImage(), at); + } + } + + if (!pdImage.getInterpolate()) + { + // JDK 1.7 has a bug where rendering hints are reset by the above call to + // the setRenderingHint method, so we re-set all hints, see PDFBOX-2302 + setRenderingHints(); + } + } + + /** + * Calculates the subsampling frequency for a given PDImage based on the current transformation + * and its calculated transform. Extend this method if you want to use your own strategy. + * + * @param pdImage PDImage to be drawn + * @param at Transform that will be applied to the image when drawing + * @return The rounded-down ratio of image pixels to drawn pixels. Returned value will always be + * >=1. + */ + protected int getSubsampling(PDImage pdImage, AffineTransform at) + { + // calculate subsampling according to the resulting image size + double scale = Math.abs(at.getDeterminant() * xform.getDeterminant()); + + int subsampling = (int) Math.floor(Math.sqrt(pdImage.getWidth() * pdImage.getHeight() / scale)); + if (subsampling > 8) + { + subsampling = 8; + } + if (subsampling < 1) + { + subsampling = 1; + } + if (subsampling > pdImage.getWidth() || subsampling > pdImage.getHeight()) + { + // For very small images it is possible that the subsampling would imply 0 size. + // To avoid problems, the subsampling is set to no less than the smallest dimension. + subsampling = Math.min(pdImage.getWidth(), pdImage.getHeight()); + } + return subsampling; + } + + private void drawBufferedImage(PDImage pdImage, BufferedImage image, AffineTransform at) throws IOException + { + AffineTransform originalTransform = graphics.getTransform(); + AffineTransform imageTransform = new AffineTransform(at); + int width = image.getWidth(); + int height = image.getHeight(); + imageTransform.scale(1.0 / width, -1.0 / height); + imageTransform.translate(0, -height); + + PDSoftMask softMask = getGraphicsState().getSoftMask(); + + // PDFBOX-5307 / PDF.js PR#19269 + // From section 11.6.4.3 Mask Shape and Opacity in the PDF specification: + // "Either form of mask in the image dictionary shall override the current soft mask + // in the graphics state" + boolean hasImageMask = pdImage.getCOSObject().containsKey(COSName.MASK) || + pdImage.getCOSObject().containsKey(COSName.SMASK); + + if (softMask != null && !hasImageMask) + { + Rectangle2D rectangle = new Rectangle2D.Float(0, 0, width, height); + Paint awtPaint = new TexturePaint(image, rectangle); + awtPaint = applySoftMaskToPaint(awtPaint, softMask); + graphics.setPaint(awtPaint); + graphics.transform(imageTransform); + graphics.fill(rectangle); + graphics.setTransform(originalTransform); + } + else + { + COSBase transfer = getGraphicsState().getTransfer(); + if (transfer instanceof COSArray || transfer instanceof COSDictionary) + { + image = applyTransferFunction(image, transfer); + } + + // PDFBOX-4516, PDFBOX-4527, PDFBOX-4815, PDFBOX-4886, PDFBOX-4863: + // graphics.drawImage() has terrible quality when scaling down, even when + // RenderingHints.VALUE_INTERPOLATION_BICUBIC, VALUE_ALPHA_INTERPOLATION_QUALITY, + // VALUE_COLOR_RENDER_QUALITY and VALUE_RENDER_QUALITY are all set. + // A workaround is to get a pre-scaled image with Image.getScaledInstance() + // and then draw that one. To reduce differences in testing + // (partly because the method needs integer parameters), only smaller scalings + // will trigger the workaround. Because of the slowness we only do it if the user + // expects quality rendering and interpolation. + Matrix imageTransformMatrix = new Matrix(imageTransform); + Matrix graphicsTransformMatrix = new Matrix(originalTransform); + float scaleX = Math.abs(imageTransformMatrix.getScalingFactorX() * graphicsTransformMatrix.getScalingFactorX()); + float scaleY = Math.abs(imageTransformMatrix.getScalingFactorY() * graphicsTransformMatrix.getScalingFactorY()); + + if ((scaleX < imageDownscalingOptimizationThreshold || scaleY < imageDownscalingOptimizationThreshold) && + RenderingHints.VALUE_RENDER_QUALITY.equals(graphics.getRenderingHint(RenderingHints.KEY_RENDERING)) && + RenderingHints.VALUE_INTERPOLATION_BICUBIC.equals(graphics.getRenderingHint(RenderingHints.KEY_INTERPOLATION))) + { + int w = Math.round(image.getWidth() * scaleX); + int h = Math.round(image.getHeight() * scaleY); + if (w < 1 || h < 1) + { + graphics.drawImage(image, imageTransform, null); + return; + } + Image imageToDraw = image.getScaledInstance(w, h, Image.SCALE_SMOOTH); + // remove the scale (extracted from w and h, to have it from the rounded values + // hoping to reverse the rounding: without this, we get an horizontal line + // when rendering PDFJS-8860-Pattern-Size1.pdf at 100% ) + imageTransform.scale(1f / w * image.getWidth(), 1f / h * image.getHeight()); + imageTransform.preConcatenate(originalTransform); + graphics.setTransform(new AffineTransform()); + graphics.drawImage(imageToDraw, imageTransform, null); + graphics.setTransform(originalTransform); + } + else + { + GraphicsConfiguration graphicsConfiguration = graphics.getDeviceConfiguration(); + int deviceType = GraphicsDevice.TYPE_RASTER_SCREEN; + if (graphicsConfiguration != null) + { + GraphicsDevice graphicsDevice = graphicsConfiguration.getDevice(); + if (graphicsDevice != null) + { + deviceType = graphicsDevice.getType(); + } + } + if (deviceType == GraphicsDevice.TYPE_PRINTER && + image.getType() != BufferedImage.TYPE_4BYTE_ABGR && + (IS_WINDOWS || IS_LINUX)) + { + // PDFBOX-5601, PDFBOX-4010, JDK-8308099, JDK-8191800: + // workaround to avoid terrible / missing output on printer unless TYPE_4BYTE_ABGR + BufferedImage bim = new BufferedImage( + image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); + Graphics g = bim.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + image = bim; + } + graphics.drawImage(image, imageTransform, null); + } + } + } + + private BufferedImage applyTransferFunction(BufferedImage image, COSBase transfer) throws IOException + { + BufferedImage bim; + if (image.getColorModel().hasAlpha()) + { + bim = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + } + else + { + bim = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + } + + // prepare transfer functions (either one per color or one for all) + // and maps (actually arrays[256] to be faster) to avoid calculating values several times + Integer[] rMap; + Integer[] gMap; + Integer[] bMap; + PDFunction rf; + PDFunction gf; + PDFunction bf; + if (transfer instanceof COSArray) + { + COSArray ar = (COSArray) transfer; + rf = PDFunction.create(ar.getObject(0)); + gf = PDFunction.create(ar.getObject(1)); + bf = PDFunction.create(ar.getObject(2)); + rMap = new Integer[256]; + gMap = new Integer[256]; + bMap = new Integer[256]; + } + else + { + rf = PDFunction.create(transfer); + gf = rf; + bf = rf; + rMap = new Integer[256]; + gMap = rMap; + bMap = rMap; + } + + // apply the transfer function to each color, but keep alpha + float[] input = new float[1]; + for (int x = 0; x < image.getWidth(); ++x) + { + for (int y = 0; y < image.getHeight(); ++y) + { + int rgb = image.getRGB(x, y); + int ri = (rgb >> 16) & 0xFF; + int gi = (rgb >> 8) & 0xFF; + int bi = rgb & 0xFF; + int ro; + int go; + int bo; + if (rMap[ri] != null) + { + ro = rMap[ri]; + } + else + { + input[0] = (ri & 0xFF) / 255f; + ro = (int) (rf.eval(input)[0] * 255); + rMap[ri] = ro; + } + if (gMap[gi] != null) + { + go = gMap[gi]; + } + else + { + input[0] = (gi & 0xFF) / 255f; + go = (int) (gf.eval(input)[0] * 255); + gMap[gi] = go; + } + if (bMap[bi] != null) + { + bo = bMap[bi]; + } + else + { + input[0] = (bi & 0xFF) / 255f; + bo = (int) (bf.eval(input)[0] * 255); + bMap[bi] = bo; + } + bim.setRGB(x, y, (rgb & 0xFF000000) | (ro << 16) | (go << 8) | bo); + } + } + return bim; + } + + @Override + public void shadingFill(COSName shadingName) throws IOException + { + if (!isContentRendered()) + { + return; + } + PDShading shading = getResources().getShading(shadingName); + if (shading == null) + { + LOG.error("shading {} does not exist in resources dictionary", shadingName); + return; + } + Matrix ctm = getGraphicsState().getCurrentTransformationMatrix(); + + graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); + Shape savedClip = graphics.getClip(); + graphics.setClip(null); + lastClips = null; + + // get the transformed BBox and intersect with current clipping path + // need to do it here and not in shading getRaster() because it may have been rotated + PDRectangle bbox = shading.getBBox(); + Area area; + if (bbox != null) + { + area = new Area(bbox.transform(ctm)); + area.intersect(getGraphicsState().getCurrentClippingPath()); + } + else + { + Rectangle2D bounds = shading.getBounds(new AffineTransform(), ctm); + if (bounds != null) + { + bounds.add(new Point2D.Double(Math.floor(bounds.getMinX() - 1), + Math.floor(bounds.getMinY() - 1))); + bounds.add(new Point2D.Double(Math.ceil(bounds.getMaxX() + 1), + Math.ceil(bounds.getMaxY() + 1))); + area = new Area(bounds); + area.intersect(getGraphicsState().getCurrentClippingPath()); + } + else + { + area = getGraphicsState().getCurrentClippingPath(); + } + } + if (!area.isEmpty()) + { + // creating Paint is sometimes a costly operation, so avoid if possible + Paint paint = shading.toPaint(ctm); + paint = applySoftMaskToPaint(paint, getGraphicsState().getSoftMask()); + graphics.setPaint(paint); + graphics.fill(area); + } + graphics.setClip(savedClip); + } + + @Override + public void showAnnotation(PDAnnotation annotation) throws IOException + { + lastClips = null; + + if (shouldSkipAnnotation(annotation)) + { + return; + } + + //TODO support NoZoom, example can be found in p5 of PDFBOX-2348 + PDAppearanceDictionary appearance = annotation.getAppearance(); + if (appearance == null || appearance.getNormalAppearance() == null) + { + annotation.constructAppearances(renderer.document); + } + if (annotation.isNoRotate() && getCurrentPage().getRotation() != 0) + { + appearance = annotation.getAppearance(); + if (appearance != null) + { + PDAppearanceEntry appearanceEntry = appearance.getNormalAppearance(); + if (appearanceEntry != null && appearanceEntry.isStream() && + hasTransparency(appearanceEntry.getAppearanceStream())) + { + // PDFBOX-4744: avoid appearances with transparency groups until we have fixed + // the rendering. A real solution should probably be + // in PDFStreamEngine.processAnnotation(). + annotation.constructAppearances(); + } + } + PDRectangle rect = annotation.getRectangle(); + AffineTransform savedTransform = graphics.getTransform(); + // "The upper-left corner of the annotation remains at the same point in + // default user space; the annotation pivots around that point." + graphics.rotate(Math.toRadians(getCurrentPage().getRotation()), + rect.getLowerLeftX(), rect.getUpperRightY()); + super.showAnnotation(annotation); + graphics.setTransform(savedTransform); + annotation.setAppearance(appearance); // restore + } + else + { + super.showAnnotation(annotation); + } + } + + private boolean shouldSkipAnnotation(PDAnnotation annotation) + { + if (destination == RenderDestination.PRINT && !annotation.isPrinted()) + { + return true; + } + if ((destination == RenderDestination.VIEW || destination == RenderDestination.EXPORT) && + annotation.isNoView()) + { + return true; + } + if (annotation.isHidden()) + { + return true; + } + if (annotation.isInvisible() && annotation instanceof PDAnnotationUnknown) + { + // "If set, do not display the annotation if it does not belong to one + // of the standard annotation types and no annotation handler is available." + return true; + } + return isHiddenOCG(annotation.getOptionalContent()); + } + + private boolean hasTransparency(PDFormXObject form) throws IOException + { + if (form == null) + { + return false; + } + PDResources resources = form.getResources(); + if (resources == null) + { + return false; + } + for (COSName name : resources.getXObjectNames()) + { + PDXObject xObject = resources.getXObject(name); + if (xObject instanceof PDTransparencyGroup) + { + return true; + } + if (xObject instanceof PDFormXObject && hasTransparency((PDFormXObject) xObject)) + { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void showForm(PDFormXObject form) throws IOException + { + if (isHiddenOCG(form.getOptionalContent())) + { + return; + } + if (isContentRendered()) + { + GeneralPath savedLinePath = linePath; + linePath = new GeneralPath(); + super.showForm(form); + linePath = savedLinePath; + } + } + + @Override + public void showTransparencyGroup(PDTransparencyGroup form) throws IOException + { + showTransparencyGroupOnGraphics(form, graphics); + } + + /** + * For advanced users, to extract the transparency group into a separate graphics device. + * + * @param form the transparency group to be extracted + * @param graphics the target graphics device + * @throws IOException if the transparency group could not be extracted + */ + protected void showTransparencyGroupOnGraphics(PDTransparencyGroup form, Graphics2D graphics) + throws IOException + { + if (isHiddenOCG(form.getOptionalContent())) + { + return; + } + if (!isContentRendered()) + { + return; + } + TransparencyGroup group + = new TransparencyGroup(form, false, getGraphicsState().getCurrentTransformationMatrix(), null); + BufferedImage image = group.getImage(); + if (image == null) + { + // image is empty, don't bother + return; + } + + graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); + setClip(); + + // both the DPI xform and the CTM were already applied to the group, so all we do + // here is draw it directly onto the Graphics2D device at the appropriate position + AffineTransform savedTransform = graphics.getTransform(); + AffineTransform transform = new AffineTransform(xform); + transform.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); + graphics.setTransform(transform); + + // adjust bbox (x,y) position at the initial scale + cropbox + PDRectangle bbox = group.getBBox(); + float x = bbox.getLowerLeftX() - pageSize.getLowerLeftX(); + float y = pageSize.getUpperRightY() - bbox.getUpperRightY(); + + if (flipTG) + { + graphics.translate(0, image.getHeight()); + graphics.scale(1, -1); + } + else + { + graphics.translate(x * xformScalingFactorX, y * xformScalingFactorY); + } + + PDSoftMask softMask = getGraphicsState().getSoftMask(); + if (softMask != null) + { + Paint awtPaint = new TexturePaint(image, + new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight())); + awtPaint = applySoftMaskToPaint(awtPaint, softMask); + graphics.setPaint(awtPaint); + graphics.fill( + new Rectangle2D.Float(0, 0, bbox.getWidth() * xformScalingFactorX, bbox.getHeight() * xformScalingFactorY)); + } + else + { + try + { + graphics.drawImage(image, null, null); + } + catch (InternalError ie) + { + LOG.error("Exception drawing image, see JDK-6689349, " + + "try rendering into a BufferedImage instead", ie); + } + } + + graphics.setTransform(savedTransform); + } + + /** + * Transparency group. + **/ + private final class TransparencyGroup + { + private final BufferedImage image; + private final PDRectangle bbox; + + private final int minX; + private final int minY; + private final int maxX; + private final int maxY; + private final int width; + private final int height; + + /** + * Creates a buffered image for a transparency group result. + * + * @param form the transparency group of the form or soft mask. + * @param isSoftMask true if this is a soft mask. + * @param ctm the relevant current transformation matrix. For soft masks, this is the CTM at + * the time the soft mask is set (not at the time the soft mask is used for fill/stroke!), + * for forms, this is the CTM at the time the form is invoked. + * @param backdropColor the color according to the /bc entry to be used for luminosity soft + * masks. + * @throws IOException + */ + private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix ctm, + PDColor backdropColor) throws IOException + { + Graphics2D savedGraphics = graphics; + List savedLastClips = lastClips; + Shape savedInitialClip = initialClip; + + // get the CTM x Form Matrix transform + Matrix transform = Matrix.concatenate(ctm, form.getMatrix()); + + // transform the bbox + PDRectangle formBBox = form.getBBox(); + if (formBBox == null) + { + // PDFBOX-5471 + // check done here and not in caller to avoid getBBox() creating rectangle twice + LOG.warn("transparency group ignored because BBox is null"); + formBBox = new PDRectangle(); + } + GeneralPath transformedBox = formBBox.transform(transform); + + // clip the bbox to prevent giant bboxes from consuming all memory + Area transformed = new Area(transformedBox); + transformed.intersect(getGraphicsState().getCurrentClippingPath()); + Rectangle2D clipRect = transformed.getBounds2D(); + if (clipRect.isEmpty()) + { + image = null; + bbox = null; + minX = 0; + minY = 0; + maxX = 0; + maxY = 0; + width = 0; + height = 0; + return; + } + this.bbox = new PDRectangle((float)clipRect.getX(), (float)clipRect.getY(), + (float)clipRect.getWidth(), (float)clipRect.getHeight()); + + // apply the underlying Graphics2D device's DPI transform + AffineTransform xformOriginal = xform; + xform = AffineTransform.getScaleInstance(xformScalingFactorX, xformScalingFactorY); + Rectangle2D bounds = xform.createTransformedShape(clipRect).getBounds2D(); + + minX = (int) Math.floor(bounds.getMinX()); + minY = (int) Math.floor(bounds.getMinY()); + maxX = (int) Math.floor(bounds.getMaxX()) + 1; + maxY = (int) Math.floor(bounds.getMaxY()) + 1; + + width = maxX - minX; + height = maxY - minY; + + // FIXME - color space + if (isGray(form.getGroup().getColorSpace(form.getResources()))) + { + image = create2ByteGrayAlphaImage(width, height); + } + else + { + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + boolean needsBackdrop = !isSoftMask && !form.getGroup().isIsolated() && + hasBlendMode(form, new HashSet<>()); + BufferedImage backdropImage = null; + // Position of this group in parent group's coordinates + int backdropX = 0; + int backdropY = 0; + if (needsBackdrop) + { + if (transparencyGroupStack.isEmpty()) + { + // Use the current page as the parent group. + backdropImage = renderer.getPageImage(); + if (backdropImage == null) + { + needsBackdrop = false; + } + else + { + backdropX = minX; + backdropY = backdropImage.getHeight() - maxY; + } + } + else + { + TransparencyGroup parentGroup = transparencyGroupStack.peek(); + backdropImage = parentGroup.image; + backdropX = minX - parentGroup.minX; + backdropY = parentGroup.maxY - maxY; + } + } + + Graphics2D g = image.createGraphics(); + if (needsBackdrop) + { + // backdropImage must be included in group image but not in group alpha. + g.drawImage(backdropImage, 0, 0, width, height, + backdropX, backdropY, backdropX + width, backdropY + height, null); + g = new GroupGraphics(image, g); + } + if (isSoftMask && backdropColor != null) + { + // "If the subtype is Luminosity, the transparency group XObject G shall be + // composited with a fully opaque backdrop whose colour is everywhere defined + // by the soft-mask dictionary's BC entry." + g.setBackground(new Color(backdropColor.toRGB())); + g.clearRect(0, 0, width, height); + } + + // flip y-axis + g.translate(0, image.getHeight()); + g.scale(1, -1); + + boolean savedFlipTG = flipTG; + flipTG = false; + + // apply device transform (DPI) + // the initial translation is ignored, because we're not writing into the initial graphics device + g.transform(xform); + + PDRectangle pageSizeOriginal = pageSize; + pageSize = new PDRectangle(minX / xformScalingFactorX, + minY / xformScalingFactorY, + (float) (bounds.getWidth() / xformScalingFactorX), + (float) (bounds.getHeight() / xformScalingFactorY)); + int clipWindingRuleOriginal = clipWindingRule; + clipWindingRule = -1; + GeneralPath linePathOriginal = linePath; + linePath = new GeneralPath(); + + // adjust the origin + g.translate(-clipRect.getX(), -clipRect.getY()); + + graphics = g; + setRenderingHints(); + try + { + if (isSoftMask) + { + processSoftMask(form); + } + else + { + transparencyGroupStack.push(this); + processTransparencyGroup(form); + if (!transparencyGroupStack.isEmpty()) + { + transparencyGroupStack.pop(); + } + } + + if (needsBackdrop) + { + ((GroupGraphics) graphics).removeBackdrop(backdropImage, backdropX, backdropY); + } + } + finally + { + flipTG = savedFlipTG; + lastClips = savedLastClips; + graphics.dispose(); + graphics = savedGraphics; + initialClip = savedInitialClip; + clipWindingRule = clipWindingRuleOriginal; + linePath = linePathOriginal; + pageSize = pageSizeOriginal; + xform = xformOriginal; + } + } + + // http://stackoverflow.com/a/21181943/535646 + private BufferedImage create2ByteGrayAlphaImage(int width, int height) + { + // gray + alpha + int[] bandOffsets = {1, 0}; + int bands = bandOffsets.length; + + // Color Model used for raw GRAY + ALPHA + final ColorModel CM_GRAY_ALPHA + = new ComponentColorModel( + ColorSpace.getInstance(ColorSpace.CS_GRAY), + true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); + + // Init data buffer of type byte + DataBuffer buffer = new DataBufferByte(width * height * bands); + + // Wrap the data buffer in a raster + WritableRaster raster = + Raster.createInterleavedRaster(buffer, width, height, + width * bands, bands, bandOffsets, new Point(0, 0)); + + // Create a custom BufferedImage with the raster and a suitable color model + return new BufferedImage(CM_GRAY_ALPHA, raster, false, null); + } + + private boolean isGray(PDColorSpace colorSpace) + { + if (colorSpace instanceof PDDeviceGray) + { + return true; + } + if (colorSpace instanceof PDICCBased) + { + try + { + return ((PDICCBased) colorSpace).getAlternateColorSpace() instanceof PDDeviceGray; + } + catch (IOException ex) + { + LOG.debug("Couldn't get an alternate ColorSpace", ex); + return false; + } + } + return false; + } + + BufferedImage getImage() + { + return image; + } + + PDRectangle getBBox() + { + return bbox; + } + + Rectangle2D getBounds() + { + Rectangle2D r; + if (flipTG) + { + // Fixes PDFBOX-5966 and PDFBOX-5251, but not pdfium 1317, which has similar PDF code. + // https://bugs.chromium.org/p/pdfium/issues/detail?id=1317 + r = new Rectangle2D.Double(minX, minY, width, height); + } + else + { + // y-axis flip + r = new Rectangle2D.Double( + minX - pageSize.getLowerLeftX() * xformScalingFactorX, + (pageSize.getLowerLeftY() + pageSize.getHeight()) * xformScalingFactorY - minY - height, + width, + height); + } + // apply the underlying Graphics2D device's DPI transform + // this adjusts the rectangle to the rotated image to put the soft mask at the correct position + //TODO + // 1. change transparencyGroup.getBounds() to getOrigin(), because size isn't used in SoftMask, + // 2. Is it possible to create the softmask and transparency group in the correct rotation? + // (needs rendering identity testing before committing!) + AffineTransform adjustedTransform = new AffineTransform(xform); + adjustedTransform.scale(1.0 / xformScalingFactorX, 1.0 / xformScalingFactorY); + return adjustedTransform.createTransformedShape(r).getBounds2D(); + } + } + + private boolean hasBlendMode(PDTransparencyGroup group, Set groupsDone) + { + COSStream cosObject = group.getCOSObject(); + if (groupsDone.contains(cosObject)) + { + // The group is being processed. Avoid endless recursion. + return false; + } + groupsDone.add(cosObject); + + Boolean val = blendModeMap.get(cosObject); + if (val != null) + { + return val; + } + + PDResources resources = group.getResources(); + if (resources == null) + { + blendModeMap.put(cosObject, false); + return false; + } + for (COSName name : resources.getExtGStateNames()) + { + PDExtendedGraphicsState extGState = resources.getExtGState(name); + if (extGState == null) + { + continue; + } + BlendMode blendMode = extGState.getBlendMode(); + if (blendMode != BlendMode.NORMAL) + { + blendModeMap.put(cosObject, true); + return true; + } + } + + // Recursively process nested transparency groups + for (COSName name : resources.getXObjectNames()) + { + PDXObject xObject; + try + { + xObject = resources.getXObject(name); + } + catch (IOException ex) + { + continue; + } + if (xObject instanceof PDTransparencyGroup && + hasBlendMode((PDTransparencyGroup)xObject, groupsDone)) + { + blendModeMap.put(cosObject, true); + return true; + } + } + + blendModeMap.put(cosObject, false); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void beginMarkedContentSequence(COSName tag, COSDictionary properties) + { + if (nestedHiddenOCGCount > 0) + { + nestedHiddenOCGCount++; + return; + } + if (properties == null) + { + return; + } + if (isHiddenOCG(PDPropertyList.create(properties))) + { + nestedHiddenOCGCount = 1; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void endMarkedContentSequence() + { + if (nestedHiddenOCGCount > 0) + { + nestedHiddenOCGCount--; + } + } + + private boolean isContentRendered() + { + return nestedHiddenOCGCount <= 0; + } + + private boolean isHiddenOCG(PDPropertyList propertyList) + { + if (propertyList instanceof PDOptionalContentGroup) + { + PDOptionalContentGroup group = (PDOptionalContentGroup) propertyList; + RenderState printState = group.getRenderState(destination); + if (printState == null) + { + if (!getRenderer().isGroupEnabled(group)) + { + return true; + } + } + else if (RenderState.OFF == printState) + { + return true; + } + } + else if (propertyList instanceof PDOptionalContentMembershipDictionary) + { + return isHiddenOCMD((PDOptionalContentMembershipDictionary) propertyList); + } + return false; + } + + private boolean isHiddenOCMD(PDOptionalContentMembershipDictionary ocmd) + { + COSArray veArray = ocmd.getCOSObject().getCOSArray(COSName.VE); + if (veArray != null && !veArray.isEmpty()) + { + return isHiddenVisibilityExpression(veArray); + } + List oCGs = ocmd.getOCGs(); + if (oCGs.isEmpty()) + { + return false; + } + List visibles = new ArrayList<>(); + oCGs.forEach(prop -> visibles.add(!isHiddenOCG(prop))); + COSName visibilityPolicy = ocmd.getVisibilityPolicy(); + + // visible if any of the entries in OCGs are OFF + if (COSName.ANY_OFF.equals(visibilityPolicy)) + { + return visibles.stream().allMatch(v -> v); + } + + // visible only if all of the entries in OCGs are ON + if (COSName.ALL_ON.equals(visibilityPolicy)) + { + return visibles.stream().anyMatch(v -> !v); + } + + // visible only if all of the entries in OCGs are OFF + if (COSName.ALL_OFF.equals(visibilityPolicy)) + { + return visibles.stream().anyMatch(v -> v); + } + + // visible if any of the entries in OCGs are ON + // AnyOn is default + return visibles.stream().noneMatch(v -> v); + } + + private boolean isHiddenVisibilityExpression(COSArray veArray) + { + if (veArray.isEmpty()) + { + return false; + } + String op = veArray.getName(0); + if (op == null) + { + return false; + } + switch (op) + { + case "And": + return isHiddenAndVisibilityExpression(veArray); + case "Or": + return isHiddenOrVisibilityExpression(veArray); + case "Not": + return isHiddenNotVisibilityExpression(veArray); + default: + return false; + } + } + + private boolean isHiddenAndVisibilityExpression(COSArray veArray) + { + // hidden if at least one isn't visible + for (int i = 1; i < veArray.size(); ++i) + { + COSBase base = veArray.getObject(i); + if (base instanceof COSArray) + { + // Another VE + boolean isHidden = isHiddenVisibilityExpression((COSArray) base); + if (isHidden) + { + return true; + } + } + else if (base instanceof COSDictionary) + { + // Another OCG + PDPropertyList prop = PDPropertyList.create((COSDictionary) base); + boolean isHidden = isHiddenOCG(prop); + if (isHidden) + { + return true; + } + } + } + return false; + } + + private boolean isHiddenOrVisibilityExpression(COSArray veArray) + { + // hidden only if all are hidden + for (int i = 1; i < veArray.size(); ++i) + { + COSBase base = veArray.getObject(i); + if (base instanceof COSArray) + { + // Another VE + boolean isHidden = isHiddenVisibilityExpression((COSArray) base); + if (!isHidden) + { + return false; + } + } + else if (base instanceof COSDictionary) + { + // Another OCG + PDPropertyList prop = PDPropertyList.create((COSDictionary) base); + boolean isHidden = isHiddenOCG(prop); + if (!isHidden) + { + return false; + } + } + } + return true; + } + + private boolean isHiddenNotVisibilityExpression(COSArray veArray) + { + if (veArray.size() != 2) + { + return false; + } + COSBase base = veArray.getObject(1); + if (base instanceof COSArray) + { + // Another VE + return !isHiddenVisibilityExpression((COSArray) base); + } + else if (base instanceof COSDictionary) + { + // Another OCG + PDPropertyList prop = PDPropertyList.create((COSDictionary) base); + return !isHiddenOCG(prop); + } + return false; + } + + private LookupTable getInvLookupTable() + { + if (invTable == null) + { + byte[] inv = new byte[256]; + for (int i = 0; i < inv.length; i++) + { + inv[i] = (byte) (255 - i); + } + invTable = new ByteLookupTable(0, inv); + } + return invTable; + } +}