You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ms...@apache.org on 2016/05/22 06:48:34 UTC

svn commit: r1745013 - in /pdfbox/trunk/pdfbox/src: main/java/org/apache/pdfbox/pdmodel/interactive/annotation/ main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/ test/java/org/apache/pdfbox/pdmodel/interactive/annotation/

Author: msahyoun
Date: Sun May 22 06:48:34 2016
New Revision: 1745013

URL: http://svn.apache.org/viewvc?rev=1745013&view=rev
Log:
PDFBOX-3353: initial support to generate a default appearance stream for annotations 

Added:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java   (with props)
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java   (with props)
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java   (with props)
    pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java   (with props)
Modified:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java?rev=1745013&r1=1745012&r2=1745013&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAnnotationSquareCircle.java Sun May 22 06:48:34 2016
@@ -22,15 +22,18 @@ import org.apache.pdfbox.cos.COSDictiona
 import org.apache.pdfbox.cos.COSName;
 import org.apache.pdfbox.pdmodel.common.PDRectangle;
 import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDAppearanceHandler;
+import org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDSquareAppearanceHandler;
 
 /**
- * This is the class that represents a rectangular or eliptical annotation Introduced in PDF 1.3 specification .
+ * This is the class that represents a rectangular or elliptical annotation introduced in PDF 1.3 specification .
  *
  * @author Paul King
  */
 public class PDAnnotationSquareCircle extends PDAnnotationMarkup
 {
 
+    private PDAppearanceHandler squareAppearanceHandler;
     /**
      * Constant for a Rectangular type of annotation.
      */
@@ -62,6 +65,36 @@ public class PDAnnotationSquareCircle ex
     }
 
     /**
+     * Set a custom appearance handler for generating the annotations appearance streams.
+     * 
+     * @param squareAppearanceHandler
+     */
+    public void setCustomSquareAppearanceHandler(PDAppearanceHandler squareAppearanceHandler)
+    {
+        this.squareAppearanceHandler = squareAppearanceHandler;
+    }
+    
+    public void constructAppearances()
+    {
+        if (getSubtype() == SUB_TYPE_SQUARE)
+        {
+            if (squareAppearanceHandler == null)
+            {
+                PDSquareAppearanceHandler appearanceHandler = new PDSquareAppearanceHandler(this);
+                appearanceHandler.generateAppearanceStreams();
+            }
+            else
+            {
+                squareAppearanceHandler.generateAppearanceStreams();
+            }
+        }
+        else if (getSubtype() == SUB_TYPE_SQUARE)
+        {
+            
+        }
+    }
+    
+    /**
      * This will set interior color of the drawn area color is in DeviceRGB colo rspace.
      *
      * @param ic color in the DeviceRGB color space.

Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java?rev=1745013&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java (added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java Sun May 22 06:48:34 2016
@@ -0,0 +1,1434 @@
+/*
+ * 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.interactive.annotation;
+
+import java.awt.Color;
+import java.awt.geom.AffineTransform;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.Stack;
+import org.apache.pdfbox.cos.COSBase;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.cos.COSNumber;
+import org.apache.pdfbox.pdfwriter.COSWriter;
+import org.apache.pdfbox.pdmodel.PDResources;
+import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceN;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
+import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
+import org.apache.pdfbox.pdmodel.graphics.color.PDPattern;
+import org.apache.pdfbox.pdmodel.graphics.color.PDSeparation;
+import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
+import org.apache.pdfbox.pdmodel.graphics.image.PDInlineImage;
+import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
+import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
+import org.apache.pdfbox.util.Charsets;
+import org.apache.pdfbox.util.Matrix;
+
+/**
+ * Provides the ability to write to a page content stream.
+ *
+ * @author Ben Litchfield
+ */
+public final class PDAppearanceContentStream implements Closeable
+{
+    
+    /**
+     * This is to choose what to do with the stream: overwrite, append or prepend.
+     */
+    public static enum AppendMode
+    {
+        /**
+         * Overwrite the existing page content streams.
+         */
+        OVERWRITE, 
+        /**
+         * Append the content stream after all existing page content streams.
+         */
+        APPEND, 
+        /**
+         * Insert before all other page content streams.
+         */
+        PREPEND;
+
+        public boolean isOverwrite()
+        {
+            return this == OVERWRITE;
+        }
+
+        public boolean isPrepend()
+        {
+            return this == PREPEND;
+        }
+    }
+    
+    private OutputStream output;
+    private PDResources resources;
+
+    private boolean inTextMode = false;
+    private final Stack<PDFont> fontStack = new Stack<PDFont>();
+
+    private final Stack<PDColorSpace> nonStrokingColorSpaceStack = new Stack<PDColorSpace>();
+    private final Stack<PDColorSpace> strokingColorSpaceStack = new Stack<PDColorSpace>();
+
+    // number format
+    private final NumberFormat formatDecimal = NumberFormat.getNumberInstance(Locale.US);
+
+    /**
+     * Create a new appearance stream.
+     *
+     * @param doc The document the page is part of.
+     * @param appearance The appearance stream to write to.
+     * @throws IOException If there is an error writing to the page contents.
+     */
+    public PDAppearanceContentStream(PDAppearanceStream appearance) throws IOException
+    {
+        this (appearance, appearance.getStream().createOutputStream()); 
+    }
+    
+    /**
+     * Create a new appearance stream. Note that this is not actually a "page" content stream.
+     *
+     * @param doc The document the appearance is part of.
+     * @param appearance The appearance stream to add to.
+     * @param outputStream The appearances output stream to write to.
+     * @throws IOException If there is an error writing to the page contents.
+     */
+    public PDAppearanceContentStream(PDAppearanceStream appearance, OutputStream outputStream)
+            throws IOException
+    {
+        output = outputStream;
+        this.resources = appearance.getResources();
+        
+        formatDecimal.setMaximumFractionDigits(4);
+        formatDecimal.setGroupingUsed(false);
+    }
+
+    /**
+     * Begin some text operations.
+     *
+     * @throws IOException If there is an error writing to the stream or if you attempt to
+     *         nest beginText calls.
+     * @throws IllegalStateException If the method was not allowed to be called at this time.
+     */
+    public void beginText() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: Nested beginText() calls are not allowed.");
+        }
+        writeOperator("BT");
+        inTextMode = true;
+    }
+
+    /**
+     * End some text operations.
+     *
+     * @throws IOException If there is an error writing to the stream or if you attempt to
+     *         nest endText calls.
+     * @throws IllegalStateException If the method was not allowed to be called at this time.
+     */
+    public void endText() throws IOException
+    {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Error: You must call beginText() before calling endText.");
+        }
+        writeOperator("ET");
+        inTextMode = false;
+    }
+    
+    /**
+     * Set the font and font size to draw text with.
+     *
+     * @param font The font to use.
+     * @param fontSize The font size to draw the text.
+     * @throws IOException If there is an error writing the font information.
+     */
+    public void setFont(PDFont font, float fontSize) throws IOException
+    {
+        if (fontStack.isEmpty())
+        {
+            fontStack.add(font);
+        }
+        else
+        {
+            fontStack.setElementAt(font, fontStack.size() - 1);
+        }
+
+        writeOperand(resources.add(font));
+        writeOperand(fontSize);
+        writeOperator("Tf");
+    }
+
+    /**
+     * Shows the given text at the location specified by the current text matrix.
+     *
+     * @param text The Unicode text to show.
+     * @throws IOException If an io exception occurs.
+     */
+    public void showText(String text) throws IOException
+    {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Must call beginText() before showText()");
+        }
+
+        if (fontStack.isEmpty())
+        {
+            throw new IllegalStateException("Must call setFont() before showText()");
+        }
+
+        PDFont font = fontStack.peek();
+
+        // Unicode code points to keep when subsetting
+        if (font.willBeSubset())
+        {
+            for (int offset = 0; offset < text.length(); )
+            {
+                int codePoint = text.codePointAt(offset);
+                font.addToSubset(codePoint);
+                offset += Character.charCount(codePoint);
+            }
+        }
+
+        COSWriter.writeString(font.encode(text), output);
+        write(" ");
+
+        writeOperator("Tj");
+    }
+
+    /**
+     * Sets the text leading.
+     *
+     * @param leading The leading in unscaled text units.
+     * @throws IOException If there is an error writing to the stream.
+     */
+    public void setLeading(double leading) throws IOException
+    {
+        writeOperand((float) leading);
+        writeOperator("TL");
+    }
+
+    /**
+     * Move to the start of the next line of text. Requires the leading (see {@link #setLeading})
+     * to have been set.
+     *
+     * @throws IOException If there is an error writing to the stream.
+     */
+    public void newLine() throws IOException
+    {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Must call beginText() before newLine()");
+        }
+        writeOperator("T*");
+    }
+
+    /**
+     * The Td operator.
+     * Move to the start of the next line, offset from the start of the current line by (tx, ty).
+     *
+     * @param tx The x translation.
+     * @param ty The y translation.
+     * @throws IOException If there is an error writing to the stream.
+     * @throws IllegalStateException If the method was not allowed to be called at this time.
+     */
+    public void newLineAtOffset(float tx, float ty) throws IOException
+    {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Error: must call beginText() before newLineAtOffset()");
+        }
+        writeOperand(tx);
+        writeOperand(ty);
+        writeOperator("Td");
+    }
+
+    /**
+     * The Tm operator. Sets the text matrix to the given values.
+     * A current text matrix will be replaced with the new one.
+     *
+     * @param matrix the transformation matrix
+     * @throws IOException If there is an error writing to the stream.
+     * @throws IllegalStateException If the method was not allowed to be called at this time.
+     */
+    public void setTextMatrix(Matrix matrix) throws IOException
+    {
+        if (!inTextMode)
+        {
+            throw new IllegalStateException("Error: must call beginText() before setTextMatrix");
+        }
+        writeAffineTransform(matrix.createAffineTransform());
+        writeOperator("Tm");
+    }
+
+    /**
+     * Draw an image at the x,y coordinates, with the default size of the image.
+     *
+     * @param image The image to draw.
+     * @param x The x-coordinate to draw the image.
+     * @param y The y-coordinate to draw the image.
+     *
+     * @throws IOException If there is an error writing to the stream.
+     */
+    public void drawImage(PDImageXObject image, float x, float y) throws IOException
+    {
+        drawImage(image, x, y, image.getWidth(), image.getHeight());
+    }
+
+    /**
+     * Draw an image at the x,y coordinates, with the given size.
+     *
+     * @param image The image to draw.
+     * @param x The x-coordinate to draw the image.
+     * @param y The y-coordinate to draw the image.
+     * @param width The width to draw the image.
+     * @param height The height to draw the image.
+     *
+     * @throws IOException If there is an error writing to the stream.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void drawImage(PDImageXObject image, float x, float y, float width, float height) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: drawImage is not allowed within a text block.");
+        }
+
+        saveGraphicsState();
+
+        AffineTransform transform = new AffineTransform(width, 0, 0, height, x, y);
+        transform(new Matrix(transform));
+
+        writeOperand(resources.add(image));
+        writeOperator("Do");
+
+        restoreGraphicsState();
+    }
+
+    /**
+     * Draw an image at the origin with the given transformation matrix.
+     *
+     * @param image The image to draw.
+     * @param matrix The transformation matrix to apply to the image.
+     *
+     * @throws IOException If there is an error writing to the stream.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void drawImage(PDImageXObject image, Matrix matrix) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: drawImage is not allowed within a text block.");
+        }
+
+        saveGraphicsState();
+
+        AffineTransform transform = matrix.createAffineTransform();
+        transform(new Matrix(transform));
+
+        writeOperand(resources.add(image));
+        writeOperator("Do");
+
+        restoreGraphicsState();
+    }
+
+    /**
+     * Draw an inline image at the x,y coordinates, with the default size of the image.
+     *
+     * @param inlineImage The inline image to draw.
+     * @param x The x-coordinate to draw the inline image.
+     * @param y The y-coordinate to draw the inline image.
+     *
+     * @throws IOException If there is an error writing to the stream.
+     */
+    public void drawImage(PDInlineImage inlineImage, float x, float y) throws IOException
+    {
+        drawImage(inlineImage, x, y, inlineImage.getWidth(), inlineImage.getHeight());
+    }
+
+    /**
+     * Draw an inline image at the x,y coordinates and a certain width and height.
+     *
+     * @param inlineImage The inline image to draw.
+     * @param x The x-coordinate to draw the inline image.
+     * @param y The y-coordinate to draw the inline image.
+     * @param width The width of the inline image to draw.
+     * @param height The height of the inline image to draw.
+     *
+     * @throws IOException If there is an error writing to the stream.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void drawImage(PDInlineImage inlineImage, float x, float y, float width, float height) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: drawImage is not allowed within a text block.");
+        }
+
+        saveGraphicsState();
+        transform(new Matrix(width, 0, 0, height, x, y));
+
+        // create the image dictionary
+        StringBuilder sb = new StringBuilder();
+        sb.append("BI");
+
+        sb.append("\n /W ");
+        sb.append(inlineImage.getWidth());
+
+        sb.append("\n /H ");
+        sb.append(inlineImage.getHeight());
+
+        sb.append("\n /CS ");
+        sb.append("/");
+        sb.append(inlineImage.getColorSpace().getName());
+
+        if (inlineImage.getDecode() != null && inlineImage.getDecode().size() > 0)
+        {
+            sb.append("\n /D ");
+            sb.append("[");
+            for (COSBase base : inlineImage.getDecode())
+            {
+                sb.append(((COSNumber) base).intValue());
+                sb.append(" ");
+            }
+            sb.append("]");
+        }
+
+        if (inlineImage.isStencil())
+        {
+            sb.append("\n /IM true");
+        }
+
+        sb.append("\n /BPC ");
+        sb.append(inlineImage.getBitsPerComponent());
+
+        // image dictionary
+        write(sb.toString());
+        writeLine();
+
+        // binary data
+        writeOperator("ID");
+        writeBytes(inlineImage.getData());
+        writeLine();
+        writeOperator("EI");
+
+        restoreGraphicsState();
+    }
+
+    /**
+     * Draws the given Form XObject at the current location.
+     *
+     * @param form Form XObject
+     * @throws IOException if the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void drawForm(PDFormXObject form) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: drawForm is not allowed within a text block.");
+        }
+
+        writeOperand(resources.add(form));
+        writeOperator("Do");
+    }
+
+    /**
+     * The cm operator. Concatenates the given matrix with the CTM.
+     *
+     * @param matrix the transformation matrix
+     * @throws IOException If there is an error writing to the stream.
+     */
+    public void transform(Matrix matrix) throws IOException
+    {
+        writeAffineTransform(matrix.createAffineTransform());
+        writeOperator("cm");
+    }
+
+    /**
+     * q operator. Saves the current graphics state.
+     * @throws IOException If an error occurs while writing to the stream.
+     */
+    public void saveGraphicsState() throws IOException
+    {
+        if (!fontStack.isEmpty())
+        {
+            fontStack.push(fontStack.peek());
+        }
+        if (!strokingColorSpaceStack.isEmpty())
+        {
+            strokingColorSpaceStack.push(strokingColorSpaceStack.peek());
+        }
+        if (!nonStrokingColorSpaceStack.isEmpty())
+        {
+            nonStrokingColorSpaceStack.push(nonStrokingColorSpaceStack.peek());
+        }
+        writeOperator("q");
+    }
+
+    /**
+     * Q operator. Restores the current graphics state.
+     * @throws IOException If an error occurs while writing to the stream.
+     */
+    public void restoreGraphicsState() throws IOException
+    {
+        if (!fontStack.isEmpty())
+        {
+            fontStack.pop();
+        }
+        if (!strokingColorSpaceStack.isEmpty())
+        {
+            strokingColorSpaceStack.pop();
+        }
+        if (!nonStrokingColorSpaceStack.isEmpty())
+        {
+            nonStrokingColorSpaceStack.pop();
+        }
+        writeOperator("Q");
+    }
+
+    private COSName getName(PDColorSpace colorSpace) throws IOException
+    {
+        if (colorSpace instanceof PDDeviceGray ||
+            colorSpace instanceof PDDeviceRGB ||
+            colorSpace instanceof PDDeviceCMYK)
+        {
+            return COSName.getPDFName(colorSpace.getName());
+        }
+        else
+        {
+            return resources.add(colorSpace);
+        }
+    }
+
+    public void setStrokingColor(float[] components) throws IOException
+    {
+        for (float value : components)
+        {
+            writeOperand(value);
+        }
+        
+        int numComponents = components.length;
+        switch(numComponents)
+        {
+            case 1:
+                writeOperator("G");
+                break;
+            case 3:
+                writeOperator("RG");
+                break;
+            case 4:
+                writeOperator("K");
+        }
+    }
+    
+    /**
+     * Sets the stroking color and, if necessary, the stroking color space.
+     *
+     * @param color Color in a specific color space.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     */
+    public void setStrokingColor(PDColor color) throws IOException
+    {
+        if (strokingColorSpaceStack.isEmpty() ||
+            strokingColorSpaceStack.peek() != color.getColorSpace())
+        {
+            writeOperand(getName(color.getColorSpace()));
+            writeOperator("CS");
+            setStrokingColorSpaceStack(color.getColorSpace());
+        }
+
+        for (float value : color.getComponents())
+        {
+            writeOperand(value);
+        }
+
+        if (color.getColorSpace() instanceof PDPattern)
+        {
+            writeOperand(color.getPatternName());
+        }
+
+        if (color.getColorSpace() instanceof PDPattern ||
+            color.getColorSpace() instanceof PDSeparation ||
+            color.getColorSpace() instanceof PDDeviceN ||
+            color.getColorSpace() instanceof PDICCBased)
+        {
+            writeOperator("SCN");
+        }
+        else
+        {
+            writeOperator("SC");
+        }
+    }
+
+    /**
+     * Set the stroking color using an AWT color. Conversion uses the default sRGB color space.
+     *
+     * @param color The color to set.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     */
+    public void setStrokingColor(Color color) throws IOException
+    {
+        float[] components = new float[] {
+                color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f };
+        PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE);
+        setStrokingColor(pdColor);
+    }
+
+    /**
+     * Set the stroking color in the DeviceRGB color space. Range is 0..255.
+     *
+     * @param r The red value
+     * @param g The green value.
+     * @param b The blue value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameters are invalid.
+     */
+    public void setStrokingColor(int r, int g, int b) throws IOException
+    {
+        if (isOutside255Interval(r) || isOutside255Interval(g) || isOutside255Interval(b))
+        {
+            throw new IllegalArgumentException("Parameters must be within 0..255, but are "
+                    + String.format("(%d,%d,%d)", r, g, b));
+        }
+        writeOperand(r / 255f);
+        writeOperand(g / 255f);
+        writeOperand(b / 255f);
+        writeOperator("RG");
+        setStrokingColorSpaceStack(PDDeviceRGB.INSTANCE);
+    }
+
+    /**
+     * Set the stroking color in the DeviceCMYK color space. Range is 0..1
+     *
+     * @param c The cyan value.
+     * @param m The magenta value.
+     * @param y The yellow value.
+     * @param k The black value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameters are invalid.
+     */
+    public void setStrokingColor(float c, float m, float y, float k) throws IOException
+    {
+        if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k))
+        {
+            throw new IllegalArgumentException("Parameters must be within 0..1, but are "
+                    + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k));
+        }
+        writeOperand(c);
+        writeOperand(m);
+        writeOperand(y);
+        writeOperand(k);
+        writeOperator("K");
+        setStrokingColorSpaceStack(PDDeviceCMYK.INSTANCE);
+    }
+
+    /**
+     * Set the stroking color in the DeviceGray color space. Range is 0..1.
+     *
+     * @param g The gray value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameter is invalid.
+     */
+    public void setStrokingColor(double g) throws IOException
+    {
+        if (isOutsideOneInterval(g))
+        {
+            throw new IllegalArgumentException("Parameter must be within 0..1, but is " + g);
+        }
+        writeOperand((float) g);
+        writeOperator("G");
+        setStrokingColorSpaceStack(PDDeviceGray.INSTANCE);
+    }
+
+    /**
+     * Sets the non-stroking color and, if necessary, the non-stroking color space.
+     *
+     * @param color Color in a specific color space.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     */
+    public void setNonStrokingColor(PDColor color) throws IOException
+    {
+        if (nonStrokingColorSpaceStack.isEmpty() ||
+            nonStrokingColorSpaceStack.peek() != color.getColorSpace())
+        {
+            writeOperand(getName(color.getColorSpace()));
+            writeOperator("cs");
+            setNonStrokingColorSpaceStack(color.getColorSpace());
+        }
+
+        for (float value : color.getComponents())
+        {
+            writeOperand(value);
+        }
+
+        if (color.getColorSpace() instanceof PDPattern)
+        {
+            writeOperand(color.getPatternName());
+        }
+
+        if (color.getColorSpace() instanceof PDPattern ||
+            color.getColorSpace() instanceof PDSeparation ||
+            color.getColorSpace() instanceof PDDeviceN ||
+            color.getColorSpace() instanceof PDICCBased)
+        {
+            writeOperator("scn");
+        }
+        else
+        {
+            writeOperator("sc");
+        }
+    }
+
+    /**
+     * Set the non-stroking color using an AWT color. Conversion uses the default sRGB color space.
+     *
+     * @param color The color to set.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     */
+    public void setNonStrokingColor(Color color) throws IOException
+    {
+        float[] components = new float[] {
+                color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f };
+        PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE);
+        setNonStrokingColor(pdColor);
+    }
+
+    /**
+     * Set the non-stroking color in the DeviceRGB color space. Range is 0..255.
+     *
+     * @param r The red value.
+     * @param g The green value.
+     * @param b The blue value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameters are invalid.
+     */
+    public void setNonStrokingColor(int r, int g, int b) throws IOException
+    {
+        if (isOutside255Interval(r) || isOutside255Interval(g) || isOutside255Interval(b))
+        {
+            throw new IllegalArgumentException("Parameters must be within 0..255, but are "
+                    + String.format("(%d,%d,%d)", r, g, b));
+        }
+        writeOperand(r / 255f);
+        writeOperand(g / 255f);
+        writeOperand(b / 255f);
+        writeOperator("rg");
+        setNonStrokingColorSpaceStack(PDDeviceRGB.INSTANCE);
+    }
+
+    /**
+     * Set the non-stroking color in the DeviceCMYK color space. Range is 0..255.
+     *
+     * @param c The cyan value.
+     * @param m The magenta value.
+     * @param y The yellow value.
+     * @param k The black value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameters are invalid.
+     */
+    public void setNonStrokingColor(int c, int m, int y, int k) throws IOException
+    {
+        if (isOutside255Interval(c) || isOutside255Interval(m) || isOutside255Interval(y) || isOutside255Interval(k))
+        {
+            throw new IllegalArgumentException("Parameters must be within 0..255, but are "
+                    + String.format("(%d,%d,%d,%d)", c, m, y, k));
+        }
+        setNonStrokingColor(c / 255f, m / 255f, y / 255f, k / 255f);
+    }
+
+    /**
+     * Set the non-stroking color in the DeviceRGB color space. Range is 0..1.
+     *
+     * @param c The cyan value.
+     * @param m The magenta value.
+     * @param y The yellow value.
+     * @param k The black value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     */
+    public void setNonStrokingColor(double c, double m, double y, double k) throws IOException
+    {
+        if (isOutsideOneInterval(c) || isOutsideOneInterval(m) || isOutsideOneInterval(y) || isOutsideOneInterval(k))
+        {
+            throw new IllegalArgumentException("Parameters must be within 0..1, but are "
+                    + String.format("(%.2f,%.2f,%.2f,%.2f)", c, m, y, k));
+        }
+        writeOperand((float) c);
+        writeOperand((float) m);
+        writeOperand((float) y);
+        writeOperand((float) k);
+        writeOperator("k");
+        setNonStrokingColorSpaceStack(PDDeviceCMYK.INSTANCE);
+    }
+
+    /**
+     * Set the non-stroking color in the DeviceGray color space. Range is 0..255.
+     *
+     * @param g The gray value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameter is invalid.
+     */
+    public void setNonStrokingColor(int g) throws IOException
+    {
+        if (isOutside255Interval(g))
+        {
+            throw new IllegalArgumentException("Parameter must be within 0..255, but is " + g);
+        }
+        setNonStrokingColor(g / 255f);
+    }
+
+    /**
+     * Set the non-stroking color in the DeviceGray color space. Range is 0..1.
+     *
+     * @param g The gray value.
+     * @throws IOException If an IO error occurs while writing to the stream.
+     * @throws IllegalArgumentException If the parameter is invalid.
+     */
+    public void setNonStrokingColor(double g) throws IOException
+    {
+        if (isOutsideOneInterval(g))
+        {
+            throw new IllegalArgumentException("Parameter must be within 0..1, but is " + g);
+        }
+        writeOperand((float) g);
+        writeOperator("g");
+        setNonStrokingColorSpaceStack(PDDeviceGray.INSTANCE);
+    }
+
+    /**
+     * Add a rectangle to the current path.
+     *
+     * @param x The lower left x coordinate.
+     * @param y The lower left y coordinate.
+     * @param width The width of the rectangle.
+     * @param height The height of the rectangle.
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void addRect(float x, float y, float width, float height) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: addRect is not allowed within a text block.");
+        }
+        writeOperand(x);
+        writeOperand(y);
+        writeOperand(width);
+        writeOperand(height);
+        writeOperator("re");
+    }
+
+    /**
+     * Append a cubic Bézier curve to the current path. The curve extends from the current point to
+     * the point (x3, y3), using (x1, y1) and (x2, y2) as the Bézier control points.
+     *
+     * @param x1 x coordinate of the point 1
+     * @param y1 y coordinate of the point 1
+     * @param x2 x coordinate of the point 2
+     * @param y2 y coordinate of the point 2
+     * @param x3 x coordinate of the point 3
+     * @param y3 y coordinate of the point 3
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: curveTo is not allowed within a text block.");
+        }
+        writeOperand(x1);
+        writeOperand(y1);
+        writeOperand(x2);
+        writeOperand(y2);
+        writeOperand(x3);
+        writeOperand(y3);
+        writeOperator("c");
+    }
+
+    /**
+     * Append a cubic Bézier curve to the current path. The curve extends from the current point to
+     * the point (x3, y3), using the current point and (x2, y2) as the Bézier control points.
+     *
+     * @param x2 x coordinate of the point 2
+     * @param y2 y coordinate of the point 2
+     * @param x3 x coordinate of the point 3
+     * @param y3 y coordinate of the point 3
+     * @throws IllegalStateException If the method was called within a text block.
+     * @throws IOException If the content stream could not be written.
+     */
+    public void curveTo2(float x2, float y2, float x3, float y3) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: curveTo2 is not allowed within a text block.");
+        }
+        writeOperand(x2);
+        writeOperand(y2);
+        writeOperand(x3);
+        writeOperand(y3);
+        writeOperator("v");
+    }
+
+    /**
+     * Append a cubic Bézier curve to the current path. The curve extends from the current point to
+     * the point (x3, y3), using (x1, y1) and (x3, y3) as the Bézier control points.
+     *
+     * @param x1 x coordinate of the point 1
+     * @param y1 y coordinate of the point 1
+     * @param x3 x coordinate of the point 3
+     * @param y3 y coordinate of the point 3
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void curveTo1(float x1, float y1, float x3, float y3) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: curveTo1 is not allowed within a text block.");
+        }
+        writeOperand(x1);
+        writeOperand(y1);
+        writeOperand(x3);
+        writeOperand(y3);
+        writeOperator("y");
+    }
+
+    /**
+     * Move the current position to the given coordinates.
+     *
+     * @param x The x coordinate.
+     * @param y The y coordinate.
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void moveTo(float x, float y) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: moveTo is not allowed within a text block.");
+        }
+        writeOperand(x);
+        writeOperand(y);
+        writeOperator("m");
+    }
+
+    /**
+     * Draw a line from the current position to the given coordinates.
+     *
+     * @param x The x coordinate.
+     * @param y The y coordinate.
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void lineTo(float x, float y) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: lineTo is not allowed within a text block.");
+        }
+        writeOperand(x);
+        writeOperand(y);
+        writeOperator("l");
+    }
+
+    /**
+     * Stroke the path.
+     * 
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void stroke() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: stroke is not allowed within a text block.");
+        }
+        writeOperator("S");
+    }
+
+    /**
+     * Close and stroke the path.
+     * 
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void closeAndStroke() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: closeAndStroke is not allowed within a text block.");
+        }
+        writeOperator("s");
+    }
+
+    /**
+     * Fills the path using the nonzero winding number rule.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void fill() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: fill is not allowed within a text block.");
+        }
+        writeOperator("f");
+    }
+
+    /**
+     * Fills the path using the even-odd winding rule.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void fillEvenOdd() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: fillEvenOdd is not allowed within a text block.");
+        }
+        writeOperator("f*");
+    }
+
+    /**
+     * Fill and then stroke the path, using the nonzero winding number rule to determine the region
+     * to fill. This shall produce the same result as constructing two identical path objects,
+     * painting the first with {@link #fill() } and the second with {@link #stroke() }.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void fillAndStroke() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: fillAndStroke is not allowed within a text block.");
+        }
+        writeOperator("B");
+    }
+
+    /**
+     * Fill and then stroke the path, using the even-odd rule to determine the region to
+     * fill. This shall produce the same result as constructing two identical path objects, painting
+     * the first with {@link #fillEvenOdd() } and the second with {@link #stroke() }.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void fillAndStrokeEvenOdd() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: fillAndStrokeEvenOdd is not allowed within a text block.");
+        }
+        writeOperator("B*");
+    }
+
+    /**
+     * Close, fill, and then stroke the path, using the nonzero winding number rule to determine the
+     * region to fill. This shall have the same effect as the sequence {@link #closePath() }
+     * and then {@link #fillAndStroke() }.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void closeAndFillAndStroke() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: closeAndFillAndStroke is not allowed within a text block.");
+        }
+        writeOperator("b");
+    }
+
+    /**
+     * Close, fill, and then stroke the path, using the even-odd rule to determine the region to
+     * fill. This shall have the same effect as the sequence {@link #closePath() }
+     * and then {@link #fillAndStrokeEvenOdd() }.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void closeAndFillAndStrokeEvenOdd() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: closeAndFillAndStrokeEvenOdd is not allowed within a text block.");
+        }
+        writeOperator("b*");
+    }
+
+    /**
+     * Fills the clipping area with the given shading.
+     *
+     * @param shading Shading resource
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void shadingFill(PDShading shading) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: shadingFill is not allowed within a text block.");
+        }
+
+        writeOperand(resources.add(shading));
+        writeOperator("sh");
+    }
+
+    /**
+     * Closes the current subpath.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void closePath() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: closePath is not allowed within a text block.");
+        }
+        writeOperator("h");
+    }
+
+    /**
+     * Intersects the current clipping path with the current path, using the nonzero rule.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void clip() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: clip is not allowed within a text block.");
+        }
+        writeOperator("W");
+        
+        // end path without filling or stroking
+        writeOperator("n");
+    }
+
+    /**
+     * Intersects the current clipping path with the current path, using the even-odd rule.
+     *
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void clipEvenOdd() throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: clipEvenOdd is not allowed within a text block.");
+        }
+        writeOperator("W*");
+        
+        // end path without filling or stroking
+        writeOperator("n");
+    }
+
+    /**
+     * Set line width to the given value.
+     *
+     * @param lineWidth The width which is used for drwaing.
+     * @throws IOException If the content stream could not be written
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void setLineWidth(float lineWidth) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: setLineWidth is not allowed within a text block.");
+        }
+        writeOperand(lineWidth);
+        writeOperator("w");
+    }
+
+    /**
+     * Set the line join style.
+     *
+     * @param lineJoinStyle 0 for miter join, 1 for round join, and 2 for bevel join.
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     * @throws IllegalArgumentException If the parameter is not a valid line join style.
+     */
+    public void setLineJoinStyle(int lineJoinStyle) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: setLineJoinStyle is not allowed within a text block.");
+        }
+        if (lineJoinStyle >= 0 && lineJoinStyle <= 2)
+        {
+            writeOperand(lineJoinStyle);
+            writeOperator("j");
+        }
+        else
+        {
+            throw new IllegalArgumentException("Error: unknown value for line join style");
+        }
+    }
+
+    /**
+     * Set the line cap style.
+     *
+     * @param lineCapStyle 0 for butt cap, 1 for round cap, and 2 for projecting square cap.
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     * @throws IllegalArgumentException If the parameter is not a valid line cap style.
+     */
+    public void setLineCapStyle(int lineCapStyle) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: setLineCapStyle is not allowed within a text block.");
+        }
+        if (lineCapStyle >= 0 && lineCapStyle <= 2)
+        {
+            writeOperand(lineCapStyle);
+            writeOperator("J");
+        }
+        else
+        {
+            throw new IllegalArgumentException("Error: unknown value for line cap style");
+        }
+    }
+
+    /**
+     * Set the line dash pattern.
+     *
+     * @param pattern The pattern array
+     * @param phase The phase of the pattern
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalStateException If the method was called within a text block.
+     */
+    public void setLineDashPattern(float[] pattern, float phase) throws IOException
+    {
+        if (inTextMode)
+        {
+            throw new IllegalStateException("Error: setLineDashPattern is not allowed within a text block.");
+        }
+        write("[");
+        for (float value : pattern)
+        {
+            writeOperand(value);
+        }
+        write("] ");
+        writeOperand(phase);
+        writeOperator("d");
+    }
+
+    /**
+     * Begin a marked content sequence.
+     *
+     * @param tag the tag
+     * @throws IOException If the content stream could not be written
+     */
+    public void beginMarkedContent(COSName tag) throws IOException
+    {
+        writeOperand(tag);
+        writeOperator("BMC");
+    }
+
+    /**
+     * Begin a marked content sequence with a reference to an entry in the page resources'
+     * Properties dictionary.
+     *
+     * @param tag the tag
+     * @param propertyList property list
+     * @throws IOException If the content stream could not be written
+     */
+    public void beginMarkedContent(COSName tag, PDPropertyList propertyList) throws IOException
+    {
+        writeOperand(tag);
+        writeOperand(resources.add(propertyList));
+        writeOperator("BDC");
+    }
+
+    /**
+     * End a marked content sequence.
+     *
+     * @throws IOException If the content stream could not be written
+     */
+    public void endMarkedContent() throws IOException
+    {
+        writeOperator("EMC");
+    }
+
+    /**
+     * Set an extended graphics state.
+     * 
+     * @param state The extended graphics state.
+     * @throws IOException If the content stream could not be written.
+     */
+    public void setGraphicsStateParameters(PDExtendedGraphicsState state) throws IOException
+    {
+        writeOperand(resources.add(state));
+        writeOperator("gs");
+    }
+
+    /**
+     * Write a comment line.
+     *
+     * @param comment
+     * @throws IOException If the content stream could not be written.
+     * @throws IllegalArgumentException If the comment contains a newline. This is not allowed,
+     * because the next line could be ordinary PDF content.
+     */
+    public void addComment(String comment) throws IOException
+    {
+        if (comment.indexOf('\n') >= 0 || comment.indexOf('\r') >= 0)
+        {
+            throw new IllegalArgumentException("comment should not include a newline");
+        }
+        output.write('%');
+        output.write(comment.getBytes(Charsets.US_ASCII));
+        output.write('\n');
+    }
+
+    /**
+     * Writes a real real to the content stream.
+     */
+    private void writeOperand(float real) throws IOException
+    {
+        write(formatDecimal.format(real));
+        output.write(' ');
+    }
+
+    /**
+     * Writes a real number to the content stream.
+     */
+    private void writeOperand(int integer) throws IOException
+    {
+        write(formatDecimal.format(integer));
+        output.write(' ');
+    }
+
+    /**
+     * Writes a COSName to the content stream.
+     */
+    private void writeOperand(COSName name) throws IOException
+    {
+        name.writePDF(output);
+        output.write(' ');
+    }
+
+    /**
+     * Writes a string to the content stream as ASCII.
+     */
+    private void writeOperator(String text) throws IOException
+    {
+        output.write(text.getBytes(Charsets.US_ASCII));
+        output.write('\n');
+    }
+
+    /**
+     * Writes a string to the content stream as ASCII.
+     */
+    private void write(String text) throws IOException
+    {
+        output.write(text.getBytes(Charsets.US_ASCII));
+    }
+
+    /**
+     * Writes a string to the content stream as ASCII.
+     */
+    private void writeLine() throws IOException
+    {
+        output.write('\n');
+    }
+
+    /**
+     * Writes binary data to the content stream.
+     */
+    private void writeBytes(byte[] data) throws IOException
+    {
+        output.write(data);
+    }
+
+    /**
+     * Writes an AffineTransform to the content stream as an array.
+     */
+    private void writeAffineTransform(AffineTransform transform) throws IOException
+    {
+        double[] values = new double[6];
+        transform.getMatrix(values);
+        for (double v : values)
+        {
+            writeOperand((float) v);
+        }
+    }
+
+    /**
+     * Close the content stream.  This must be called when you are done with this object.
+     *
+     * @throws IOException If the underlying stream has a problem being written to.
+     */
+    @Override
+    public void close() throws IOException
+    {
+        output.close();
+    }
+
+    private boolean isOutside255Interval(int val)
+    {
+        return val < 0 || val > 255;
+    }
+
+    private boolean isOutsideOneInterval(double val)
+    {
+        return val < 0 || val > 1;
+    }
+
+    private void setStrokingColorSpaceStack(PDColorSpace colorSpace)
+    {
+        if (strokingColorSpaceStack.isEmpty())
+        {
+            strokingColorSpaceStack.add(colorSpace);
+        }
+        else
+        {
+            strokingColorSpaceStack.setElementAt(colorSpace, strokingColorSpaceStack.size() - 1);
+        }
+    }
+
+    private void setNonStrokingColorSpaceStack(PDColorSpace colorSpace)
+    {
+        if (nonStrokingColorSpaceStack.isEmpty())
+        {
+            nonStrokingColorSpaceStack.add(colorSpace);
+        }
+        else
+        {
+            nonStrokingColorSpaceStack.setElementAt(colorSpace, nonStrokingColorSpaceStack.size() - 1);
+        }
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDAppearanceContentStream.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java?rev=1745013&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java (added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java Sun May 22 06:48:34 2016
@@ -0,0 +1,177 @@
+/*
+ * 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.interactive.annotation.handlers;
+
+import org.apache.pdfbox.cos.COSStream;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
+
+/**
+ * Generic handler to generate the fields appearance.
+ * 
+ * Individual handler will provide specific implementations for different field
+ * types.
+ * 
+ */
+public abstract class PDAppearanceHandler
+{
+
+    private PDAnnotation annotation;
+
+    public PDAppearanceHandler(PDAnnotation annotation)
+    {
+        this.annotation = annotation;
+
+    }
+
+    public void generateAppearanceStreams()
+    {
+        if (annotation.getRectangle() != null)
+        {
+            generateNormalAppearance();
+            generateRolloverAppearance();
+            generateDownAppearance();
+        }
+    }
+
+    public abstract void generateNormalAppearance();
+
+    public abstract void generateRolloverAppearance();
+
+    public abstract void generateDownAppearance();
+
+    PDAnnotation getAnnotation()
+    {
+        return annotation;
+    }
+
+    /**
+     * Get the line with of the border.
+     * 
+     * Get the width of the line used to draw a border around the annotation.
+     * This may either be specified by the annotation dictionaries Border
+     * setting or by the W entry in the BS border style dictionary. If both are
+     * missing the default width is 1.
+     * 
+     * @return the line width
+     */
+    // TODO: according to the PDF spec the use of the BS entry is annotation
+    // specific
+    // so we will leave that to be implemented by individual handlers.
+    // If at the end all annotations support the BS entry this can be handled
+    // here and removed from the individual handlers.
+    abstract float getLineWidth();
+    
+    PDColor getColor()
+    {
+        return annotation.getColor();
+    }
+    
+    PDRectangle getRectangle()
+    {
+        return annotation.getRectangle();
+    }
+
+    /**
+     * Get the annotations appearance dictionary.
+     * 
+     * <p>This will get the annotations appearance dictionary. If this is
+     * not existent an empty appearance dictionary will be created.
+     * 
+     * @return the annotations appearance dictionary
+     */
+    PDAppearanceDictionary getAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = annotation.getAppearance();
+        if (appearanceDictionary == null)
+        {
+            appearanceDictionary = new PDAppearanceDictionary();
+            annotation.setAppearance(appearanceDictionary);
+        }
+        return appearanceDictionary;
+    }
+
+    /**
+     * Get the annotations normal appearance.
+     * 
+     * <p>This will get the annotations normal appearance. If this is
+     * not existent an empty appearance entry will be created.
+     * 
+     * @return the appearance entry representing the normal appearance.
+     */
+    PDAppearanceEntry getNormalAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = getAppearance();
+        PDAppearanceEntry appearanceEntry = appearanceDictionary.getNormalAppearance();
+
+        if (appearanceEntry.isSubDictionary())
+        {
+           appearanceEntry = new PDAppearanceEntry(new COSStream());
+           appearanceDictionary.setNormalAppearance(appearanceEntry);
+        }
+
+        return appearanceEntry;
+    }
+    
+    /**
+     * Get the annotations down appearance.
+     * 
+     * <p>This will get the annotations down appearance. If this is
+     * not existent an empty appearance entry will be created.
+     * 
+     * @return the appearance entry representing the down appearance.
+     */
+    PDAppearanceEntry getDownAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = getAppearance();
+        PDAppearanceEntry appearanceEntry = appearanceDictionary.getDownAppearance();
+        
+        if (appearanceEntry.isSubDictionary())
+        {
+            appearanceEntry = new PDAppearanceEntry(new COSStream());
+            appearanceDictionary.setDownAppearance(appearanceEntry);
+        }
+
+        return appearanceEntry;
+    }
+
+    /**
+     * Get the annotations rollover appearance.
+     * 
+     * <p>This will get the annotations rollover appearance. If this is
+     * not existent an empty appearance entry will be created.
+     * 
+     * @return the appearance entry representing the rollover appearance.
+     */
+    PDAppearanceEntry getRolloverAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = getAppearance();
+        PDAppearanceEntry appearanceEntry = appearanceDictionary.getDownAppearance();
+
+        if (appearanceEntry.isSubDictionary())
+        {
+            appearanceEntry = new PDAppearanceEntry(new COSStream());
+            appearanceDictionary.setRolloverAppearance(appearanceEntry);
+        }
+
+        return appearanceEntry;
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAppearanceHandler.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java?rev=1745013&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java (added)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java Sun May 22 06:48:34 2016
@@ -0,0 +1,114 @@
+/*
+ * 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.interactive.annotation.handlers;
+
+
+import java.awt.geom.AffineTransform;
+import java.io.IOException;
+
+import org.apache.pdfbox.cos.COSArray;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationSquareCircle;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceContentStream;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
+
+/**
+ * Handler to generate the square annotations appearance.
+ *
+ */
+public class PDSquareAppearanceHandler extends PDAppearanceHandler
+{
+    public PDSquareAppearanceHandler(PDAnnotation annotation)
+    {
+        super(annotation);
+    }
+
+    @Override
+    public void generateNormalAppearance()
+    {
+        PDAppearanceEntry appearanceEntry = getNormalAppearance();
+        PDAppearanceStream appearanceStream = appearanceEntry.getAppearanceStream();
+        try
+        {
+            PDAppearanceContentStream contentStream = new PDAppearanceContentStream(appearanceStream);
+            PDRectangle bbox = getRectangle();
+            appearanceStream.setBBox(bbox);
+            AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(), -bbox.getLowerLeftY());
+            appearanceStream.setMatrix(transform);
+            contentStream.setStrokingColor(getColor().getComponents());
+            contentStream.addRect(bbox.getLowerLeftX()+1f, bbox.getLowerLeftY()+1f, bbox.getWidth()-2f, bbox.getHeight()-2f);
+            
+            contentStream.stroke();
+            contentStream.close();
+        } catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void generateRolloverAppearance()
+    {
+        // TODO to be implemented
+    }
+
+    @Override
+    public void generateDownAppearance()
+    {
+        // TODO to be implemented
+    }
+    
+    /**
+     * Get the line with of the border.
+     * 
+     * Get the width of the line used to draw a border around
+     * the annotation. This may either be specified by the annotation
+     * dictionaries Border setting or by the W entry in the BS border
+     * style dictionary. If both are missing the default width is 1.
+     * 
+     * @return the line width
+     */
+    // TODO: according to the PDF spec the use of the BS entry is annotation specific
+    //       so we will leave that to be implemented by individual handlers.
+    //       If at the end all annotations support the BS entry this can be handled
+    //       here and removed from the individual handlers.
+    public float getLineWidth()
+    {
+        PDAnnotationSquareCircle annotation = (PDAnnotationSquareCircle) getAnnotation();
+        
+        PDBorderStyleDictionary bs = annotation.getBorderStyle();
+        
+        if (bs != null)
+        {
+            return bs.getWidth();
+        }
+        else
+        {
+            COSArray borderCharacteristics = annotation.getBorder();
+            if (borderCharacteristics != null && borderCharacteristics.size() >= 3)
+            {
+                return borderCharacteristics.getInt(3);
+            }
+        }
+        
+        return 1;
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDSquareAppearanceHandler.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java?rev=1745013&view=auto
==============================================================================
--- pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java (added)
+++ pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java Sun May 22 06:48:34 2016
@@ -0,0 +1,172 @@
+/*
+ * 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.interactive.annotation;
+
+import org.apache.pdfbox.contentstream.operator.Operator;
+import org.apache.pdfbox.cos.COSFloat;
+import org.apache.pdfbox.cos.COSInteger;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdfparser.PDFStreamParser;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.common.PDStream;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
+import org.apache.pdfbox.util.Matrix;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Test for the PDAnnotation classes.
+ *
+ */
+public class PDSquareAnnotationTest
+{
+
+    // delta for comparing equality of float values
+    private static final double DELTA = 1e-4;
+    
+    // the location of the annotation
+    static PDRectangle rectangle;
+    
+    private static final File OUT_DIR = new File("target/test-output");
+    private static final String NAME_OF_PDF = "PDSquareAnnotationTest.pdf";
+
+    @Before
+    public void setUp() throws IOException
+    {
+        rectangle = new PDRectangle();
+        rectangle.setLowerLeftX(91.5958f);
+        rectangle.setLowerLeftY(741.91f);
+        rectangle.setUpperRightX(113.849f);
+        rectangle.setUpperRightY(757.078f);
+    }
+
+    @Test
+    public void createDefaultSquareAnnotation()
+    {
+        PDAnnotation annotation = new PDAnnotationSquareCircle(PDAnnotationSquareCircle.SUB_TYPE_SQUARE);
+        assertEquals(COSName.ANNOT, annotation.getCOSObject().getItem(COSName.TYPE));
+        assertEquals(PDAnnotationSquareCircle.SUB_TYPE_SQUARE,
+                annotation.getCOSObject().getNameAsString(COSName.SUBTYPE));
+    }
+
+    @Test
+    public void createWithAppearance() throws IOException
+    {
+        // the width of the <nnotations border
+        final int borderWidth = 1;
+
+        PDDocument document = new PDDocument();
+        PDPage page = new PDPage();
+        document.addPage(page);
+        List<PDAnnotation> annotations = page.getAnnotations();
+
+        PDAnnotationSquareCircle annotation = new PDAnnotationSquareCircle(PDAnnotationSquareCircle.SUB_TYPE_SQUARE);
+
+        PDBorderStyleDictionary borderThin = new PDBorderStyleDictionary();
+        borderThin.setWidth(borderWidth);
+
+        PDColor red = new PDColor(new float[] { 1, 0, 0 }, PDDeviceRGB.INSTANCE);
+        annotation.setContents("Square Annotation");
+        annotation.setColor(red);
+        annotation.setBorderStyle(borderThin);
+
+
+        annotation.setRectangle(rectangle);
+
+        annotation.constructAppearances();
+        annotations.add(annotation);
+        
+        // store for validation test
+        File file = new File(OUT_DIR, NAME_OF_PDF);
+        document.save(file);
+        document.close();
+    }
+    
+    @Test
+    public void validateAppearance() throws IOException
+    {
+        // the width of the <nnotations border
+        final int borderWidth = 1;
+
+        File file = new File(OUT_DIR, NAME_OF_PDF);
+        PDDocument document = PDDocument.load(file);
+        PDPage page = document.getPage(0);
+        List<PDAnnotation> annotations = page.getAnnotations();
+
+        PDAnnotationSquareCircle annotation = (PDAnnotationSquareCircle) annotations.get(0);
+
+        PDRectangle rectangle = new PDRectangle();
+        rectangle.setLowerLeftX(91.5958f);
+        rectangle.setLowerLeftY(741.91f);
+        rectangle.setUpperRightX(113.849f);
+        rectangle.setUpperRightY(757.078f);
+
+        // test the correct setting of the appearance stream
+        assertNotNull("Appearance dictionary shall not be null", annotation.getAppearance());
+        assertNotNull("Normal appearance shall not be null", annotation.getAppearance().getNormalAppearance());
+        PDAppearanceStream appearanceStream = annotation.getAppearance().getNormalAppearance().getAppearanceStream();
+        assertNotNull("Appearance stream shall not be null", appearanceStream);
+        assertEquals(rectangle.getLowerLeftX(), appearanceStream.getBBox().getLowerLeftX(), DELTA);
+        assertEquals(rectangle.getLowerLeftY(), appearanceStream.getBBox().getLowerLeftY(), DELTA);
+        assertEquals(rectangle.getWidth(), appearanceStream.getBBox().getWidth(), DELTA);
+        assertEquals(rectangle.getHeight(), appearanceStream.getBBox().getHeight(), DELTA);
+
+        Matrix matrix = appearanceStream.getMatrix();
+        assertNotNull("Matrix shall not be null", matrix);
+
+        // should have been translated to a 0 origin
+        assertEquals(-rectangle.getLowerLeftX(), matrix.getTranslateX(), DELTA);
+        assertEquals(-rectangle.getLowerLeftY(), matrix.getTranslateY(), DELTA);
+
+        // test the content of the apperance stream
+        PDStream contentStream = appearanceStream.getContentStream();
+        assertNotNull("Content stream shall not be null", contentStream);
+        PDFStreamParser parser = new PDFStreamParser(appearanceStream);
+        parser.parse();
+        List<Object> tokens = parser.getTokens();
+
+        // the samples content stream should contain 10 tokens
+        assertEquals(10, tokens.size());
+
+        // setting of the stroking color
+        assertEquals(1, ((COSInteger) tokens.get(0)).intValue());
+        assertEquals(0, ((COSInteger) tokens.get(1)).intValue());
+        assertEquals(0, ((COSInteger) tokens.get(2)).intValue());
+        assertEquals("RG", ((Operator) tokens.get(3)).getName());
+
+        // setting of the rectangle for the border
+        // it shall be inset by the border width
+        assertEquals(rectangle.getLowerLeftX() + borderWidth, ((COSFloat) tokens.get(4)).floatValue(), DELTA);
+        assertEquals(rectangle.getLowerLeftY() + borderWidth, ((COSFloat) tokens.get(5)).floatValue(), DELTA);
+        assertEquals(rectangle.getWidth() - 2 * borderWidth, ((COSFloat) tokens.get(6)).floatValue(), DELTA);
+        assertEquals(rectangle.getHeight() - 2 * borderWidth, ((COSFloat) tokens.get(7)).floatValue(), DELTA);
+        assertEquals("re", ((Operator) tokens.get(8)).getName());
+        assertEquals("S", ((Operator) tokens.get(9)).getName());
+        
+        document.close();
+    }
+}

Propchange: pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: pdfbox/trunk/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/interactive/annotation/PDSquareAnnotationTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain