# FDFAnnotationStamp.java

    /*
     * 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.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.OutputStream;

    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.COSArray;
    import org.apache.pdfbox.cos.COSBoolean;

    import org.apache.pdfbox.cos.COSDictionary;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.cos.COSNumber;
    import org.apache.pdfbox.cos.COSStream;
    import org.apache.pdfbox.util.Hex;
    import org.apache.pdfbox.util.XMLUtil;
    import org.w3c.dom.Document;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;

    /**
     * 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.
         */
        public static final String SUBTYPE = "Stamp";

        /**
         * Default constructor.
         */
        public FDFAnnotationStamp()
        {
            annot.setName(COSName.SUBTYPE, SUBTYPE);
        }

        /**
         * Constructor.
         *
         * @param a An existing FDF Annotation.
         */
        public FDFAnnotationStamp(COSDictionary a)
        {
            super(a);
        }

        /**
         * Constructor.
         *
         * @param element An XFDF element.
         *
         * @throws IOException If there is an error extracting information from the element.
         */
        public FDFAnnotationStamp(Element element) throws IOException
        {
            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())
            {
                LOG.debug("Decoded XML: " + new String(decodedAppearanceXML));

                Document stampAppearance = XMLUtil
                        .parse(new ByteArrayInputStream(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, parseStampAnnotationAppearanceXML(appearanceEl));
            }
        }

        /**
         * This will create an Appearance dictionary from an appearance XML element.
         *
         * @param appearanceXML The XML element that contains the appearance data.
         */
        private COSDictionary parseStampAnnotationAppearanceXML(Element appearanceXML) throws IOException
        {
            COSDictionary dictionary = new COSDictionary();
            // the N entry is required.
            dictionary.setItem(COSName.N, new COSStream());
            LOG.debug("Build dictionary for Appearance based on the appearanceXML");

            NodeList nodeList = appearanceXML.getChildNodes();
            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")))
            {
                LOG.warn(parentAttrKey + " => Not handling element: " + appearanceXML.getTagName() +
                                         " with key: " + appearanceXML.getAttribute("KEY"));
                return dictionary;
            }
            for (int i = 0; i < nodeList.getLength(); i++)
            {
                Node node = nodeList.item(i);
                if (node instanceof Element)
                {
                    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());
                    }
                }
            }
            return dictionary;
        }

        private COSStream parseStreamElement(Element streamEl) throws IOException
        {
            LOG.debug("Parse " + streamEl.getAttribute("KEY") + " Stream");
            COSStream stream = new COSStream();

            NodeList nodeList = streamEl.getChildNodes();
            String parentAttrKey = streamEl.getAttribute("KEY");

            for (int i = 0; i < nodeList.getLength(); i++)
            {
                Node node = nodeList.item(i);
                if (node instanceof Element)
                {
                    Element child = (Element) node;
                    String childAttrKey = child.getAttribute("KEY");
                    String 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 ("FIXED".equalsIgnoreCase(child.getTagName()))
                    {
                        stream.setFloat(COSName.getPDFName(childAttrKey), Float.parseFloat(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 " + 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")))
                        {
                            try (OutputStream os = stream.createRawOutputStream())
                            {
                                os.write(Hex.decodeHex(child.getTextContent()));
                                LOG.debug(parentAttrKey + " => Data was streamed");
                            }
                        }
                        else if ("ASCII".equals(child.getAttribute("ENCODING")))
                        {
                            try (OutputStream os = stream.createOutputStream())
                            {
                                // not sure about charset
                                os.write(child.getTextContent().getBytes());
                                LOG.debug(parentAttrKey + " => Data was streamed");
                            }
                        }
                        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.getChildNodes();
            String parentAttrKey = arrayEl.getAttribute("KEY");

            if ("BBox".equals(parentAttrKey) && nodeList.getLength() < 4)
            {
                throw new IOException("BBox does not have enough coordinates, only has: " +
                        nodeList.getLength());
            }
            else if ("Matrix".equals(parentAttrKey) && nodeList.getLength() < 6)
            {
                throw new IOException("Matrix does not have enough coordinates, only has: " +
                        nodeList.getLength());
            }

            for (int i = 0; i < nodeList.getLength(); i++)
            {
                Node node = nodeList.item(i);
                if (node instanceof Element)
                {
                    Element child = (Element) node;
                    String childAttrKey = child.getAttribute("KEY");
                    String childAttrVal = child.getAttribute("VAL");
                    LOG.debug(parentAttrKey + " => reading child: " + child.getTagName() +
                               " with key: " + childAttrKey);
                    if ("INT".equalsIgnoreCase(child.getTagName()) || "FIXED".equalsIgnoreCase(child.getTagName()))
                    {
                        LOG.debug(parentAttrKey + " value(" + i + "): " + childAttrVal);
                        array.add(COSNumber.get(childAttrVal));
                    }
                    else if ("NAME".equalsIgnoreCase(child.getTagName()))
                    {
                        LOG.debug(parentAttrKey + " value(" + i + "): " + childAttrVal);
                        array.add(COSName.getPDFName(childAttrVal));
                    }
                    else if ("BOOL".equalsIgnoreCase(child.getTagName()))
                    {
                        LOG.debug(parentAttrKey + " value(" + i + "): " + childAttrVal);
                        array.add(COSBoolean.getBoolean(Boolean.parseBoolean(childAttrVal)));
                    }
                    else if ("DICT".equalsIgnoreCase(child.getTagName()))
                    {
                        LOG.debug(parentAttrKey + " value(" + i + "): " + childAttrVal);
                        array.add(parseDictElement(child));
                    }
                    else if ("STREAM".equalsIgnoreCase(child.getTagName()))
                    {
                        LOG.debug(parentAttrKey + " value(" + i + "): " + childAttrVal);
                        array.add(parseStreamElement(child));
                    }
                    else if ("ARRAY".equalsIgnoreCase(child.getTagName()))
                    {
                        LOG.debug(parentAttrKey + " value(" + i + "): " + childAttrVal);
                        array.add(parseArrayElement(child));
                    }
                    else
                    {
                        LOG.warn(parentAttrKey + " => Not handling child element: " + child.getTagName());
                    }
                }
            }

            return array;
        }

        private COSDictionary parseDictElement(Element dictEl) throws IOException
        {
            LOG.debug("Parse " + dictEl.getAttribute("KEY") + " Dictionary");
            COSDictionary dict = new COSDictionary();

            NodeList nodeList = dictEl.getChildNodes();
            String parentAttrKey = dictEl.getAttribute("KEY");

            for (int i = 0; i < nodeList.getLength(); i++)
            {
                Node node = nodeList.item(i);
                if (node instanceof Element)
                {
                    Element child = (Element) node;
                    String childAttrKey = child.getAttribute("KEY");
                    String 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 if ("INT".equalsIgnoreCase(child.getTagName()))
                    {
                        dict.setInt(COSName.getPDFName(childAttrKey), Integer.parseInt(childAttrVal));
                        LOG.debug(parentAttrKey + " => Set " + childAttrKey + ": " + childAttrVal);
                    }
                    else if ("FIXED".equalsIgnoreCase(child.getTagName()))
                    {
                        dict.setFloat(COSName.getPDFName(childAttrKey), Float.parseFloat(childAttrVal));
                        LOG.debug(parentAttrKey + " => Set " + childAttrKey + ": " + childAttrVal);
                    }
                    else if ("BOOL".equalsIgnoreCase(child.getTagName()))
                    {
                        dict.setBoolean(COSName.getPDFName(childAttrKey), Boolean.parseBoolean(childAttrVal));
                        LOG.debug(parentAttrKey + " => Set " + childAttrVal);
                    }
                    else if ("ARRAY".equalsIgnoreCase(child.getTagName()))
                    {
                        dict.setItem(COSName.getPDFName(childAttrKey), parseArrayElement(child));
                        LOG.debug(parentAttrKey + " => Set " + childAttrKey);
                    }
                    else
                    {
                        LOG.warn(parentAttrKey + " => NOT handling child element: " + child.getTagName());
                    }
                }
            }

            return dict;
        }
    }