You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ti...@apache.org on 2015/02/17 20:27:51 UTC
svn commit: r1660471 -
/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java
Author: tilman
Date: Tue Feb 17 19:27:51 2015
New Revision: 1660471
URL: http://svn.apache.org/r1660471
Log:
PDFBOX-1871: use more accurate y translation, thanks John Hewson
Modified:
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java?rev=1660471&r1=1660470&r2=1660471&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java Tue Feb 17 19:27:51 2015
@@ -1,935 +1,935 @@
-/*
- * 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.GraphicsDevice;
-import java.awt.Paint;
-import java.awt.RenderingHints;
-import java.awt.Shape;
-import java.awt.TexturePaint;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Area;
-import java.awt.geom.GeneralPath;
-import java.awt.geom.PathIterator;
-import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
-import java.awt.image.Raster;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.pdfbox.cos.COSName;
-import org.apache.pdfbox.pdmodel.font.PDCIDFontType0;
-import org.apache.pdfbox.pdmodel.font.PDCIDFontType2;
-import org.apache.pdfbox.pdmodel.graphics.color.PDPattern;
-import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
-import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern;
-import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern;
-import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
-import org.apache.pdfbox.pdmodel.PDPage;
-import org.apache.pdfbox.pdmodel.common.PDRectangle;
-import org.apache.pdfbox.pdmodel.font.PDFont;
-import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
-import org.apache.pdfbox.pdmodel.font.PDType0Font;
-import org.apache.pdfbox.pdmodel.font.PDType1CFont;
-import org.apache.pdfbox.pdmodel.font.PDType1Font;
-import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern;
-import org.apache.pdfbox.pdmodel.graphics.state.PDSoftMask;
-import org.apache.pdfbox.pdmodel.graphics.blend.SoftMaskPaint;
-import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
-import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
-import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
-import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
-import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
-import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
-import org.apache.pdfbox.util.Matrix;
-import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
-import org.apache.pdfbox.util.Vector;
-
-/**
- * Paints a page in a PDF document to a Graphics context.
- *
- * @author Ben Litchfield
- */
-public final class PageDrawer extends PDFGraphicsStreamEngine
-{
- private static final Log LOG = LogFactory.getLog(PageDrawer.class);
-
- // parent document renderer
- private final PDFRenderer renderer;
-
- // the graphics device to draw to, xform is the initial transform of the device (i.e. DPI)
- private Graphics2D graphics;
- private AffineTransform xform;
-
- // the page box to draw (usually the crop box but may be another)
- PDRectangle pageSize;
-
- // clipping winding rule used for the clipping path
- private int clipWindingRule = -1;
- private GeneralPath linePath = new GeneralPath();
-
- // last clipping path
- private Area lastClip;
-
- // buffered clipping area for text being drawn
- private Area textClippingArea;
-
- private final Map<PDFont, Glyph2D> fontGlyph2D = new HashMap<PDFont, Glyph2D>();
-
- /**
- * Constructor.
- *
- * @param renderer renderer to render the page.
- * @param page the page that is to be rendered.
- * @throws IOException If there is an error loading properties from the file.
- */
- public PageDrawer(PDFRenderer renderer, PDPage page) throws IOException
- {
- super(page);
- this.renderer = renderer;
- }
-
- /**
- * Returns the parent renderer.
- */
- public PDFRenderer getRenderer()
- {
- return renderer;
- }
-
- /**
- * Sets high-quality rendering hints on the current Graphics2D.
- */
- private void setRenderingHints()
- {
- graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
- RenderingHints.VALUE_INTERPOLATION_BICUBIC);
- graphics.setRenderingHint(RenderingHints.KEY_RENDERING,
- RenderingHints.VALUE_RENDER_QUALITY);
- graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
- RenderingHints.VALUE_ANTIALIAS_ON);
- }
-
- /**
- * 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(Graphics g, PDRectangle pageSize) throws IOException
- {
- graphics = (Graphics2D) g;
- xform = graphics.getTransform();
- this.pageSize = pageSize;
-
- setRenderingHints();
-
- graphics.translate(0, (int) pageSize.getHeight());
- graphics.scale(1, -1);
-
- // TODO use getStroke() to set the initial stroke
- graphics.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
-
- // adjust for non-(0,0) crop box
- graphics.translate(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY());
-
- processPage(getPage());
-
- for (PDAnnotation annotation : getPage().getAnnotations())
- {
- 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.
- */
- public void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace colorSpace,
- PDColor color, Matrix patternMatrix) throws IOException
- {
- Graphics2D oldGraphics = graphics;
- graphics = g;
-
- GeneralPath oldLinePath = linePath;
- linePath = new GeneralPath();
-
- Area oldLastClip = lastClip;
- lastClip = null;
-
- setRenderingHints();
- processTilingPattern(pattern, color, colorSpace, patternMatrix);
-
- graphics = oldGraphics;
- linePath = oldLinePath;
- lastClip = oldLastClip;
- }
-
- /**
- * Returns an AWT paint for the given PDColor.
- */
- private Paint getPaint(PDColor color) throws IOException
- {
- PDColorSpace colorSpace = color.getColorSpace();
- if (!(colorSpace instanceof PDPattern))
- {
- float[] rgb = colorSpace.toRGB(color.getComponents());
- return new Color(rgb[0], rgb[1], 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 new TilingPaint(this, tilingPattern, xform);
- }
- else
- {
- // uncolored tiling pattern
- return new TilingPaint(this, 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
- // Graphics2D#getClip() returns a new object instead of the same one passed to setClip
- private void setClip()
- {
- Area clippingPath = getGraphicsState().getCurrentClippingPath();
- if (clippingPath != lastClip)
- {
- graphics.setClip(clippingPath);
- lastClip = clippingPath;
- }
- }
-
- @Override
- public void beginText() throws IOException
- {
- setClip();
- }
-
- @Override
- protected void showText(byte[] string) throws IOException
- {
- PDGraphicsState state = getGraphicsState();
- RenderingMode renderingMode = state.getTextState().getRenderingMode();
-
- // buffer the text clip because it represents a single clipping area
- if (renderingMode.isClip())
- {
- textClippingArea = new Area();
- }
-
- super.showText(string);
-
- // apply the buffered clip as one area
- if (renderingMode.isClip())
- {
- state.intersectClippingPath(textClippingArea);
- textClippingArea = null;
- }
- }
-
- @Override
- protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode,
- Vector displacement) throws IOException
- {
- AffineTransform at = textRenderingMatrix.createAffineTransform();
- at.concatenate(font.getFontMatrix().createAffineTransform());
-
- Glyph2D glyph2D = createGlyph2D(font);
- drawGlyph2D(glyph2D, font, code, displacement, at);
- }
-
- /**
- * Render the font using the Glyph2D interface.
- *
- * @param glyph2D the Glyph2D implementation provided a GeneralPath for each 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 drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displacement,
- AffineTransform at) throws IOException
- {
- PDGraphicsState state = getGraphicsState();
- RenderingMode renderingMode = state.getTextState().getRenderingMode();
-
- GeneralPath path = glyph2D.getPathForCharacterCode(code);
- if (path != null)
- {
- // stretch non-embedded glyph if it does not match the width contained in the PDF
- if (!font.isEmbedded())
- {
- float fontWidth = font.getWidthFromFont(code);
- if (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 (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())
- {
- textClippingArea.add(new Area(glyph));
- }
- }
- }
-
- /**
- * Provide a Glyph2D for the given font.
- *
- * @param font the font
- * @return the implementation of the Glyph2D interface for the given font
- * @throws IOException if something went wrong
- */
- private Glyph2D createGlyph2D(PDFont font) throws IOException
- {
- // Is there already a Glyph2D for the given font?
- if (fontGlyph2D.containsKey(font))
- {
- return fontGlyph2D.get(font);
- }
-
- Glyph2D glyph2D = null;
- if (font instanceof PDTrueTypeFont)
- {
- PDTrueTypeFont ttfFont = (PDTrueTypeFont)font;
- glyph2D = new TTFGlyph2D(ttfFont); // TTF is never null
- }
- else if (font instanceof PDType1Font)
- {
- PDType1Font pdType1Font = (PDType1Font)font;
- glyph2D = new Type1Glyph2D(pdType1Font); // T1 is never null
- }
- else if (font instanceof PDType1CFont)
- {
- PDType1CFont type1CFont = (PDType1CFont)font;
- glyph2D = new Type1Glyph2D(type1CFont);
- }
- else if (font instanceof PDType0Font)
- {
- PDType0Font type0Font = (PDType0Font) font;
- if (type0Font.getDescendantFont() instanceof PDCIDFontType2)
- {
- glyph2D = new TTFGlyph2D(type0Font); // TTF is never null
- }
- else if (type0Font.getDescendantFont() instanceof PDCIDFontType0)
- {
- // a Type0 CIDFont contains CFF font
- PDCIDFontType0 cidType0Font = (PDCIDFontType0)type0Font.getDescendantFont();
- glyph2D = new CIDType0Glyph2D(cidType0Font); // todo: could be null (need incorporate fallback)
- }
- }
- else
- {
- throw new IllegalStateException("Bad font type: " + font.getClass().getSimpleName());
- }
-
- // cache the Glyph2D instance
- if (glyph2D != null)
- {
- fontGlyph2D.put(font, glyph2D);
- }
-
- if (glyph2D == null)
- {
- // todo: make sure this never happens
- throw new UnsupportedOperationException("No font for " + font.getName());
- }
-
- return glyph2D;
- }
-
- @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();
- }
-
- /**
- * Generates AWT raster for a soft mask
- *
- * @param softMask soft mask
- * @return AWT raster for soft mask
- * @throws IOException
- */
- private Raster createSoftMaskRaster(PDSoftMask softMask) throws IOException
- {
- TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true);
- COSName subtype = softMask.getSubType();
- if (COSName.ALPHA.equals(subtype))
- {
- return transparencyGroup.getAlphaRaster();
- }
- else if (COSName.LUMINOSITY.equals(subtype))
- {
- return transparencyGroup.getLuminosityRaster();
- }
- else
- {
- throw new IOException("Invalid soft mask subtype.");
- }
- }
-
- private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException
- {
- if (softMask != null)
- {
- return new SoftMaskPaint(parentPaint, createSoftMaskRaster(softMask));
- }
- else
- {
- return parentPaint;
- }
- }
-
- // returns the stroking AWT Paint
- private Paint getStrokingPaint() throws IOException
- {
- return applySoftMaskToPaint(
- getPaint(getGraphicsState().getStrokingColor()),
- getGraphicsState().getSoftMask());
- }
-
- // returns the non-stroking AWT Paint
- private Paint getNonStrokingPaint() throws IOException
- {
- return getPaint(getGraphicsState().getNonStrokingColor());
- }
-
- // create a new stroke based on the current CTM and the current stroke
- private BasicStroke 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();
- int phaseStart = dashPattern.getPhase();
- float[] dashArray = dashPattern.getDashArray();
- if (dashArray != null)
- {
- // apply the CTM
- for (int i = 0; i < dashArray.length; ++i)
- {
- // minimum line dash width avoids JVM crash, see PDFBOX-2373
- dashArray[i] = Math.max(transformWidth(dashArray[i]), 0.016f);
- }
- phaseStart = (int)transformWidth(phaseStart);
-
- // empty dash array is illegal
- if (dashArray.length == 0)
- {
- dashArray = null;
- }
- }
- return new BasicStroke(lineWidth, state.getLineCap(), state.getLineJoin(),
- state.getMiterLimit(), dashArray, phaseStart);
- }
-
- @Override
- public void strokePath() throws IOException
- {
- graphics.setComposite(getGraphicsState().getStrokingJavaComposite());
- graphics.setPaint(getStrokingPaint());
- graphics.setStroke(getStroke());
- setClip();
- graphics.draw(linePath);
- linePath.reset();
- }
-
- @Override
- public void fillPath(int windingRule) throws IOException
- {
- graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
- graphics.setPaint(getNonStrokingPaint());
- 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
- boolean isRectangular = isRectangular(linePath);
- if (isRectangular)
- {
- graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
- RenderingHints.VALUE_ANTIALIAS_OFF);
- }
-
- graphics.fill(linePath);
- linePath.reset();
-
- if (isRectangular)
- {
- // 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();
- }
- }
-
- /**
- * 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;
-
- case PathIterator.SEG_CLOSE:
- 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
- {
- // TODO can we avoid cloning the path?
- 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;
- }
-
- @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.Float getCurrentPoint()
- {
- Point2D current = linePath.getCurrentPoint();
- return new Point2D.Float((float)current.getX(), (float)current.getY());
- }
-
- @Override
- public void closePath()
- {
- linePath.closePath();
- }
-
- @Override
- public void endPath()
- {
- if (clipWindingRule != -1)
- {
- linePath.setWindingRule(clipWindingRule);
- getGraphicsState().intersectClippingPath(linePath);
- clipWindingRule = -1;
- }
- linePath.reset();
- }
-
- @Override
- public void drawImage(PDImage pdImage) throws IOException
- {
- Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
- AffineTransform at = ctm.createAffineTransform();
-
- if (!pdImage.getInterpolate())
- {
- boolean isScaledUp = pdImage.getWidth() < Math.round(at.getScaleX()) ||
- pdImage.getHeight() < Math.round(at.getScaleY());
-
- // 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
- // stencils are excluded from this rule (see survey.pdf)
- if (isScaledUp || pdImage.isStencil())
- {
- graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
- RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
- }
- }
-
- if (pdImage.isStencil())
- {
- // fill the image with paint
- PDColor color = getGraphicsState().getNonStrokingColor();
- BufferedImage image = pdImage.getStencilImage(getPaint(color));
-
- // draw the image
- drawBufferedImage(image, at);
- }
- else
- {
- // draw the image
- drawBufferedImage(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();
- }
- }
-
- public void drawBufferedImage(BufferedImage image, AffineTransform at) throws IOException
- {
- graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
- setClip();
- PDSoftMask softMask = getGraphicsState().getSoftMask();
- if( softMask != null )
- {
- AffineTransform imageTransform = new AffineTransform(at);
- imageTransform.scale(1, -1);
- imageTransform.translate(0, -1);
- Paint awtPaint = new TexturePaint(image,
- new Rectangle2D.Double(imageTransform.getTranslateX(), imageTransform.getTranslateY(),
- imageTransform.getScaleX(), imageTransform.getScaleY()));
- awtPaint = applySoftMaskToPaint(awtPaint, softMask);
- graphics.setPaint(awtPaint);
- Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1);
- graphics.fill(at.createTransformedShape(unitRect));
- }
- else
- {
- int width = image.getWidth(null);
- int height = image.getHeight(null);
- AffineTransform imageTransform = new AffineTransform(at);
- imageTransform.scale(1.0 / width, -1.0 / height);
- imageTransform.translate(0, -height);
- graphics.drawImage(image, imageTransform, null);
- }
- }
-
- @Override
- public void shadingFill(COSName shadingName) throws IOException
- {
- PDShading shading = getResources().getShading(shadingName);
- Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
- Paint paint = shading.toPaint(ctm);
-
- graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
- graphics.setPaint(paint);
- graphics.setClip(null);
- lastClip = null;
- graphics.fill(getGraphicsState().getCurrentClippingPath());
- }
-
- @Override
- public void showAnnotation(PDAnnotation annotation) throws IOException
- {
- lastClip = null;
- //TODO support more annotation flags (Invisible, NoZoom, NoRotate)
- int deviceType = graphics.getDeviceConfiguration().getDevice().getType();
- if (deviceType == GraphicsDevice.TYPE_PRINTER && !annotation.isPrinted())
- {
- return;
- }
- if (deviceType == GraphicsDevice.TYPE_RASTER_SCREEN && annotation.isNoView())
- {
- return;
- }
- if (annotation.isHidden())
- {
- return;
- }
- super.showAnnotation(annotation);
- }
-
- @Override
- public void showTransparencyGroup(PDFormXObject form) throws IOException
- {
- TransparencyGroup group = new TransparencyGroup(form, false);
-
- 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
- PDRectangle bbox = group.getBBox();
- AffineTransform prev = graphics.getTransform();
- float x = bbox.getLowerLeftX();
- float y = pageSize.getHeight() - bbox.getLowerLeftY() - bbox.getHeight();
- graphics.setTransform(AffineTransform.getTranslateInstance(x * xform.getScaleX(),
- y * xform.getScaleY()));
-
- PDSoftMask softMask = getGraphicsState().getSoftMask();
- if (softMask != null)
- {
- BufferedImage image = group.getImage();
- Paint awtPaint = new TexturePaint(image,
- new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight()));
- awtPaint = applySoftMaskToPaint(awtPaint, softMask); // todo: PDFBOX-994 problem here?
- graphics.setPaint(awtPaint);
- graphics.fill(new Rectangle2D.Float(0, 0, bbox.getWidth() * (float)xform.getScaleX(),
- bbox.getHeight() * (float)xform.getScaleY()));
- }
- else
- {
- graphics.drawImage(group.getImage(), null, null);
- }
-
- graphics.setTransform(prev);
- }
-
- /**
- * 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 width;
- private final int height;
-
- /**
- * Creates a buffered image for a transparency group result.
- */
- private TransparencyGroup(PDFormXObject form, boolean isSoftMask) throws IOException
- {
- Graphics2D g2dOriginal = graphics;
- Area lastClipOriginal = lastClip;
-
- // get the CTM x Form Matrix transform
- Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
- Matrix transform = Matrix.concatenate(ctm, form.getMatrix());
-
- // transform the bbox
- GeneralPath transformedBox = form.getBBox().transform(transform);
-
- // clip the bbox to prevent giant bboxes from consuming all memory
- Area clip = (Area)getGraphicsState().getCurrentClippingPath().clone();
- clip.intersect(new Area(transformedBox));
- Rectangle2D clipRect = clip.getBounds2D();
- this.bbox = new PDRectangle((float)clipRect.getX(), (float)clipRect.getY(),
- (float)clipRect.getWidth(), (float)clipRect.getHeight());
-
- // apply the underlying Graphics2D device's DPI transform
- Shape deviceClip = xform.createTransformedShape(clip);
- Rectangle2D bounds = deviceClip.getBounds2D();
-
- minX = (int) Math.floor(bounds.getMinX());
- minY = (int) Math.floor(bounds.getMinY());
- int maxX = (int) Math.floor(bounds.getMaxX()) + 1;
- int maxY = (int) Math.floor(bounds.getMaxY()) + 1;
-
- width = maxX - minX;
- height = maxY - minY;
-
- image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // FIXME - color space
- Graphics2D g = image.createGraphics();
-
- // flip y-axis
- g.translate(0, height);
- g.scale(1, -1);
-
- // apply device transform (DPI)
- g.transform(xform);
-
- // adjust the origin
- g.translate(-clipRect.getX(), -clipRect.getY());
-
- graphics = g;
- try
- {
- if (isSoftMask)
- {
- processSoftMask(form);
- }
- else
- {
- processTransparencyGroup(form);
- }
- }
- finally
- {
- lastClip = lastClipOriginal;
- graphics.dispose();
- graphics = g2dOriginal;
- }
- }
-
- public BufferedImage getImage()
- {
- return image;
- }
-
- public PDRectangle getBBox()
- {
- return bbox;
- }
-
- public Raster getAlphaRaster()
- {
- return image.getAlphaRaster();
- }
-
- public Raster getLuminosityRaster()
- {
- BufferedImage gray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
- Graphics g = gray.getGraphics();
- g.drawImage(image, 0, 0, null);
- g.dispose();
-
- return gray.getRaster();
- }
- }
-}
+/*
+ * 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.GraphicsDevice;
+import java.awt.Paint;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.TexturePaint;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Area;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.Raster;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.font.PDCIDFontType0;
+import org.apache.pdfbox.pdmodel.font.PDCIDFontType2;
+import org.apache.pdfbox.pdmodel.graphics.color.PDPattern;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
+import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern;
+import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern;
+import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
+import org.apache.pdfbox.pdmodel.font.PDType0Font;
+import org.apache.pdfbox.pdmodel.font.PDType1CFont;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern;
+import org.apache.pdfbox.pdmodel.graphics.state.PDSoftMask;
+import org.apache.pdfbox.pdmodel.graphics.blend.SoftMaskPaint;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
+import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
+import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
+import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
+import org.apache.pdfbox.util.Matrix;
+import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
+import org.apache.pdfbox.util.Vector;
+
+/**
+ * Paints a page in a PDF document to a Graphics context.
+ *
+ * @author Ben Litchfield
+ */
+public final class PageDrawer extends PDFGraphicsStreamEngine
+{
+ private static final Log LOG = LogFactory.getLog(PageDrawer.class);
+
+ // parent document renderer
+ private final PDFRenderer renderer;
+
+ // the graphics device to draw to, xform is the initial transform of the device (i.e. DPI)
+ private Graphics2D graphics;
+ private AffineTransform xform;
+
+ // the page box to draw (usually the crop box but may be another)
+ PDRectangle pageSize;
+
+ // clipping winding rule used for the clipping path
+ private int clipWindingRule = -1;
+ private GeneralPath linePath = new GeneralPath();
+
+ // last clipping path
+ private Area lastClip;
+
+ // buffered clipping area for text being drawn
+ private Area textClippingArea;
+
+ private final Map<PDFont, Glyph2D> fontGlyph2D = new HashMap<PDFont, Glyph2D>();
+
+ /**
+ * Constructor.
+ *
+ * @param renderer renderer to render the page.
+ * @param page the page that is to be rendered.
+ * @throws IOException If there is an error loading properties from the file.
+ */
+ public PageDrawer(PDFRenderer renderer, PDPage page) throws IOException
+ {
+ super(page);
+ this.renderer = renderer;
+ }
+
+ /**
+ * Returns the parent renderer.
+ */
+ public PDFRenderer getRenderer()
+ {
+ return renderer;
+ }
+
+ /**
+ * Sets high-quality rendering hints on the current Graphics2D.
+ */
+ private void setRenderingHints()
+ {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+ graphics.setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ }
+
+ /**
+ * 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(Graphics g, PDRectangle pageSize) throws IOException
+ {
+ graphics = (Graphics2D) g;
+ xform = graphics.getTransform();
+ this.pageSize = pageSize;
+
+ setRenderingHints();
+
+ graphics.translate(0, pageSize.getHeight());
+ graphics.scale(1, -1);
+
+ // TODO use getStroke() to set the initial stroke
+ graphics.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
+
+ // adjust for non-(0,0) crop box
+ graphics.translate(-pageSize.getLowerLeftX(), -pageSize.getLowerLeftY());
+
+ processPage(getPage());
+
+ for (PDAnnotation annotation : getPage().getAnnotations())
+ {
+ 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.
+ */
+ public void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace colorSpace,
+ PDColor color, Matrix patternMatrix) throws IOException
+ {
+ Graphics2D oldGraphics = graphics;
+ graphics = g;
+
+ GeneralPath oldLinePath = linePath;
+ linePath = new GeneralPath();
+
+ Area oldLastClip = lastClip;
+ lastClip = null;
+
+ setRenderingHints();
+ processTilingPattern(pattern, color, colorSpace, patternMatrix);
+
+ graphics = oldGraphics;
+ linePath = oldLinePath;
+ lastClip = oldLastClip;
+ }
+
+ /**
+ * Returns an AWT paint for the given PDColor.
+ */
+ private Paint getPaint(PDColor color) throws IOException
+ {
+ PDColorSpace colorSpace = color.getColorSpace();
+ if (!(colorSpace instanceof PDPattern))
+ {
+ float[] rgb = colorSpace.toRGB(color.getComponents());
+ return new Color(rgb[0], rgb[1], 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 new TilingPaint(this, tilingPattern, xform);
+ }
+ else
+ {
+ // uncolored tiling pattern
+ return new TilingPaint(this, 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
+ // Graphics2D#getClip() returns a new object instead of the same one passed to setClip
+ private void setClip()
+ {
+ Area clippingPath = getGraphicsState().getCurrentClippingPath();
+ if (clippingPath != lastClip)
+ {
+ graphics.setClip(clippingPath);
+ lastClip = clippingPath;
+ }
+ }
+
+ @Override
+ public void beginText() throws IOException
+ {
+ setClip();
+ }
+
+ @Override
+ protected void showText(byte[] string) throws IOException
+ {
+ PDGraphicsState state = getGraphicsState();
+ RenderingMode renderingMode = state.getTextState().getRenderingMode();
+
+ // buffer the text clip because it represents a single clipping area
+ if (renderingMode.isClip())
+ {
+ textClippingArea = new Area();
+ }
+
+ super.showText(string);
+
+ // apply the buffered clip as one area
+ if (renderingMode.isClip())
+ {
+ state.intersectClippingPath(textClippingArea);
+ textClippingArea = null;
+ }
+ }
+
+ @Override
+ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code, String unicode,
+ Vector displacement) throws IOException
+ {
+ AffineTransform at = textRenderingMatrix.createAffineTransform();
+ at.concatenate(font.getFontMatrix().createAffineTransform());
+
+ Glyph2D glyph2D = createGlyph2D(font);
+ drawGlyph2D(glyph2D, font, code, displacement, at);
+ }
+
+ /**
+ * Render the font using the Glyph2D interface.
+ *
+ * @param glyph2D the Glyph2D implementation provided a GeneralPath for each 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 drawGlyph2D(Glyph2D glyph2D, PDFont font, int code, Vector displacement,
+ AffineTransform at) throws IOException
+ {
+ PDGraphicsState state = getGraphicsState();
+ RenderingMode renderingMode = state.getTextState().getRenderingMode();
+
+ GeneralPath path = glyph2D.getPathForCharacterCode(code);
+ if (path != null)
+ {
+ // stretch non-embedded glyph if it does not match the width contained in the PDF
+ if (!font.isEmbedded())
+ {
+ float fontWidth = font.getWidthFromFont(code);
+ if (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 (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())
+ {
+ textClippingArea.add(new Area(glyph));
+ }
+ }
+ }
+
+ /**
+ * Provide a Glyph2D for the given font.
+ *
+ * @param font the font
+ * @return the implementation of the Glyph2D interface for the given font
+ * @throws IOException if something went wrong
+ */
+ private Glyph2D createGlyph2D(PDFont font) throws IOException
+ {
+ // Is there already a Glyph2D for the given font?
+ if (fontGlyph2D.containsKey(font))
+ {
+ return fontGlyph2D.get(font);
+ }
+
+ Glyph2D glyph2D = null;
+ if (font instanceof PDTrueTypeFont)
+ {
+ PDTrueTypeFont ttfFont = (PDTrueTypeFont)font;
+ glyph2D = new TTFGlyph2D(ttfFont); // TTF is never null
+ }
+ else if (font instanceof PDType1Font)
+ {
+ PDType1Font pdType1Font = (PDType1Font)font;
+ glyph2D = new Type1Glyph2D(pdType1Font); // T1 is never null
+ }
+ else if (font instanceof PDType1CFont)
+ {
+ PDType1CFont type1CFont = (PDType1CFont)font;
+ glyph2D = new Type1Glyph2D(type1CFont);
+ }
+ else if (font instanceof PDType0Font)
+ {
+ PDType0Font type0Font = (PDType0Font) font;
+ if (type0Font.getDescendantFont() instanceof PDCIDFontType2)
+ {
+ glyph2D = new TTFGlyph2D(type0Font); // TTF is never null
+ }
+ else if (type0Font.getDescendantFont() instanceof PDCIDFontType0)
+ {
+ // a Type0 CIDFont contains CFF font
+ PDCIDFontType0 cidType0Font = (PDCIDFontType0)type0Font.getDescendantFont();
+ glyph2D = new CIDType0Glyph2D(cidType0Font); // todo: could be null (need incorporate fallback)
+ }
+ }
+ else
+ {
+ throw new IllegalStateException("Bad font type: " + font.getClass().getSimpleName());
+ }
+
+ // cache the Glyph2D instance
+ if (glyph2D != null)
+ {
+ fontGlyph2D.put(font, glyph2D);
+ }
+
+ if (glyph2D == null)
+ {
+ // todo: make sure this never happens
+ throw new UnsupportedOperationException("No font for " + font.getName());
+ }
+
+ return glyph2D;
+ }
+
+ @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();
+ }
+
+ /**
+ * Generates AWT raster for a soft mask
+ *
+ * @param softMask soft mask
+ * @return AWT raster for soft mask
+ * @throws IOException
+ */
+ private Raster createSoftMaskRaster(PDSoftMask softMask) throws IOException
+ {
+ TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true);
+ COSName subtype = softMask.getSubType();
+ if (COSName.ALPHA.equals(subtype))
+ {
+ return transparencyGroup.getAlphaRaster();
+ }
+ else if (COSName.LUMINOSITY.equals(subtype))
+ {
+ return transparencyGroup.getLuminosityRaster();
+ }
+ else
+ {
+ throw new IOException("Invalid soft mask subtype.");
+ }
+ }
+
+ private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException
+ {
+ if (softMask != null)
+ {
+ return new SoftMaskPaint(parentPaint, createSoftMaskRaster(softMask));
+ }
+ else
+ {
+ return parentPaint;
+ }
+ }
+
+ // returns the stroking AWT Paint
+ private Paint getStrokingPaint() throws IOException
+ {
+ return applySoftMaskToPaint(
+ getPaint(getGraphicsState().getStrokingColor()),
+ getGraphicsState().getSoftMask());
+ }
+
+ // returns the non-stroking AWT Paint
+ private Paint getNonStrokingPaint() throws IOException
+ {
+ return getPaint(getGraphicsState().getNonStrokingColor());
+ }
+
+ // create a new stroke based on the current CTM and the current stroke
+ private BasicStroke 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();
+ int phaseStart = dashPattern.getPhase();
+ float[] dashArray = dashPattern.getDashArray();
+ if (dashArray != null)
+ {
+ // apply the CTM
+ for (int i = 0; i < dashArray.length; ++i)
+ {
+ // minimum line dash width avoids JVM crash, see PDFBOX-2373
+ dashArray[i] = Math.max(transformWidth(dashArray[i]), 0.016f);
+ }
+ phaseStart = (int)transformWidth(phaseStart);
+
+ // empty dash array is illegal
+ if (dashArray.length == 0)
+ {
+ dashArray = null;
+ }
+ }
+ return new BasicStroke(lineWidth, state.getLineCap(), state.getLineJoin(),
+ state.getMiterLimit(), dashArray, phaseStart);
+ }
+
+ @Override
+ public void strokePath() throws IOException
+ {
+ graphics.setComposite(getGraphicsState().getStrokingJavaComposite());
+ graphics.setPaint(getStrokingPaint());
+ graphics.setStroke(getStroke());
+ setClip();
+ graphics.draw(linePath);
+ linePath.reset();
+ }
+
+ @Override
+ public void fillPath(int windingRule) throws IOException
+ {
+ graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
+ graphics.setPaint(getNonStrokingPaint());
+ 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
+ boolean isRectangular = isRectangular(linePath);
+ if (isRectangular)
+ {
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_OFF);
+ }
+
+ graphics.fill(linePath);
+ linePath.reset();
+
+ if (isRectangular)
+ {
+ // 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();
+ }
+ }
+
+ /**
+ * 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;
+
+ case PathIterator.SEG_CLOSE:
+ 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
+ {
+ // TODO can we avoid cloning the path?
+ 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;
+ }
+
+ @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.Float getCurrentPoint()
+ {
+ Point2D current = linePath.getCurrentPoint();
+ return new Point2D.Float((float)current.getX(), (float)current.getY());
+ }
+
+ @Override
+ public void closePath()
+ {
+ linePath.closePath();
+ }
+
+ @Override
+ public void endPath()
+ {
+ if (clipWindingRule != -1)
+ {
+ linePath.setWindingRule(clipWindingRule);
+ getGraphicsState().intersectClippingPath(linePath);
+ clipWindingRule = -1;
+ }
+ linePath.reset();
+ }
+
+ @Override
+ public void drawImage(PDImage pdImage) throws IOException
+ {
+ Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
+ AffineTransform at = ctm.createAffineTransform();
+
+ if (!pdImage.getInterpolate())
+ {
+ boolean isScaledUp = pdImage.getWidth() < Math.round(at.getScaleX()) ||
+ pdImage.getHeight() < Math.round(at.getScaleY());
+
+ // 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
+ // stencils are excluded from this rule (see survey.pdf)
+ if (isScaledUp || pdImage.isStencil())
+ {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
+ }
+ }
+
+ if (pdImage.isStencil())
+ {
+ // fill the image with paint
+ PDColor color = getGraphicsState().getNonStrokingColor();
+ BufferedImage image = pdImage.getStencilImage(getPaint(color));
+
+ // draw the image
+ drawBufferedImage(image, at);
+ }
+ else
+ {
+ // draw the image
+ drawBufferedImage(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();
+ }
+ }
+
+ public void drawBufferedImage(BufferedImage image, AffineTransform at) throws IOException
+ {
+ graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
+ setClip();
+ PDSoftMask softMask = getGraphicsState().getSoftMask();
+ if( softMask != null )
+ {
+ AffineTransform imageTransform = new AffineTransform(at);
+ imageTransform.scale(1, -1);
+ imageTransform.translate(0, -1);
+ Paint awtPaint = new TexturePaint(image,
+ new Rectangle2D.Double(imageTransform.getTranslateX(), imageTransform.getTranslateY(),
+ imageTransform.getScaleX(), imageTransform.getScaleY()));
+ awtPaint = applySoftMaskToPaint(awtPaint, softMask);
+ graphics.setPaint(awtPaint);
+ Rectangle2D unitRect = new Rectangle2D.Float(0, 0, 1, 1);
+ graphics.fill(at.createTransformedShape(unitRect));
+ }
+ else
+ {
+ int width = image.getWidth(null);
+ int height = image.getHeight(null);
+ AffineTransform imageTransform = new AffineTransform(at);
+ imageTransform.scale(1.0 / width, -1.0 / height);
+ imageTransform.translate(0, -height);
+ graphics.drawImage(image, imageTransform, null);
+ }
+ }
+
+ @Override
+ public void shadingFill(COSName shadingName) throws IOException
+ {
+ PDShading shading = getResources().getShading(shadingName);
+ Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
+ Paint paint = shading.toPaint(ctm);
+
+ graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
+ graphics.setPaint(paint);
+ graphics.setClip(null);
+ lastClip = null;
+ graphics.fill(getGraphicsState().getCurrentClippingPath());
+ }
+
+ @Override
+ public void showAnnotation(PDAnnotation annotation) throws IOException
+ {
+ lastClip = null;
+ //TODO support more annotation flags (Invisible, NoZoom, NoRotate)
+ int deviceType = graphics.getDeviceConfiguration().getDevice().getType();
+ if (deviceType == GraphicsDevice.TYPE_PRINTER && !annotation.isPrinted())
+ {
+ return;
+ }
+ if (deviceType == GraphicsDevice.TYPE_RASTER_SCREEN && annotation.isNoView())
+ {
+ return;
+ }
+ if (annotation.isHidden())
+ {
+ return;
+ }
+ super.showAnnotation(annotation);
+ }
+
+ @Override
+ public void showTransparencyGroup(PDFormXObject form) throws IOException
+ {
+ TransparencyGroup group = new TransparencyGroup(form, false);
+
+ 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
+ PDRectangle bbox = group.getBBox();
+ AffineTransform prev = graphics.getTransform();
+ float x = bbox.getLowerLeftX();
+ float y = pageSize.getHeight() - bbox.getLowerLeftY() - bbox.getHeight();
+ graphics.setTransform(AffineTransform.getTranslateInstance(x * xform.getScaleX(),
+ y * xform.getScaleY()));
+
+ PDSoftMask softMask = getGraphicsState().getSoftMask();
+ if (softMask != null)
+ {
+ BufferedImage image = group.getImage();
+ Paint awtPaint = new TexturePaint(image,
+ new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight()));
+ awtPaint = applySoftMaskToPaint(awtPaint, softMask); // todo: PDFBOX-994 problem here?
+ graphics.setPaint(awtPaint);
+ graphics.fill(new Rectangle2D.Float(0, 0, bbox.getWidth() * (float)xform.getScaleX(),
+ bbox.getHeight() * (float)xform.getScaleY()));
+ }
+ else
+ {
+ graphics.drawImage(group.getImage(), null, null);
+ }
+
+ graphics.setTransform(prev);
+ }
+
+ /**
+ * 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 width;
+ private final int height;
+
+ /**
+ * Creates a buffered image for a transparency group result.
+ */
+ private TransparencyGroup(PDFormXObject form, boolean isSoftMask) throws IOException
+ {
+ Graphics2D g2dOriginal = graphics;
+ Area lastClipOriginal = lastClip;
+
+ // get the CTM x Form Matrix transform
+ Matrix ctm = getGraphicsState().getCurrentTransformationMatrix();
+ Matrix transform = Matrix.concatenate(ctm, form.getMatrix());
+
+ // transform the bbox
+ GeneralPath transformedBox = form.getBBox().transform(transform);
+
+ // clip the bbox to prevent giant bboxes from consuming all memory
+ Area clip = (Area)getGraphicsState().getCurrentClippingPath().clone();
+ clip.intersect(new Area(transformedBox));
+ Rectangle2D clipRect = clip.getBounds2D();
+ this.bbox = new PDRectangle((float)clipRect.getX(), (float)clipRect.getY(),
+ (float)clipRect.getWidth(), (float)clipRect.getHeight());
+
+ // apply the underlying Graphics2D device's DPI transform
+ Shape deviceClip = xform.createTransformedShape(clip);
+ Rectangle2D bounds = deviceClip.getBounds2D();
+
+ minX = (int) Math.floor(bounds.getMinX());
+ minY = (int) Math.floor(bounds.getMinY());
+ int maxX = (int) Math.floor(bounds.getMaxX()) + 1;
+ int maxY = (int) Math.floor(bounds.getMaxY()) + 1;
+
+ width = maxX - minX;
+ height = maxY - minY;
+
+ image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // FIXME - color space
+ Graphics2D g = image.createGraphics();
+
+ // flip y-axis
+ g.translate(0, height);
+ g.scale(1, -1);
+
+ // apply device transform (DPI)
+ g.transform(xform);
+
+ // adjust the origin
+ g.translate(-clipRect.getX(), -clipRect.getY());
+
+ graphics = g;
+ try
+ {
+ if (isSoftMask)
+ {
+ processSoftMask(form);
+ }
+ else
+ {
+ processTransparencyGroup(form);
+ }
+ }
+ finally
+ {
+ lastClip = lastClipOriginal;
+ graphics.dispose();
+ graphics = g2dOriginal;
+ }
+ }
+
+ public BufferedImage getImage()
+ {
+ return image;
+ }
+
+ public PDRectangle getBBox()
+ {
+ return bbox;
+ }
+
+ public Raster getAlphaRaster()
+ {
+ return image.getAlphaRaster();
+ }
+
+ public Raster getLuminosityRaster()
+ {
+ BufferedImage gray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+ Graphics g = gray.getGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+
+ return gray.getRaster();
+ }
+ }
+}