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();
+        }
+    }
+}