Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/PDXObject.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/PDXObject.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/PDXObject.java	(revision )
@@ -17,6 +17,7 @@
 package org.apache.pdfbox.pdmodel.graphics;
 
 import org.apache.pdfbox.cos.COSBase;
+import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.cos.COSStream;
 import org.apache.pdfbox.pdmodel.PDDocument;
@@ -24,6 +25,7 @@
 import org.apache.pdfbox.pdmodel.common.COSObjectable;
 import org.apache.pdfbox.pdmodel.common.PDStream;
 import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup;
 import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
 
 import java.io.IOException;
@@ -36,11 +38,11 @@
  */
 public class PDXObject implements COSObjectable
 {
-    private PDStream stream;
+    private final PDStream stream;
 
     /**
      * Creates a new XObject instance of the appropriate type for the COS stream.
-     * @param base The stream which is wrapped by this XObject.
+     * @param base The stream which is wrapped by this XObject. Cannot be null.
      * @return A new XObject instance.
      */
     public static PDXObject createXObject(COSBase base, String name, PDResources resources)
@@ -48,8 +50,7 @@
     {
         if (base == null)
         {
-            // TODO throw an exception?
-            return null;
+            throw new IllegalArgumentException("base cannot be null");
         }
 
         if (!(base instanceof COSStream))
@@ -66,8 +67,16 @@
         }
         else if (COSName.FORM.getName().equals(subtype))
         {
+            COSDictionary group = (COSDictionary)stream.getDictionaryObject(COSName.GROUP);
+            if (group != null && COSName.TRANSPARENCY.equals(group.getCOSName(COSName.S)))
+            {
+                return new PDTransparencyGroup(new PDStream(stream));
+            }
+            else
+            {
-            return new PDFormXObject(new PDStream(stream), name);
-        }
+                return new PDFormXObject(new PDStream(stream), name);
+            }
+        }
         else if (COSName.PS.getName().equals(subtype))
         {
             return new PDPostScriptXObject(new PDStream(stream));
@@ -104,7 +113,6 @@
 
     /**
      * Returns the stream.
-     * {@inheritDoc}
      */
     public final COSBase getCOSObject()
     {
Index: pdfbox/src/main/java/org/apache/pdfbox/contentstream/operator/graphics/DrawObject.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/contentstream/operator/graphics/DrawObject.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/contentstream/operator/graphics/DrawObject.java	(revision )
@@ -23,6 +23,7 @@
 import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.pdmodel.MissingResourceException;
 import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup;
 import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
 import org.apache.pdfbox.pdmodel.graphics.PDXObject;
 import org.apache.pdfbox.contentstream.operator.Operator;
@@ -50,17 +51,13 @@
             PDImageXObject image = (PDImageXObject)xobject;
             context.drawImage(image);
         }
-        else if (xobject instanceof PDFormXObject)
+        else if (xobject instanceof PDTransparencyGroup)
         {
-            PDFormXObject form = (PDFormXObject) xobject;
-            if (form.getGroup() != null &&
-                COSName.TRANSPARENCY.equals(form.getGroup().getSubType())) {
-                getContext().showTransparencyGroup(form);
+            getContext().showTransparencyGroup((PDTransparencyGroup)xobject);
-            }
+        }
-            else
+        else if (xobject instanceof PDFormXObject)
-            {
+        {
-                getContext().showForm(form);
-            }
+            getContext().showForm((PDFormXObject)xobject);
         }
     }
 
Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDGroup.java
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDGroup.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDTransparencyGroupAttributes.java	(revision )
@@ -18,55 +18,37 @@
 
 import java.io.IOException;
 
-import org.apache.pdfbox.cos.COSBase;
 import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.pdmodel.common.COSObjectable;
 import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
 
 /**
- * Transparency group.
+ * Transparency group attributes.
  * 
  * @author Kühn & Weyh Software, GmbH
  */
-public final class PDGroup implements COSObjectable
+public final class PDTransparencyGroupAttributes implements COSObjectable
 {
-    private COSDictionary dictionary;
-    private COSName subType;
+    private final COSDictionary dictionary;
     private PDColorSpace colorSpace;
 
     /**
      * Creates a group object from a given dictionary
      * @param dic {@link COSDictionary} object
      */
-    public PDGroup(COSDictionary dic)
+    public PDTransparencyGroupAttributes(COSDictionary dic)
     {
         dictionary = dic;
     }
 
-    public COSBase getCOSObject()
+    @Override
+    public COSDictionary getCOSObject()
     {
         return dictionary;
     }
 
-    public COSDictionary getCOSDictionary()
-    {
-        return dictionary;
-    }
-
     /**
-     * Returns the groups's subtype, should be "Transparency".
-     */
-    public COSName getSubType()
-    {
-        if (subType == null)
-        {
-            subType = (COSName) getCOSDictionary().getDictionaryObject(COSName.S);
-        }
-        return subType;
-    }
-
-    /**
      * Returns the blending color space
      * @return color space
      * @throws IOException
@@ -75,8 +57,7 @@
     {
         if (colorSpace == null)
         {
-            colorSpace = PDColorSpace.create(getCOSDictionary().getDictionaryObject(
-                    COSName.COLORSPACE));
+            colorSpace = PDColorSpace.create(dictionary.getDictionaryObject(COSName.COLORSPACE));
         }
         return colorSpace;
     }
@@ -87,7 +68,7 @@
      */
     public boolean isIsolated()
     {
-        return getCOSDictionary().getBoolean(COSName.I, false);
+        return dictionary.getBoolean(COSName.I, false);
     }
 
     /**
@@ -96,6 +77,6 @@
      */
     public boolean isKnockout()
     {
-        return getCOSDictionary().getBoolean(COSName.K, false);
+        return dictionary.getBoolean(COSName.K, false);
     }
 }
Index: pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java	(revision )
@@ -52,6 +52,7 @@
 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.form.PDTransparencyGroup;
 import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
 import org.apache.pdfbox.pdmodel.graphics.state.PDGraphicsState;
 import org.apache.pdfbox.pdmodel.graphics.state.PDTextState;
@@ -157,7 +158,7 @@
      * @param form transparency group (form) XObject
      * @throws IOException if the transparency group cannot be processed
      */
-    public void showTransparencyGroup(PDFormXObject form) throws IOException
+    public void showTransparencyGroup(PDTransparencyGroup form) throws IOException
     {
         processTransparencyGroup(form);
     }
Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDExtendedGraphicsState.java	(revision )
@@ -17,7 +17,6 @@
 package org.apache.pdfbox.pdmodel.graphics.state;
 
 import java.io.IOException;
-
 import org.apache.pdfbox.cos.COSArray;
 import org.apache.pdfbox.cos.COSBase;
 import org.apache.pdfbox.cos.COSDictionary;
@@ -28,6 +27,7 @@
 import org.apache.pdfbox.pdmodel.graphics.PDFontSetting;
 import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern;
 import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
+import org.apache.pdfbox.util.Matrix;
 
 /**
  * An extended graphics state dictionary.
@@ -135,7 +135,7 @@
             }
             else if( key.equals( COSName.SMASK ) ) 
             {
-                gs.setSoftMask(getSoftMask());
+                gs.setSoftMask(getSoftMask(gs.getCurrentTransformationMatrix()));
             }
             else if( key.equals( COSName.BM ) ) 
             {
@@ -527,12 +527,10 @@
      *
      * @return the soft mask
      */
-    public PDSoftMask getSoftMask()
+    public PDSoftMask getSoftMask(Matrix ctm)
     {
-        return PDSoftMask.create(dict.getDictionaryObject(COSName.SMASK));
+        return PDSoftMask.create(dict.getDictionaryObject(COSName.SMASK), ctm);
     }
-
-    /**
 
     /**
      * This will get the text knockout flag.
Index: pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java	(revision )
+++ pdfbox/src/main/java/org/apache/pdfbox/rendering/SoftMask.java	(revision )
@@ -0,0 +1,131 @@
+/*
+ * 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.Paint;
+import java.awt.PaintContext;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+
+/**
+ * A Paint which applies a soft mask to an underlying Paint.
+ *
+ * @author John Hewson
+ */
+class SoftMask implements Paint
+{
+    private static final ColorModel ARGB_COLOR_MODEL =
+            new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getColorModel();
+
+    private final Paint paint;
+    private final BufferedImage mask;
+    private final Rectangle2D bboxDevice;
+
+    /**
+     * Creates a new soft mask paint.
+     *
+     * @param paint underlying paint.
+     * @param mask soft mask
+     * @param bboxDevice bbox of the soft mask in the underlying Graphics2D device space
+     */
+    SoftMask(Paint paint, BufferedImage mask, Rectangle2D bboxDevice)
+    {
+        this.paint = paint;
+        this.mask = mask;
+        this.bboxDevice = bboxDevice;
+    }
+
+    @Override
+    public PaintContext createContext(ColorModel cm, Rectangle deviceBounds,
+                                      Rectangle2D userBounds, AffineTransform xform,
+                                      RenderingHints hints)
+    {
+        PaintContext ctx = paint.createContext(cm, deviceBounds, userBounds, xform, hints);
+        return new SoftPaintContext(cm, deviceBounds, userBounds, xform, hints, ctx);
+    }
+
+    @Override
+    public int getTransparency()
+    {
+        return TRANSLUCENT;
+    }
+
+    private class SoftPaintContext implements PaintContext
+    {
+        private final PaintContext context;
+
+        SoftPaintContext(ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds,
+                         AffineTransform xform, RenderingHints hints, PaintContext context)
+        {
+            this.context = context;
+        }
+
+        @Override
+        public ColorModel getColorModel()
+        {
+            return ARGB_COLOR_MODEL;
+        }
+
+        @Override
+        public Raster getRaster(int x1, int y1, int w, int h)
+        {
+            WritableRaster raster = (WritableRaster)context.getRaster(x1, y1, w, h);
+
+            // buffer
+            BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+            WritableRaster output = image.getRaster();
+
+            // the soft mask has its own bbox
+            x1 = x1 - (int)bboxDevice.getX();
+            y1 = y1 - (int)bboxDevice.getY();
+
+            int[] gray = new int[4];
+            int[] rgba = new int[4];
+            for (int y = 0; y < h; y++)
+            {
+                for (int x = 0; x < w; x++)
+                {
+                    raster.getPixel(x, y, rgba);
+
+                    // get the alpha value from the gray mask, if within mask bounds
+                    gray[0] = 0;
+                    if (x1 + x < mask.getWidth() && y1 + y < mask.getHeight())
+                    {
+                        mask.getRaster().getPixel(x1 + x, y1 + y, gray);
+                    }
+
+                    rgba[3] = Math.round(rgba[3] * (gray[0] / 255f)); // multiply alpha
+                    output.setPixel(x, y, rgba);
+                }
+            }
+
+            return output;
+        }
+
+        @Override
+        public void dispose()
+        {
+        }
+    }
+}
Index: pdfbox/src/main/java/org/apache/pdfbox/contentstream/operator/DrawObject.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/contentstream/operator/DrawObject.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/contentstream/operator/DrawObject.java	(revision )
@@ -20,6 +20,7 @@
 import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.pdmodel.graphics.PDXObject;
 import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup;
 import org.apache.pdfbox.util.PDFMarkedContentExtractor;
 
 import java.io.IOException;
@@ -43,10 +44,13 @@
             ((PDFMarkedContentExtractor) context).xobject(xobject);
         }
 
-        if(xobject instanceof PDFormXObject)
+        if (xobject instanceof PDTransparencyGroup)
         {
-            PDFormXObject form = (PDFormXObject)xobject;
-            context.showForm(form);
+            context.showTransparencyGroup((PDTransparencyGroup)xobject);
+        }
+        else if (xobject instanceof PDFormXObject)
+        {
+            context.showForm((PDFormXObject)xobject);
         }
     }
 
Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDTransparencyGroup.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDTransparencyGroup.java	(revision )
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDTransparencyGroup.java	(revision )
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.pdmodel.graphics.form;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.common.PDStream;
+
+/**
+ * A transparency group.
+ *
+ * @author John Hewson
+ */
+public class PDTransparencyGroup extends PDFormXObject
+{
+    /**
+     * Creates a Transparency Group for reading.
+     * @param stream The XObject stream
+     */
+    public PDTransparencyGroup(PDStream stream)
+    {
+        super(stream);
+    }
+
+    /**
+     * Creates a Transparency Group for writing, in the given document.
+     * @param document The current document
+     */
+    public PDTransparencyGroup(PDDocument document)
+    {
+        super(document);
+        // todo: set mandatory fields
+    }
+}
Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/form/PDFormXObject.java	(revision )
@@ -31,20 +31,6 @@
 import org.apache.pdfbox.pdmodel.graphics.PDXObject;
 import org.apache.pdfbox.util.Matrix;
 
-/*
-TODO There are further Form XObjects to implement:
-
-+ PDFormXObject
-|- PDReferenceXObject
-|- PDGroupXObject
-   |- PDTransparencyXObject
-
-See PDF 32000 p111
-
-When doing this all methods on PDFormXObject should probably be made
-final and all fields private.
-*/
-
 /**
  * A Form XObject.
  * 
@@ -52,11 +38,9 @@
  */
 public class PDFormXObject extends PDXObject implements PDContentStream
 {
-    // name of XObject in resources, to prevent recursion
-    private String name;
+    private PDTransparencyGroupAttributes group;
+    private String name; // resource name, for debugging only
 
-    private PDGroup group;
-
     /**
      * Creates a Form XObject for reading.
      * @param stream The XObject stream
@@ -105,17 +89,18 @@
     }
 
     /**
-     * Returns the group attributes dictionary (Group XObject).
+     * Returns the group attributes dictionary.
      *
      * @return the group attributes dictionary
      */
-    public PDGroup getGroup() {
+    public PDTransparencyGroupAttributes getGroup()
+    {
-        if( group == null ) 
+        if( group == null )
         {
             COSDictionary dic = (COSDictionary) getCOSStream().getDictionaryObject(COSName.GROUP);
             if( dic != null ) 
             {
-                group = new PDGroup(dic);
+                group = new PDTransparencyGroupAttributes(dic);
             }
         }
         return group;
@@ -241,5 +226,11 @@
     public void setStructParents(int structParent)
     {
         getCOSStream().setInt(COSName.STRUCT_PARENTS, structParent);
+    }
+
+    @Override
+    public String toString()
+    {
+        return "PDFormXObject{" + name + "}";
     }
 }
Index: pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java	(revision )
@@ -32,7 +32,6 @@
 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;
@@ -43,6 +42,7 @@
 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.form.PDTransparencyGroup;
 import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
 import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern;
 import org.apache.pdfbox.pdmodel.graphics.pattern.PDShadingPattern;
@@ -56,10 +56,8 @@
 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;
@@ -83,6 +81,7 @@
     // the graphics device to draw to, xform is the initial transform of the device (i.e. DPI)
     private Graphics2D graphics;
     private AffineTransform xform;
+    private AffineTransform xformFlipped;
 
     // the page box to draw (usually the crop box but may be another)
     PDRectangle pageSize;
@@ -148,8 +147,9 @@
 
         setRenderingHints();
 
-        graphics.translate(0, (int) pageSize.getHeight());
+        graphics.translate(0, pageSize.getHeight());
         graphics.scale(1, -1);
+        xformFlipped = graphics.getTransform();
 
         // TODO use getStroke() to set the initial stroke
         graphics.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
@@ -239,7 +239,6 @@
                 }
                 return shading.toPaint(Matrix.concatenate(getInitialMatrix(),
                                                           shadingPattern.getMatrix()));
-
             }
         }
     }
@@ -433,38 +432,37 @@
         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
+    // todo: move soft mask apply to getPaint()?
+    private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException
     {
+        if (softMask != null)
+        {
-        TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true);
+            TransparencyGroup transparencyGroup = new TransparencyGroup(softMask.getGroup(), true);
+            BufferedImage image;
+
-        COSName subtype = softMask.getSubType();
-        if (COSName.ALPHA.equals(subtype))
-        {
+            COSName subtype = softMask.getSubType();
+            if (COSName.ALPHA.equals(subtype))
+            {
-            return transparencyGroup.getAlphaRaster();
+                //image = transparencyGroup.getImage().getAlphaRaster();
+                throw new UnsupportedOperationException("Not implemented: Alpha SMask"); // todo (need example PDF)
-        }
-        else if (COSName.LUMINOSITY.equals(subtype))
-        {
+            }
+            else if (COSName.LUMINOSITY.equals(subtype))
+            {
-            return transparencyGroup.getLuminosityRaster();
+                // todo: use ColorConvertOp?
+                image = transparencyGroup.getImage();
+                BufferedImage gray = new BufferedImage(image.getWidth(), image.getHeight(),
+                                                       BufferedImage.TYPE_BYTE_GRAY);
+                Graphics g = gray.getGraphics();
+                g.drawImage(image, 0, 0, null);
+                g.dispose();
-        }
-        else
-        {
+            }
+            else
+            {
-            throw new IOException("Invalid soft mask subtype.");
+                throw new IOException("Invalid soft mask subtype"); // todo: shouldn't this be elsewhere?
-        }
+            }
+            return new SoftMask(parentPaint, image, transparencyGroup.getBounds());
-    }
+        }
-
-    private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throws IOException
-    {
-        if (softMask != null) 
-        {
-            return new SoftMaskPaint(parentPaint, createSoftMaskRaster(softMask));
-        }
-        else 
+        else
         {
             return parentPaint;
         }
@@ -481,6 +479,7 @@
     // returns the non-stroking AWT Paint
     private Paint getNonStrokingPaint() throws IOException
     {
+        // todo: why no soft mask?
         return getPaint(getGraphicsState().getNonStrokingColor());
     }
 
@@ -708,7 +707,7 @@
         {
             // fill the image with paint
             PDColor color = getGraphicsState().getNonStrokingColor();
-            BufferedImage image = pdImage.getStencilImage(getPaint(color));
+            BufferedImage image = pdImage.getStencilImage(getPaint(color));  // todo: why no soft mask?
 
             // draw the image
             drawBufferedImage(image, at);
@@ -792,111 +791,108 @@
     }
 
     @Override
-    public void showTransparencyGroup(PDFormXObject form) throws IOException
+    public void showTransparencyGroup(PDTransparencyGroup 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()));
+        // blit directly onto the graphics device using the identity transform
+        Rectangle2D bounds = group.getBounds();
+        graphics.setTransform(new AffineTransform());
 
         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?
+            Paint awtPaint = new TexturePaint(image, new Rectangle2D.Float(0, 0,
+                                              image.getWidth(), image.getHeight()));
+            awtPaint = applySoftMaskToPaint(awtPaint, softMask);
             graphics.setPaint(awtPaint);
-            graphics.fill(new Rectangle2D.Float(0, 0, bbox.getWidth() * (float)xform.getScaleX(),
-                                                bbox.getHeight() * (float)xform.getScaleY()));
+            graphics.fill(bounds);
         }
         else
         {
-            graphics.drawImage(group.getImage(), null, null);
+            AffineTransform at = AffineTransform.getTranslateInstance(bounds.getX(), bounds.getY());
+            graphics.drawImage(group.getImage(), at, null);
         }
 
-        graphics.setTransform(prev);
+        // restore the device transform
+        graphics.setTransform(xformFlipped);
     }
 
     /**
      * Transparency group.
      **/
-    private final class TransparencyGroup
+    final class TransparencyGroup
     {
         private final BufferedImage image;
-        private final PDRectangle bbox;
+        private final Rectangle2D deviceBounds;
 
-        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.
+         * Creates a buffered image for a transparency group result. This is rendered to a buffer
+         * which uses the underlying Graphics2D device space, requiring no further transformation.
          */
-        private TransparencyGroup(PDFormXObject form, boolean isSoftMask) throws IOException
+        private TransparencyGroup(PDTransparencyGroup group, 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());
+            Matrix transform = Matrix.concatenate(ctm, group.getMatrix());
 
             // transform the bbox
-            GeneralPath transformedBox = form.getBBox().transform(transform);
+            GeneralPath transformedBox = group.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());
+            //Area clip = (Area)getGraphicsState().getCurrentClippingPath().clone();
+            //clip.intersect(new Area(transformedBox));
+            //Rectangle2D clipRect = clip.getBounds2D();
 
-            // apply the underlying Graphics2D device's DPI transform
-            Shape deviceClip = xform.createTransformedShape(clip);
+            // fixme: we render *the entire page* due to bug in above code / bbox
+            Rectangle2D clip = new Rectangle2D.Float(0, 0, pageSize.getWidth(), pageSize.getHeight());
+
+            // apply the underlying Graphics2D device's DPI transform and y-axis flip
+            Shape deviceClip = xformFlipped.createTransformedShape(clip);
             Rectangle2D bounds = deviceClip.getBounds2D();
+            deviceBounds = bounds;
 
-            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;
+            int minX = (int)Math.floor(bounds.getMinX());
+            int minY = (int)Math.floor(bounds.getMinY());
+            int maxX = (int)Math.ceil(bounds.getMaxX());
+            int maxY = (int)Math.ceil(bounds.getMaxY());
 
-            width = maxX - minX;
-            height = maxY - minY;
+            int width = maxX - minX;
+            int height = maxY - minY;
 
-            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // FIXME - color space
+            image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
             Graphics2D g = image.createGraphics();
 
             // flip y-axis
-            g.translate(0, height);
+            //g.translate(0, height);
+            g.translate(0, bounds.getHeight());
             g.scale(1, -1);
 
             // apply device transform (DPI)
             g.transform(xform);
 
             // adjust the origin
-            g.translate(-clipRect.getX(), -clipRect.getY());
+            //g.translate(-clipRect.getX(), -clipRect.getY());
+            //g.translate(-minX, -minY);
+            g.translate(-bounds.getX(), -bounds.getY());
 
             graphics = g;
             try
             {
                 if (isSoftMask)
                 {
-                    processSoftMask(form);
+                    processSoftMask(group);
                 }
                 else
                 {
-                    processTransparencyGroup(form);
+                    processTransparencyGroup(group);
                 }
             }
             finally 
@@ -907,29 +903,20 @@
             }
         }
 
+        /**
+         * Returns the rendered transparency group in the underlying Graphics2D device space.
+         */
         public BufferedImage getImage()
         {
             return image;
         }
 
-        public PDRectangle getBBox()
+        /**
+         * Returns the bounds of the transparency group in the underlying Graphics2D device space.
+         */
+        public Rectangle2D getBounds()
         {
-            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();
+            return deviceBounds;
         }
     }
 }
Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/SoftMaskPaint.java
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/SoftMaskPaint.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/blend/SoftMaskPaint.java	(revision 1659037)
@@ -1,180 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.pdfbox.pdmodel.graphics.blend;
-
-import java.awt.Paint;
-import java.awt.PaintContext;
-import java.awt.Point;
-import java.awt.Rectangle;
-import java.awt.RenderingHints;
-import java.awt.Transparency;
-import java.awt.geom.AffineTransform;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
-import java.awt.image.ColorConvertOp;
-import java.awt.image.ColorModel;
-import java.awt.image.ComponentColorModel;
-import java.awt.image.DataBuffer;
-import java.awt.image.Raster;
-import java.awt.image.WritableRaster;
-import java.io.IOException;
-
-/**
- * AWT Paint that adds a soft mask to the alpha channel of the existing parent paint. If the parent
- * paint does not have an alpha channel, a new raster is created.
- * 
- * @author Kühn & Weyh Software, GmbH
- */
-public final class SoftMaskPaint implements Paint
-{
-    private final Paint parentPaint;
-    private final Raster softMaskRaster;
-
-    /**
-     * Applies the soft mask to the parent.
-     */
-    public SoftMaskPaint(Paint parentPaint, Raster softMaskRaster)
-    {
-        this.parentPaint = parentPaint;
-        this.softMaskRaster = softMaskRaster;
-    }
-
-    @Override
-    public int getTransparency()
-    {
-        return Transparency.TRANSLUCENT;
-    }
-
-    @Override
-    public PaintContext createContext(ColorModel cm, Rectangle deviceBounds,
-            Rectangle2D userBounds, AffineTransform at, RenderingHints hints)
-    {
-        try
-        {
-            PaintContext parentContext = parentPaint.createContext(null, deviceBounds, userBounds,
-                    at, hints);
-            return new Context(parentContext);
-        }
-        catch (IOException e)
-        {
-            return null; // context cannot be created
-        }
-    }
-
-    private class Context implements PaintContext
-    {
-        private final PaintContext parentContext;
-        private final ColorModel colorModel;
-        private final int numColorComponents;
-        private final ColorModel parentColorModel;
-
-        Context(PaintContext parentContext) throws IOException
-        {
-            this.parentContext = parentContext;
-            parentColorModel = parentContext.getColorModel();
-            if (parentContext.getColorModel().hasAlpha())
-            {
-                colorModel = parentColorModel;
-            }
-            else
-            {
-                colorModel = new ComponentColorModel(parentContext.getColorModel()
-                        .getColorSpace(), true, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
-            }
-            numColorComponents = colorModel.getNumColorComponents();
-        }
-
-        @Override
-        public ColorModel getColorModel()
-        {
-            return colorModel;
-        }
-
-        @Override
-        public Raster getRaster(int x, int y, int w, int h)
-        {
-            Raster parentRaster = parentContext.getRaster(x, y, w, h);
-
-            // getRaster can return the raster with origin (0,0) even if we applied for (x,y)
-            int parentMinX = parentRaster.getMinX();
-            int parentMinY = parentRaster.getMinY();
-
-            WritableRaster result;
-            if (parentRaster instanceof WritableRaster)
-            {
-                if (parentColorModel.equals(colorModel))
-                {
-                    result = parentRaster.createCompatibleWritableRaster();
-                    result.setDataElements(-parentMinX, -parentMinY, parentRaster);
-                }
-                else
-                {
-                    BufferedImage parentImage = new BufferedImage(parentColorModel,
-                            (WritableRaster) parentRaster,
-                            parentColorModel.isAlphaPremultiplied(), null);
-                    result = Raster.createWritableRaster(
-                            colorModel.createCompatibleSampleModel(w, h), new Point(0, 0));
-                    BufferedImage resultImage = new BufferedImage(colorModel, result, false, null);
-                    resultImage.getGraphics().drawImage(parentImage, 0, 0, null);
-                }
-            }
-            else
-            {
-                result = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, w, h, getColorModel()
-                        .getNumComponents(), new Point(0, 0));
-                ColorConvertOp colorConvertOp = new ColorConvertOp(
-                        parentColorModel.getColorSpace(), colorModel.getColorSpace(), null);
-                colorConvertOp.filter(parentRaster, result);
-            }
-
-            int softMaskMinX = softMaskRaster.getMinX();
-            int softMaskMinY = softMaskRaster.getMinY();
-            int softMaskMaxX = softMaskMinX + softMaskRaster.getWidth();
-            int softMaskMaxY = softMaskMinY + softMaskRaster.getHeight();
-
-            for (int j = 0; j < h; j++)
-            {
-                for (int i = 0; i < w; i++)
-                {
-                    int rx = x + i;
-                    int ry = y + j;
-
-                    int alpha;
-                    if ((rx >= softMaskMinX) && (rx < softMaskMaxX) && (ry >= softMaskMinY)
-                            && (ry < softMaskMaxY))
-                    {
-                        alpha = softMaskRaster.getSample(rx, ry, 0);
-                    }
-                    else
-                    {
-                        alpha = 0;
-                    }
-                    alpha = alpha * result.getSample(i, j, numColorComponents) / 255;
-                    result.setSample(i, j, numColorComponents, alpha);
-                }
-            }
-
-            return result;
-        }
-
-        @Override
-        public void dispose()
-        {
-            // do nothing
-        }
-    }
-}
Index: pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDSoftMask.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDSoftMask.java	(revision 1659037)
+++ pdfbox/src/main/java/org/apache/pdfbox/pdmodel/graphics/state/PDSoftMask.java	(revision )
@@ -27,7 +27,8 @@
 import org.apache.pdfbox.pdmodel.common.COSObjectable;
 import org.apache.pdfbox.pdmodel.common.function.PDFunction;
 import org.apache.pdfbox.pdmodel.graphics.PDXObject;
-import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.form.PDTransparencyGroup;
+import org.apache.pdfbox.util.Matrix;
 
 /**
  * Soft mask.
@@ -36,12 +37,14 @@
  */
 public final class PDSoftMask implements COSObjectable
 {
+    private static final Log LOG = LogFactory.getLog(PDSoftMask.class);
+
     /**
      * Creates a new soft mask.
      *
      * @param dictionary SMask
      */
-    public static PDSoftMask create(COSBase dictionary)
+    public static PDSoftMask create(COSBase dictionary, Matrix ctm)
     {
         if (dictionary instanceof COSName)
         {
@@ -57,7 +60,7 @@
         }
         else if (dictionary instanceof COSDictionary)
         {
-            return new PDSoftMask((COSDictionary) dictionary);
+            return new PDSoftMask((COSDictionary) dictionary, ctm);
         }
         else
         {
@@ -66,33 +69,28 @@
         }
     }
 
-    private static final Log LOG = LogFactory.getLog(PDSoftMask.class);
-
     private COSDictionary dictionary;
-    private COSName subType = null;
-    private PDFormXObject group = null;
-    private COSArray backdropColor = null;
-    private PDFunction transferFunction = null;
+    private COSName subType;
+    private PDTransparencyGroup group;
+    private COSArray backdropColor;
+    private PDFunction transferFunction;
+    private Matrix ctm;
 
     /**
      * Creates a new soft mask.
      */
-    public PDSoftMask(COSDictionary dictionary)
+    public PDSoftMask(COSDictionary dictionary, Matrix ctm)
     {
-        super();
         this.dictionary = dictionary;
+        this.ctm = ctm;
     }
 
-    public COSBase getCOSObject()
+    @Override
+    public COSDictionary getCOSObject()
     {
         return dictionary;
     }
 
-    public COSDictionary getCOSDictionary()
-    {
-        return dictionary;
-    }
-
     /**
      * Returns the subtype of the soft mask (Alpha, Luminosity) - S entry
      */
@@ -100,7 +98,7 @@
     {
         if (subType == null)
         {
-            subType = (COSName) getCOSDictionary().getDictionaryObject(COSName.S);
+            subType = (COSName) dictionary.getDictionaryObject(COSName.S);
         }
         return subType;
     }
@@ -111,15 +109,14 @@
      * @return form containing the transparency group
      * @throws IOException
      */
-    public PDFormXObject getGroup() throws IOException
+    public PDTransparencyGroup getGroup() throws IOException
     {
         if (group == null)
         {
-            COSBase cosGroup = getCOSDictionary().getDictionaryObject(COSName.G);
+            COSBase cosGroup = dictionary.getDictionaryObject(COSName.G);
             if (cosGroup != null)
             {
-                group = (PDFormXObject) PDXObject
-                        .createXObject(cosGroup, COSName.G.getName(), null);
+                group = (PDTransparencyGroup)PDXObject.createXObject(cosGroup, COSName.G.getName(), null);
             }
         }
         return group;
@@ -132,7 +129,7 @@
     {
         if (backdropColor == null)
         {
-            backdropColor = (COSArray) getCOSDictionary().getDictionaryObject(COSName.BC);
+            backdropColor = (COSArray) dictionary.getDictionaryObject(COSName.BC);
         }
         return backdropColor;
     }
@@ -144,12 +141,20 @@
     {
         if (transferFunction == null)
         {
-            COSBase cosTF = getCOSDictionary().getDictionaryObject(COSName.TR);
+            COSBase cosTF = dictionary.getDictionaryObject(COSName.TR);
             if (cosTF != null)
             {
                 transferFunction = PDFunction.create(cosTF);
             }
         }
         return transferFunction;
+    }
+
+    /**
+     * Returns the CTM when this soft mask was set using the 'gs' operator.
+     */
+    public Matrix getCTM()
+    {
+        return ctm;
     }
 }
