You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ti...@apache.org on 2019/01/25 20:30:41 UTC

svn commit: r1852176 - in /pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf: FDFAnnotationStamp.java FDFStampAnnotationAppearance.java

Author: tilman
Date: Fri Jan 25 20:30:41 2019
New Revision: 1852176

URL: http://svn.apache.org/viewvc?rev=1852176&view=rev
Log:
PDFBOX-4437: parse XML from xfdf for stamp annotation /AP, by Andrew Hung

Added:
    pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFStampAnnotationAppearance.java   (with props)
Modified:
    pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java

Modified: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java?rev=1852176&r1=1852175&r2=1852176&view=diff
==============================================================================
--- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java (original)
+++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFAnnotationStamp.java Fri Jan 25 20:30:41 2019
@@ -16,19 +16,37 @@
  */
 package org.apache.pdfbox.pdmodel.fdf;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
 import org.apache.pdfbox.cos.COSDictionary;
 import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.util.Hex;
+
+import org.w3c.dom.Document;
 import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
 
 /**
  * This represents a Stamp FDF annotation.
  *
  * @author Ben Litchfield
+ * @author Andrew Hung
  */
 public class FDFAnnotationStamp extends FDFAnnotation
 {
+    private static final Log LOG = LogFactory.getLog(FDFAnnotationStamp.class);
+
     /**
      * COS Model value for SubType entry.
      */
@@ -39,7 +57,6 @@ public class FDFAnnotationStamp extends
      */
     public FDFAnnotationStamp()
     {
-        super();
         annot.setName(COSName.SUBTYPE, SUBTYPE);
     }
 
@@ -64,5 +81,74 @@ public class FDFAnnotationStamp extends
     {
         super(element);
         annot.setName(COSName.SUBTYPE, SUBTYPE);
+
+        // PDFBOX-4437: Initialize the Stamp appearance from the XFDF
+        // https://www.immagic.com/eLibrary/ARCHIVES/TECH/ADOBE/A070914X.pdf
+        // appearance is only defined for stamps
+        XPath xpath = XPathFactory.newInstance().newXPath();
+
+        // Set the Appearance to the annotation
+        LOG.debug("Get the DOM Document for the stamp appearance");
+        String base64EncodedAppearance;
+        try
+        {
+            base64EncodedAppearance = xpath.evaluate("appearance", element);
+        }
+        catch (XPathExpressionException e)
+        {
+            // should not happen
+            LOG.error("Error while evaluating XPath expression for appearance: " + e);
+            return;
+        }
+        byte[] decodedAppearanceXML;
+        try
+        {
+            decodedAppearanceXML = Hex.decodeBase64(base64EncodedAppearance);
+        }
+        catch (IllegalArgumentException ex)
+        {
+            LOG.error("Bad base64 encoded appearance ignored", ex);
+            return;
+        }
+        if (base64EncodedAppearance != null && !base64EncodedAppearance.isEmpty())
+        {
+            Document stampAppearance = getStampAppearanceDocument(decodedAppearanceXML);
+
+            Element appearanceEl = stampAppearance.getDocumentElement();
+
+            // Is the root node have tag as DICT, error otherwise
+            if (!"dict".equalsIgnoreCase(appearanceEl.getNodeName()))
+            {
+                throw new IOException("Error while reading stamp document, "
+                        + "root should be 'dict' and not '" + appearanceEl.getNodeName() + "'");
+            }
+            LOG.debug("Generate and set the appearance dictionary to the stamp annotation");
+            annot.setItem(COSName.AP, new FDFStampAnnotationAppearance(appearanceEl));
+        }
     }
+
+    /**
+     * Parse the <param>xmlString</param> to DOM Document tree from XML content
+     */
+    private Document getStampAppearanceDocument(byte[] xml) throws IOException
+    {
+        try
+        {
+            // Obtain DOM Document instance and create DocumentBuilder with default configuration
+            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+
+            // Parse the content to Document object
+            return builder.parse(new ByteArrayInputStream(xml));
+        }
+        catch (ParserConfigurationException ex)
+        {
+            LOG.error("Error while converting appearance xml to document: " + ex);
+            throw new IOException(ex);
+        }
+        catch (SAXException ex)
+        {
+            LOG.error("Error while converting appearance xml to document: " + ex);
+            throw new IOException(ex);
+        }
+    }    
 }

Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFStampAnnotationAppearance.java
URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFStampAnnotationAppearance.java?rev=1852176&view=auto
==============================================================================
--- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFStampAnnotationAppearance.java (added)
+++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFStampAnnotationAppearance.java Fri Jan 25 20:30:41 2019
@@ -0,0 +1,305 @@
+/*
+ * 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.fdf;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.apache.pdfbox.cos.COSArray;
+import org.apache.pdfbox.cos.COSDictionary;
+import org.apache.pdfbox.cos.COSFloat;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.cos.COSStream;
+import org.apache.pdfbox.pdmodel.common.COSObjectable;
+import org.apache.pdfbox.util.Hex;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Parse the XML for the /AP entry tree.
+ *
+ * @author Andrew Hung
+ */
+class FDFStampAnnotationAppearance implements COSObjectable
+{
+
+    private static final Log LOG = LogFactory.getLog(FDFStampAnnotationAppearance.class);
+
+    private COSDictionary dictionary;
+
+    /**
+     * Default constructor
+     */
+    FDFStampAnnotationAppearance()
+    {
+        dictionary = new COSDictionary();
+        // the N entry is required.
+        dictionary.setItem(COSName.N, new COSStream());
+    }
+
+    /**
+     * Constructor for reading.
+     *
+     * @param dictionary The annotations dictionary.
+     */
+    FDFStampAnnotationAppearance(COSDictionary dictionary)
+    {
+        this.dictionary = dictionary;
+    }
+
+    /**
+     * This will create an Appearance dictionary from an appearance XML document.
+     *
+     * @param fdfXML The XML document that contains the appearance data.
+     */
+    FDFStampAnnotationAppearance(Element appearanceXML) throws IOException
+    {
+        this();
+        LOG.debug("Build dictionary for Appearance based on the appearanceXML");
+
+        NodeList nodeList = appearanceXML.getChildNodes();
+        Node node = null;
+        Element child = null;
+        String parentAttrKey = appearanceXML.getAttribute("KEY");
+        LOG.debug("Appearance Root - tag: " + appearanceXML.getTagName() + ", name: " + 
+                appearanceXML.getNodeName() + ", key: " + parentAttrKey + ", children: " + 
+                nodeList.getLength());
+
+        // Currently only handles Appearance dictionary (AP key on the root)
+        if ("AP".equals(appearanceXML.getAttribute("KEY")))
+        {
+            for (int i = 0; i < nodeList.getLength(); i++)
+            {
+                node = nodeList.item(i);
+                if (node instanceof Element)
+                {
+                    child = (Element) node;
+                    if ("STREAM".equalsIgnoreCase(child.getTagName()))
+                    {
+                        LOG.debug(parentAttrKey +
+                                " => Process " + child.getAttribute("KEY") + 
+                                " item in the dictionary after processing the " + 
+                                child.getTagName());
+                        dictionary.setItem(child.getAttribute("KEY"), parseStreamElement(child));
+                        LOG.debug(parentAttrKey + " => Set " + child.getAttribute("KEY"));
+                    }
+                    else
+                    {
+                        LOG.warn(parentAttrKey + " => Not handling element: " + child.getTagName());
+                    }
+                }
+            }
+        }
+        else
+        {
+            LOG.warn(parentAttrKey + " => Not handling element: " + appearanceXML.getTagName() + 
+                                     " with key: " + appearanceXML.getAttribute("KEY"));
+        }
+    }
+
+    @Override
+    public COSDictionary getCOSObject()
+    {
+        return dictionary;
+    }
+
+    private COSStream parseStreamElement(Element streamEl) throws IOException
+    {
+        COSStream stream = new COSStream();
+
+        NodeList nodeList = streamEl.getChildNodes();
+        Node node;
+        Element child;
+        String childAttrKey;
+        String childAttrVal;
+        String parentAttrKey = streamEl.getAttribute("KEY");
+
+        for (int i = 0; i < nodeList.getLength(); i++)
+        {
+            node = nodeList.item(i);
+            if (node instanceof Element)
+            {
+                child = (Element) node;
+                childAttrKey = child.getAttribute("KEY");
+                childAttrVal = child.getAttribute("VAL");
+                LOG.debug(parentAttrKey + " => reading child: " + child.getTagName() +
+                           " with key: " + childAttrKey);
+                if ("INT".equalsIgnoreCase(child.getTagName()))
+                {
+                    if (!"Length".equals(childAttrKey))
+                    {
+                        stream.setInt(COSName.getPDFName(childAttrKey), Integer.parseInt(childAttrVal));
+                        LOG.debug(parentAttrKey + " => Set " + childAttrKey + ": " + childAttrVal);
+                    }
+                }
+                else if ("NAME".equalsIgnoreCase(child.getTagName()))
+                {
+                    stream.setName(COSName.getPDFName(childAttrKey), childAttrVal);
+                    LOG.debug(parentAttrKey + " => Set " + childAttrKey + ": " + childAttrVal);
+                }
+                else if ("BOOL".equalsIgnoreCase(child.getTagName()))
+                {
+                    stream.setBoolean(COSName.getPDFName(childAttrKey), Boolean.parseBoolean(childAttrVal));
+                    LOG.debug(parentAttrKey + " => Set Interpolate: " + childAttrVal);
+                }
+                else if ("ARRAY".equalsIgnoreCase(child.getTagName()))
+                {
+                    stream.setItem(COSName.getPDFName(childAttrKey), parseArrayElement(child));
+                    LOG.debug(parentAttrKey + " => Set " + childAttrKey);
+                }
+                else if ("DICT".equalsIgnoreCase(child.getTagName()))
+                {
+                    stream.setItem(COSName.getPDFName(childAttrKey), parseDictElement(child));
+                    LOG.debug(parentAttrKey + " => Set " + childAttrKey);
+                }
+                else if ("STREAM".equalsIgnoreCase(child.getTagName()))
+                {
+                    stream.setItem(COSName.getPDFName(childAttrKey), parseStreamElement(child));
+                    LOG.debug(parentAttrKey + " => Set " + childAttrKey);
+                }
+                else if ("DATA".equalsIgnoreCase(child.getTagName()))
+                {
+                    LOG.debug(parentAttrKey + " => Handling DATA with encoding: " +
+                            child.getAttribute("ENCODING"));
+                    if ("HEX".equals(child.getAttribute("ENCODING")))
+                    {
+                        OutputStream os = null;
+                        try
+                        {
+                            os = stream.createRawOutputStream();
+                            os.write(Hex.decodeHex(child.getTextContent()));
+                            LOG.debug(parentAttrKey + " => Data was streamed");
+                        }
+                        finally
+                        {
+                            if (os != null)
+                            {
+                                os.close();
+                            }
+                        }
+                    }
+                    else
+                    {
+                        LOG.warn(parentAttrKey + " => Not handling element DATA encoding: " +
+                                child.getAttribute("ENCODING"));
+                    }
+                }
+                else
+                {
+                    LOG.warn(parentAttrKey + " => Not handling child element: " + child.getTagName());
+                }
+            }
+        }
+
+        return stream;
+    }
+
+    private COSArray parseArrayElement(Element arrayEl) throws IOException
+    {
+        LOG.debug("Parse " + arrayEl.getAttribute("KEY") + " Array");
+        COSArray array = new COSArray();
+        NodeList nodeList = arrayEl.getElementsByTagName("FIXED");
+        Node node;
+        Element el;
+        String elAttrKey = arrayEl.getAttribute("KEY");
+
+        if ("BBox".equals(elAttrKey))
+        {
+            if (nodeList.getLength() < 4)
+            {
+                throw new IOException("BBox does not have enough coordinates, only has: " +
+                        nodeList.getLength());
+            }
+        }
+        else if ("Matrix".equals(elAttrKey))
+        {
+            if (nodeList.getLength() < 6)
+            {
+                throw new IOException("Matrix does not have enough coordinates, only has: " + 
+                        nodeList.getLength());
+            }
+        }
+
+        LOG.debug("There are " + nodeList.getLength() + " FIXED elements");
+
+        for (int i = 0; i < nodeList.getLength(); i++)
+        {
+            node = nodeList.item(i);
+            if (node instanceof Element)
+            {
+                el = (Element) node;
+                LOG.debug(elAttrKey + " value(" + i + "): " + el.getAttribute("VAL"));
+                array.add(new COSFloat(el.getAttribute("VAL")));
+            }
+        }
+
+        return array;
+    }
+
+    private COSDictionary parseDictElement(Element dictEl) throws IOException
+    {
+        LOG.debug("Parse " + dictEl.getAttribute("KEY") + " Dictionary");
+        COSDictionary dict = new COSDictionary();
+
+        NodeList nodeList = dictEl.getChildNodes();
+        Node node;
+        Element child;
+        String childAttrKey;
+        String childAttrVal;
+        String parentAttrKey = dictEl.getAttribute("KEY");
+
+        for (int i = 0; i < nodeList.getLength(); i++)
+        {
+            node = nodeList.item(i);
+            if (node instanceof Element)
+            {
+                child = (Element) node;
+                childAttrKey = child.getAttribute("KEY");
+                childAttrVal = child.getAttribute("VAL");
+
+                if ("DICT".equals(child.getTagName()))
+                {
+                    LOG.debug(parentAttrKey + " => Handling DICT element with key: " + childAttrKey);
+                    dict.setItem(COSName.getPDFName(childAttrKey), parseDictElement(child));
+                    LOG.debug(parentAttrKey + " => Set " + childAttrKey);
+                }
+                else if ("STREAM".equals(child.getTagName()))
+                {
+                    LOG.debug(parentAttrKey + " => Handling STREAM element with key: " + childAttrKey);
+                    dict.setItem(COSName.getPDFName(childAttrKey), parseStreamElement(child));
+                }
+                else if ("NAME".equals(child.getTagName()))
+                {
+                    LOG.debug(parentAttrKey + " => Handling NAME element with key: " + childAttrKey);
+                    dict.setName(COSName.getPDFName(childAttrKey), childAttrVal);
+                    LOG.debug(parentAttrKey + " => Set " + childAttrKey + ": " + childAttrVal);
+                }
+                else
+                {
+                    LOG.warn(parentAttrKey + " => NOT handling child element: " + child.getTagName());
+                }
+            }
+        }
+
+        return dict;
+    }
+}

Propchange: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFStampAnnotationAppearance.java
------------------------------------------------------------------------------
    svn:eol-style = native