]> source.dussan.org Git - poi.git/commitdiff
#64773 - Visual signatures for .xlsx/.docx
authorAndreas Beeker <kiwiwings@apache.org>
Sat, 10 Oct 2020 23:33:26 +0000 (23:33 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Sat, 10 Oct 2020 23:33:26 +0000 (23:33 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1882394 13f79535-47bb-0310-9956-ffa450edef68

25 files changed:
build.xml
src/java/org/apache/poi/common/usermodel/PictureType.java [new file with mode: 0644]
src/multimodule/ooxml-lite/java9/module-info.class
src/multimodule/ooxml-lite/java9/module-info.java
src/multimodule/ooxml-schemas/java9/module-info.class
src/multimodule/ooxml-schemas/java9/module-info.java
src/ooxml/java/org/apache/poi/ooxml/POIXMLDocumentPart.java
src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java
src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java
src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java [new file with mode: 0644]
src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java
src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java
src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java
src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSignatureLine.java [new file with mode: 0644]
src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFVMLDrawing.java
src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java [new file with mode: 0644]
src/ooxml/resources/org/apache/poi/schemas/ooxmlSchemas.xsdconfig
src/ooxml/resources/org/apache/poi/schemas/vmlDrawing.xsd [new file with mode: 0644]
src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java
src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFVMLDrawing.java
test-data/xmldsign/jack-sign.emf [new file with mode: 0644]

index 46308cc2b527d03ef6dc61a5ee32241875a3dcf5..717b064a07700b2a01cc7d97042634643e8ba1f6 100644 (file)
--- a/build.xml
+++ b/build.xml
@@ -792,7 +792,7 @@ under the License.
         <copy todir="${xmlbean.xsds.dir}">
             <zipfileset src="${ooxml.xsds.izip.1}"/>
             <fileset dir="${ooxml.visio.xsd.dir}"/>
-            <fileset dir="${ooxml.schema.xsdconfig.dir}" includes="ooxmlSchemas.xsdconfig,markup-compatibility.xsd"/>
+            <fileset dir="${ooxml.schema.xsdconfig.dir}" includes="ooxmlSchemas.xsdconfig,markup-compatibility.xsd,vmlDrawing.xsd"/>
             <zipfileset src="${ooxml.xsds.izip.2}" includes="opc-digSig.xsd,opc-relationships.xsd"/>
             <fileset dir="${ooxml.security.xsd.dir}" includes="signatureInfo.xsd"/>
             <fileset dir="${ooxml.schema.xsdconfig.dir}" includes="XAdES*.xsd,*.xsdconfig,xmldsig*.xsd"/>
diff --git a/src/java/org/apache/poi/common/usermodel/PictureType.java b/src/java/org/apache/poi/common/usermodel/PictureType.java
new file mode 100644 (file)
index 0000000..f0fef79
--- /dev/null
@@ -0,0 +1,51 @@
+package org.apache.poi.common.usermodel;
+
+/**
+ * General enum class to define a picture format/type
+ *
+ * @since POI 5.0
+ */
+public enum PictureType {
+    /** Extended windows meta file */
+    EMF("image/x-emf",".emf"),
+    /** Windows Meta File */
+    WMF("image/x-wmf",".wmf"),
+    /** Mac PICT format */
+    PICT("image/pict",".pict"), // or image/x-pict (for HSLF) ???
+    /** JPEG format */
+    JPEG("image/jpeg",".jpg"),
+    /** PNG format */
+    PNG("image/png",".png"),
+    /** Device independent bitmap */
+    DIB("image/dib",".dib"),
+    /** GIF image format */
+    GIF("image/gif",".gif"),
+    /** Tag Image File (.tiff) */
+    TIFF("image/tiff",".tif"),
+    /** Encapsulated Postscript (.eps) */
+    EPS("image/x-eps",".eps"),
+    /** Windows Bitmap (.bmp) */
+    BMP("image/x-ms-bmp",".bmp"),
+    /** WordPerfect graphics (.wpg) */
+    WPG("image/x-wpg",".wpg"),
+    /** Microsoft Windows Media Photo image (.wdp) */
+    WDP("image/vnd.ms-photo",".wdp"),
+    /** Scalable vector graphics (.svg) - supported by Office 2016 and higher */
+    SVG("image/svg+xml", ".svg"),
+    /** Unknown picture type - specific to escher bse record */
+    UNKNOWN("", ".dat"),
+    /** Picture type error - specific to escher bse record */
+    ERROR("", ".dat"),
+    /** JPEG in the YCCK or CMYK color space. */
+    CMYKJPEG("image/jpeg", ".jpg"),
+    /** client defined blip type - native-id 32 to 255 */
+    CLIENT("", ".dat")
+    ;
+
+    public final String contentType,extension;
+
+    PictureType(String contentType,String extension) {
+        this.contentType = contentType;
+        this.extension = extension;
+    }
+}
index 78ddc1f01281747b9eda2de66edf395cc37b05c4..0eb072af3d0d9d892ac9617fcc0742f78a26fc1a 100644 (file)
Binary files a/src/multimodule/ooxml-lite/java9/module-info.class and b/src/multimodule/ooxml-lite/java9/module-info.class differ
index 77a3bb34b306cdc1709253e508be06a95fa7c424..6736f8669c7d87731fca484a92dffeef324d8925 100644 (file)
@@ -22,6 +22,7 @@ open module org.apache.poi.ooxml.schemas {
     requires transitive org.apache.xmlbeans;
     requires java.xml;
 
+
     exports com.microsoft.schemas.compatibility;
     exports com.microsoft.schemas.office.excel;
     exports com.microsoft.schemas.office.office;
@@ -29,6 +30,7 @@ open module org.apache.poi.ooxml.schemas {
     exports com.microsoft.schemas.office.x2006.digsig;
     exports com.microsoft.schemas.vml;
     exports org.apache.poi.schemas.ooxml.system.ooxml;
+    exports org.apache.poi.schemas.vmldrawing;
     exports org.etsi.uri.x01903.v13;
     exports org.openxmlformats.schemas.drawingml.x2006.chart;
     exports org.openxmlformats.schemas.drawingml.x2006.main;
index 339c9d793ed7d34bb1cdf4721ae65a906c08efa6..f2a86deda0182602db0f564679563a3e2cb44da6 100644 (file)
Binary files a/src/multimodule/ooxml-schemas/java9/module-info.class and b/src/multimodule/ooxml-schemas/java9/module-info.class differ
index 974467061fc8b76d8d5f43d51083bbd77673cc3c..102ab6ad69486f3813dbc9e435a953c8780f5b05 100644 (file)
@@ -56,4 +56,6 @@ open module org.apache.poi.ooxml.schemas {
     exports org.openxmlformats.schemas.xpackage.x2006.digitalSignature;
     exports org.openxmlformats.schemas.xpackage.x2006.relationships;
     exports org.w3.x2000.x09.xmldsig;
+
+    exports org.apache.poi.schemas.vmldrawing;
 }
\ No newline at end of file
index 9346942e6c97977a19c22f15408891e131f60be7..fd9b032ef6c7b18c8b2d28a9194126b1a1f47965 100644 (file)
@@ -531,7 +531,8 @@ public class POIXMLDocumentPart {
      * @param minIdx     The minimum free index to assign, use -1 for any
      * @return The next free part number, or -1 if none available
      */
-    protected final int getNextPartNumber(POIXMLRelation descriptor, int minIdx) {
+    @Internal
+    public final int getNextPartNumber(POIXMLRelation descriptor, int minIdx) {
         OPCPackage pkg = packagePart.getPackage();
 
         try {
index ef492bd223393bc3328c71f9466d74de4ffcf558..0e1700317fff89c4bbbdbd79d5b1ee971566509e 100644 (file)
 
 package org.apache.poi.ooxml.util;
 
-import org.apache.poi.util.POILogFactory;
-import org.apache.poi.util.POILogger;
+import java.util.Locale;
 
 import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
 import javax.xml.xpath.XPathFactory;
 
+import com.microsoft.schemas.compatibility.AlternateContentDocument;
+import org.apache.poi.util.Internal;
+import org.apache.poi.util.POILogFactory;
+import org.apache.poi.util.POILogger;
+import org.apache.poi.xslf.usermodel.XSLFShape;
+import org.apache.xmlbeans.XmlCursor;
+import org.apache.xmlbeans.XmlException;
+import org.apache.xmlbeans.XmlObject;
+import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl;
+
 public final class XPathHelper {
-    private static POILogger logger = POILogFactory.getLogger(XPathHelper.class);
+    private static final POILogger LOG = POILogFactory.getLogger(XPathHelper.class);
+
+    private static final String OSGI_ERROR =
+        "Schemas (*.xsb) for <CLASS> can't be loaded - usually this happens when OSGI " +
+        "loading is used and the thread context classloader has no reference to " +
+        "the xmlbeans classes - please either verify if the <XSB>.xsb is on the " +
+        "classpath or alternatively try to use the full ooxml-schemas-x.x.jar";
+
+    private static final String MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
+    private static final String MAC_DML_NS = "http://schemas.microsoft.com/office/mac/drawingml/2008/main";
+    private static final QName ALTERNATE_CONTENT_TAG = new QName(MC_NS, "AlternateContent");
+     // AlternateContentDocument.AlternateContent.type.getName();
 
     private XPathHelper() {}
 
@@ -41,9 +62,165 @@ public final class XPathHelper {
         try {
             xpf.setFeature(feature, enabled);
         } catch (Exception e) {
-            logger.log(POILogger.WARN, "XPathFactory Feature unsupported", feature, e);
+            LOG.log(POILogger.WARN, "XPathFactory Feature unsupported", feature, e);
         } catch (AbstractMethodError ame) {
-            logger.log(POILogger.WARN, "Cannot set XPathFactory feature because outdated XML parser in classpath", feature, ame);
+            LOG.log(POILogger.WARN, "Cannot set XPathFactory feature because outdated XML parser in classpath", feature, ame);
+        }
+    }
+
+
+
+    /**
+     * Internal code - API may change any time!
+     * <p>
+     * The {@link #selectProperty(Class, String)} xquery method has some performance penalties,
+     * which can be workaround by using {@link XmlCursor}. This method also takes into account
+     * that {@code AlternateContent} tags can occur anywhere on the given path.
+     * <p>
+     * It returns the first element found - the search order is:
+     * <ul>
+     *     <li>searching for a direct child</li>
+     *     <li>searching for a AlternateContent.Choice child</li>
+     *     <li>searching for a AlternateContent.Fallback child</li>
+     * </ul>
+     * Currently POI OOXML is based on the first edition of the ECMA 376 schema, which doesn't
+     * allow AlternateContent tags to show up everywhere. The factory flag is
+     * a workaround to process files based on a later edition. But it comes with the drawback:
+     * any change on the returned XmlObject aren't saved back to the underlying document -
+     * so it's a non updatable clone. If factory is null, a XmlException is
+     * thrown if the AlternateContent is not allowed by the surrounding element or if the
+     * extracted object is of the generic type XmlAnyTypeImpl.
+     *
+     * @param resultClass the requested result class
+     * @param factory a factory parse method reference to allow reparsing of elements
+     *                extracted from AlternateContent elements. Usually the enclosing XmlBeans type needs to be used
+     *                to parse the stream
+     * @param path the elements path, each array must contain at least 1 QName,
+     *             but can contain additional alternative tags
+     * @return the xml object at the path location, or null if not found
+     *
+     * @throws XmlException If factory is null, a XmlException is
+     *      thrown if the AlternateContent is not allowed by the surrounding element or if the
+     *      extracted object is of the generic type XmlAnyTypeImpl.
+     *
+     * @since POI 4.1.2
+     */
+    @SuppressWarnings("unchecked")
+    @Internal
+    public static <T extends XmlObject> T selectProperty(XmlObject startObject, Class<T> resultClass, XSLFShape.ReparseFactory<T> factory, QName[]... path)
+            throws XmlException {
+        XmlObject xo = startObject;
+        XmlCursor cur = xo.newCursor();
+        XmlCursor innerCur = null;
+        try {
+            innerCur = selectProperty(cur, path, 0, factory != null, false);
+            if (innerCur == null) {
+                return null;
+            }
+
+            // Pesky XmlBeans bug - see Bugzilla #49934
+            // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas
+            xo = innerCur.getObject();
+            if (xo instanceof XmlAnyTypeImpl) {
+                String errorTxt = OSGI_ERROR
+                        .replace("<CLASS>", resultClass.getSimpleName())
+                        .replace("<XSB>", resultClass.getSimpleName().toLowerCase(Locale.ROOT)+"*");
+                if (factory == null) {
+                    throw new XmlException(errorTxt);
+                } else {
+                    xo = factory.parse(innerCur.newXMLStreamReader());
+                }
+            }
+
+            return (T)xo;
+        } finally {
+            cur.dispose();
+            if (innerCur != null) {
+                innerCur.dispose();
+            }
         }
     }
+
+    private static XmlCursor selectProperty(final XmlCursor cur, final QName[][] path, final int offset, final boolean reparseAlternate, final boolean isAlternate)
+            throws XmlException {
+        // first try the direct children
+        for (QName qn : path[offset]) {
+            for (boolean found = cur.toChild(qn); found; found = cur.toNextSibling(qn)) {
+                if (offset == path.length-1) {
+                    return cur;
+                }
+                cur.push();
+                XmlCursor innerCur = selectProperty(cur, path, offset+1, reparseAlternate, false);
+                if (innerCur != null) {
+                    return innerCur;
+                }
+                cur.pop();
+            }
+        }
+        // if we were called inside an alternate content handling don't look for alternates again
+        if (isAlternate || !cur.toChild(ALTERNATE_CONTENT_TAG)) {
+            return null;
+        }
+
+        // otherwise check first the choice then the fallback content
+        XmlObject xo = cur.getObject();
+        AlternateContentDocument.AlternateContent alterCont;
+        if (xo instanceof AlternateContentDocument.AlternateContent) {
+            alterCont = (AlternateContentDocument.AlternateContent)xo;
+        } else {
+            // Pesky XmlBeans bug - see Bugzilla #49934
+            // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas
+            if (!reparseAlternate) {
+                throw new XmlException(OSGI_ERROR
+                                               .replace("<CLASS>", "AlternateContent")
+                                               .replace("<XSB>", "alternatecontentelement")
+                );
+            }
+            try {
+                AlternateContentDocument acd = AlternateContentDocument.Factory.parse(cur.newXMLStreamReader());
+                alterCont = acd.getAlternateContent();
+            } catch (XmlException e) {
+                throw new XmlException("unable to parse AlternateContent element", e);
+            }
+        }
+
+        final int choices = alterCont.sizeOfChoiceArray();
+        for (int i=0; i<choices; i++) {
+            // TODO: check [Requires] attribute of [Choice] element, if we can handle the content
+            AlternateContentDocument.AlternateContent.Choice choice = alterCont.getChoiceArray(i);
+            XmlCursor cCur = choice.newCursor();
+            XmlCursor innerCur = null;
+            try {
+                String requiresNS = cCur.namespaceForPrefix(choice.getRequires());
+                if (MAC_DML_NS.equalsIgnoreCase(requiresNS)) {
+                    // Mac DML usually contains PDFs ...
+                    continue;
+                }
+                innerCur = selectProperty(cCur, path, offset, reparseAlternate, true);
+                if (innerCur != null) {
+                    return innerCur;
+                }
+            } finally {
+                if (innerCur != cCur) {
+                    cCur.dispose();
+                }
+            }
+        }
+
+        if (!alterCont.isSetFallback()) {
+            return null;
+        }
+
+        XmlCursor fCur = alterCont.getFallback().newCursor();
+        XmlCursor innerCur = null;
+        try {
+            innerCur = selectProperty(fCur, path, offset, reparseAlternate, true);
+            return innerCur;
+        } finally {
+            if (innerCur != fCur) {
+                fCur.dispose();
+            }
+        }
+    }
+
 }
index e7660d8dd8164e14373ab586fd5067681d44ce10..030bcaa22d5da5fbf5bc2b85471d57389e93da46 100644 (file)
@@ -45,6 +45,7 @@ import javax.xml.crypto.dsig.XMLSignatureFactory;
 import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
 
 import org.apache.poi.EncryptedDocumentException;
+import org.apache.poi.hpsf.ClassID;
 import org.apache.poi.openxml4j.opc.OPCPackage;
 import org.apache.poi.poifs.crypt.HashAlgorithm;
 import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet;
@@ -89,10 +90,10 @@ public class SignatureConfig {
     );
 
 
-    private ThreadLocal<OPCPackage> opcPackage = new ThreadLocal<>();
-    private ThreadLocal<XMLSignatureFactory> signatureFactory = new ThreadLocal<>();
-    private ThreadLocal<KeyInfoFactory> keyInfoFactory = new ThreadLocal<>();
-    private ThreadLocal<Provider> provider = new ThreadLocal<>();
+    private final ThreadLocal<OPCPackage> opcPackage = new ThreadLocal<>();
+    private final ThreadLocal<XMLSignatureFactory> signatureFactory = new ThreadLocal<>();
+    private final ThreadLocal<KeyInfoFactory> keyInfoFactory = new ThreadLocal<>();
+    private final ThreadLocal<Provider> provider = new ThreadLocal<>();
 
     private List<SignatureFacet> signatureFacets = new ArrayList<>();
     private HashAlgorithm digestAlgo = HashAlgorithm.sha256;
@@ -165,6 +166,26 @@ public class SignatureConfig {
      */
     private String signatureDescription = "Office OpenXML Document";
 
+    /**
+     * Only applies when working with visual signatures:
+     * Specifies a GUID which can be cross-referenced with the GUID of the signature line stored in the document content.
+     * I.e. the signatureline element id attribute in the document/sheet has to be references in the SetupId element.
+     */
+    private ClassID signatureImageSetupId;
+
+    /**
+     * Provides a signature image for visual signature lines
+     */
+    private byte[] signatureImage;
+    /**
+     * The image shown, when the signature is valid
+     */
+    private byte[] signatureImageValid;
+    /**
+     * The image shown, when the signature is invalid
+     */
+    private byte[] signatureImageInvalid;
+
     /**
      * The process of signing includes the marshalling of xml structures.
      * This also includes the canonicalization. Currently this leads to problems
@@ -386,6 +407,38 @@ public class SignatureConfig {
         this.signatureDescription = signatureDescription;
     }
 
+    public byte[] getSignatureImage() {
+        return signatureImage;
+    }
+
+    public byte[] getSignatureImageValid() {
+        return signatureImageValid;
+    }
+
+    public byte[] getSignatureImageInvalid() {
+        return signatureImageInvalid;
+    }
+
+    public ClassID getSignatureImageSetupId() {
+        return signatureImageSetupId;
+    }
+
+    public void setSignatureImageSetupId(ClassID signatureImageSetupId) {
+        this.signatureImageSetupId = signatureImageSetupId;
+    }
+
+    public void setSignatureImage(byte[] signatureImage) {
+        this.signatureImage = (signatureImage == null) ? null : signatureImage.clone();
+    }
+
+    public void setSignatureImageValid(byte[] signatureImageValid) {
+        this.signatureImageValid = (signatureImageValid == null) ? null : signatureImageValid.clone();
+    }
+
+    public void setSignatureImageInvalid(byte[] signatureImageInvalid) {
+        this.signatureImageInvalid = (signatureImageInvalid == null) ? null : signatureImageInvalid.clone();
+    }
+
     /**
      * @return the default canonicalization method, defaults to INCLUSIVE
      */
diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java
new file mode 100644 (file)
index 0000000..8d9ffe4
--- /dev/null
@@ -0,0 +1,489 @@
+/* ====================================================================
+   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.poi.poifs.crypt.dsig;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.GradientPaint;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineBreakMeasurer;
+import java.awt.font.TextAttribute;
+import java.awt.font.TextLayout;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Dimension2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.UUID;
+
+import javax.imageio.ImageIO;
+import javax.xml.namespace.QName;
+
+import com.microsoft.schemas.office.office.CTSignatureLine;
+import com.microsoft.schemas.office.office.STTrueFalse;
+import com.microsoft.schemas.vml.CTGroup;
+import com.microsoft.schemas.vml.CTImageData;
+import com.microsoft.schemas.vml.CTShape;
+import com.microsoft.schemas.vml.STExt;
+import org.apache.poi.common.usermodel.PictureType;
+import org.apache.poi.hpsf.ClassID;
+import org.apache.poi.ooxml.POIXMLException;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.poifs.filesystem.FileMagic;
+import org.apache.poi.sl.draw.DrawPictureShape;
+import org.apache.poi.sl.draw.ImageRenderer;
+import org.apache.xmlbeans.XmlCursor;
+import org.apache.xmlbeans.XmlObject;
+
+/**
+ * Base class for SignatureLines (XSSF,XWPF only)
+ */
+public abstract class SignatureLine {
+
+    private static final String MS_OFFICE_URN = "urn:schemas-microsoft-com:office:office";
+    protected static final QName QNAME_SIGNATURE_LINE = new QName(MS_OFFICE_URN, "signatureline");
+
+
+    private ClassID setupId;
+    private Boolean allowComments;
+    private String signingInstructions = "Before signing the document, verify that the content you are signing is correct.";
+    private String suggestedSigner;
+    private String suggestedSigner2;
+    private String suggestedSignerEmail;
+    private String caption;
+    private String invalidStamp = "invalid";
+    private byte[] plainSignature;
+    private String contentType;
+
+    private CTShape signatureShape;
+
+    public ClassID getSetupId() {
+        return setupId;
+    }
+
+    public void setSetupId(ClassID setupId) {
+        this.setupId = setupId;
+    }
+
+    public Boolean getAllowComments() {
+        return allowComments;
+    }
+
+    public void setAllowComments(Boolean allowComments) {
+        this.allowComments = allowComments;
+    }
+
+    public String getSigningInstructions() {
+        return signingInstructions;
+    }
+
+    public void setSigningInstructions(String signingInstructions) {
+        this.signingInstructions = signingInstructions;
+    }
+
+    public String getSuggestedSigner() {
+        return suggestedSigner;
+    }
+
+    public void setSuggestedSigner(String suggestedSigner) {
+        this.suggestedSigner = suggestedSigner;
+    }
+
+    public String getSuggestedSigner2() {
+        return suggestedSigner2;
+    }
+
+    public void setSuggestedSigner2(String suggestedSigner2) {
+        this.suggestedSigner2 = suggestedSigner2;
+    }
+
+    public String getSuggestedSignerEmail() {
+        return suggestedSignerEmail;
+    }
+
+    public void setSuggestedSignerEmail(String suggestedSignerEmail) {
+        this.suggestedSignerEmail = suggestedSignerEmail;
+    }
+
+    /**
+     * The default caption
+     * @return "[suggestedSigner] \n [suggestedSigner2] \n [suggestedSignerEmail]"
+     */
+    public String getDefaultCaption() {
+        return suggestedSigner+"\n"+suggestedSigner2+"\n"+suggestedSignerEmail;
+    }
+
+    public String getCaption() {
+        return caption;
+    }
+
+    /**
+     * Set the caption - use maximum of three lines separated by "\n".
+     * Defaults to {@link #getDefaultCaption()}
+     * @param caption the signature caption
+     */
+    public void setCaption(String caption) {
+        this.caption = caption;
+    }
+
+    public String getInvalidStamp() {
+        return invalidStamp;
+    }
+
+    /**
+     * Sets the text stamped over the signature image when the document got tampered with
+     * @param invalidStamp the invalid stamp text
+     */
+    public void setInvalidStamp(String invalidStamp) {
+        this.invalidStamp = invalidStamp;
+    }
+
+    /** the plain signature without caption */
+    public byte[] getPlainSignature() {
+        return plainSignature;
+    }
+
+    /**
+     * Sets the plain signature
+     * supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF.
+     * for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath
+     *
+     * @param plainSignature the plain signature - if {@code null}, the signature is not rendered
+     *                       and only the caption is visible
+     */
+    public void setPlainSignature(byte[] plainSignature) {
+        this.plainSignature = plainSignature;
+        this.contentType = null;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public void setContentType(String contentType) {
+        this.contentType = contentType;
+    }
+
+    public CTShape getSignatureShape() {
+        return signatureShape;
+    }
+
+    public void setSignatureShape(CTShape signatureShape) {
+        this.signatureShape = signatureShape;
+    }
+
+    public void setSignatureShape(CTSignatureLine signatureLine) {
+        XmlCursor cur = signatureLine.newCursor();
+        cur.toParent();
+        this.signatureShape = (CTShape)cur.getObject();
+        cur.dispose();
+    }
+
+    public void updateSignatureConfig(SignatureConfig config) throws IOException {
+        if (plainSignature == null) {
+            throw new IllegalStateException("Plain signature not initialized");
+        }
+
+        if (contentType == null) {
+            determineContentType();
+        }
+
+        byte[] signValid = generateImage(true, false);
+        byte[] signInvalid = generateImage(true, true);
+
+        config.setSignatureImageSetupId(getSetupId());
+        config.setSignatureImage(plainPng());
+        config.setSignatureImageValid(signValid);
+        config.setSignatureImageInvalid(signInvalid);
+    }
+
+    protected void parse() {
+        if (signatureShape == null) {
+            return;
+        }
+
+        CTSignatureLine signatureLine = signatureShape.getSignaturelineArray(0);
+
+        setSetupId(new ClassID(signatureLine.getId()));
+        setAllowComments(signatureLine.isSetAllowcomments() ? STTrueFalse.TRUE.equals(signatureLine.getAllowcomments()) : null);
+        setSuggestedSigner(signatureLine.getSuggestedsigner());
+        setSuggestedSigner2(signatureLine.getSuggestedsigner2());
+        setSuggestedSignerEmail(signatureLine.getSuggestedsigneremail());
+        XmlCursor cur = signatureLine.newCursor();
+        try {
+            // the signinginstructions are actually qualified, but our schema version is too old
+            setSigningInstructions(cur.getAttributeText(new QName(MS_OFFICE_URN, "signinginstructions")));
+        } finally {
+            cur.dispose();
+        }
+    }
+
+    protected interface AddPictureData {
+        /**
+         * Add picture data to the document
+         * @param imageData the image bytes
+         * @param pictureType the picture type - typically PNG
+         * @return the relation id of the newly add picture
+         */
+        String addPictureData(byte[] imageData, PictureType pictureType) throws InvalidFormatException;
+    }
+
+    protected abstract void setRelationId(CTImageData imageData, String relId);
+
+    protected void add(XmlObject signatureContainer, AddPictureData addPictureData) {
+        byte[] inputImage;
+        try {
+            inputImage = generateImage(false, false);
+
+            CTGroup grp = CTGroup.Factory.newInstance();
+            grp.addNewShape();
+
+            XmlCursor contCur = signatureContainer.newCursor();
+            contCur.toEndToken();
+            XmlCursor otherC = grp.newCursor();
+            otherC.copyXmlContents(contCur);
+            otherC.dispose();
+            contCur.toPrevSibling();
+            signatureShape = (CTShape)contCur.getObject();
+            contCur.dispose();
+
+            signatureShape.setAlt("Microsoft Office Signature Line...");
+            signatureShape.setStyle("width:191.95pt;height:96.05pt");
+//            signatureShape.setStyle("position:absolute;margin-left:100.8pt;margin-top:43.2pt;width:192pt;height:96pt;z-index:1");
+            signatureShape.setType("rect");
+
+            String relationId = addPictureData.addPictureData(inputImage, PictureType.PNG);
+            CTImageData imgData = signatureShape.addNewImagedata();
+            setRelationId(imgData, relationId);
+            imgData.setTitle("");
+
+            CTSignatureLine xsl = signatureShape.addNewSignatureline();
+            if (suggestedSigner != null) {
+                xsl.setSuggestedsigner(suggestedSigner);
+            }
+            if (suggestedSigner2 != null) {
+                xsl.setSuggestedsigner2(suggestedSigner2);
+            }
+            if (suggestedSignerEmail != null) {
+                xsl.setSuggestedsigneremail(suggestedSignerEmail);
+            }
+            if (setupId == null) {
+                setupId = new ClassID("{"+ UUID.randomUUID().toString()+"}");
+            }
+            xsl.setId(setupId.toString());
+            xsl.setAllowcomments(STTrueFalse.T);
+            xsl.setIssignatureline(STTrueFalse.T);
+            xsl.setProvid("{00000000-0000-0000-0000-000000000000}");
+            xsl.setExt(STExt.EDIT);
+            xsl.setSigninginstructionsset(STTrueFalse.T);
+            XmlCursor cur = xsl.newCursor();
+            cur.setAttributeText(new QName(MS_OFFICE_URN, "signinginstructions"), signingInstructions);
+            cur.dispose();
+        } catch (IOException | InvalidFormatException e) {
+            // shouldn't happen ...
+            throw new POIXMLException("Can't generate signature line image", e);
+        }
+    }
+
+    protected void update() {
+
+    }
+
+    /**
+     * Word and Excel a regenerating the valid and invalid signature line based on the
+     * plain signature. Both are picky about the input format.
+     * Especially EMF images need to a specific device dimension (dpi)
+     * instead of fiddling around with the input image, we generate/register a bitmap image instead
+     *
+     * @return the converted PNG image
+     */
+    protected byte[] plainPng() throws IOException {
+        byte[] plain = getPlainSignature();
+        PictureType pictureType;
+        switch (FileMagic.valueOf(plain)) {
+            case PNG:
+                return plain;
+            case BMP:
+                pictureType = PictureType.BMP;
+                break;
+            case EMF:
+                pictureType = PictureType.EMF;
+                break;
+            case GIF:
+                pictureType = PictureType.GIF;
+                break;
+            case JPEG:
+                pictureType = PictureType.JPEG;
+                break;
+            case XML:
+                pictureType = PictureType.SVG;
+                break;
+            case TIFF:
+                pictureType = PictureType.TIFF;
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported picture format");
+        }
+
+
+
+        ImageRenderer rnd = DrawPictureShape.getImageRenderer(null, pictureType.contentType);
+        if (rnd == null) {
+            throw new UnsupportedOperationException(pictureType + " can't be rendered - did you provide poi-scratchpad and its dependencies (batik et al.)");
+        }
+        rnd.loadImage(getPlainSignature(), pictureType.contentType);
+
+        Dimension2D dim = rnd.getDimension();
+        int defaultWidth = 300;
+        int defaultHeight = (int)(defaultWidth * dim.getHeight() / dim.getWidth());
+        BufferedImage bi = new BufferedImage(defaultWidth, defaultHeight, BufferedImage.TYPE_INT_ARGB);
+        Graphics2D gfx = bi.createGraphics();
+        gfx.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+        gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+        gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        gfx.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        rnd.drawImage(gfx, new Rectangle2D.Double(0, 0, defaultWidth, defaultHeight));
+        gfx.dispose();
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ImageIO.write(bi, "PNG", bos);
+        return bos.toByteArray();
+    }
+
+
+    /**
+     * Generate the image for a signature line
+     * @param caption three lines separated by "\n" - usually something like "First name Last name\nRole\nname of the key"
+     * @param inputImage the plain signature - supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF.
+     *                   for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath
+     *                   if {@code null}, the inputImage is not rendered
+     * @param invalidText for invalid signature images, use the given text
+     * @return the signature image in PNG format as byte array
+     */
+    protected byte[] generateImage(boolean showSignature, boolean showInvalidStamp) throws IOException {
+        BufferedImage bi = new BufferedImage(400, 150, BufferedImage.TYPE_INT_ARGB);
+        Graphics2D gfx = bi.createGraphics();
+        gfx.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+        gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+        gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        gfx.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+        String markX = "X\n";
+        String lineX = (new String(new char[500]).replace("\0", " ")) +"\n";
+        String cap = (getCaption() == null) ? getDefaultCaption() : getCaption();
+        String text = markX+lineX+cap.replaceAll("(?m)^", "    ");
+
+        AttributedString as = new AttributedString(text);
+        as.addAttribute(TextAttribute.FAMILY, Font.SANS_SERIF);
+        as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, markX.length(), text.indexOf('\n', markX.length()));
+
+        as.addAttribute(TextAttribute.SIZE, 15, 0, markX.length());
+        as.addAttribute(TextAttribute.SIZE, 12, markX.length(), text.length());
+
+        gfx.setColor(Color.BLACK);
+
+        AttributedCharacterIterator chIter = as.getIterator();
+        FontRenderContext frc = gfx.getFontRenderContext();
+        LineBreakMeasurer measurer = new LineBreakMeasurer(chIter, frc);
+        float y = 80, x = 5;
+        for (int lineNr = 0; measurer.getPosition() < chIter.getEndIndex(); lineNr++) {
+            int mpos = measurer.getPosition();
+            int limit = text.indexOf('\n', mpos);
+            limit = (limit == -1) ? text.length() : limit+1;
+            TextLayout textLayout = measurer.nextLayout(bi.getWidth()-10, limit, false);
+            if (lineNr != 1) {
+                y += textLayout.getAscent();
+            }
+            textLayout.draw(gfx, x, y);
+            y += textLayout.getDescent() + textLayout.getLeading();
+        }
+
+        if (showSignature && plainSignature != null && contentType != null) {
+
+            ImageRenderer renderer = DrawPictureShape.getImageRenderer(gfx, contentType);
+
+            renderer.loadImage(plainSignature, contentType);
+
+            double targetX = 10;
+            double targetY = 100;
+            double targetWidth = bi.getWidth() - targetX;
+            double targetHeight = targetY - 5;
+            Dimension2D dim = renderer.getDimension();
+            double scale = Math.min(targetWidth / dim.getWidth(), targetHeight / dim.getHeight());
+            double effWidth = dim.getWidth() * scale;
+            double effHeight = dim.getHeight() * scale;
+
+            renderer.drawImage(gfx, new Rectangle2D.Double(targetX + ((bi.getWidth() - effWidth) / 2), targetY - effHeight, effWidth, effHeight));
+        }
+
+        if (showInvalidStamp && invalidStamp != null && !invalidStamp.isEmpty()) {
+            gfx.setFont(new Font("Lucida Bright", Font.ITALIC, 60));
+            gfx.rotate(Math.toRadians(-15), bi.getWidth()/2., bi.getHeight()/2.);
+            TextLayout tl = new TextLayout(invalidStamp, gfx.getFont(), gfx.getFontRenderContext());
+            Rectangle2D bounds = tl.getBounds();
+            x = (float)((bi.getWidth()-bounds.getWidth())/2 - bounds.getX());
+            y = (float)((bi.getHeight()-bounds.getHeight())/2 - bounds.getY());
+            Shape outline = tl.getOutline(AffineTransform.getTranslateInstance(x+2, y+1));
+            gfx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));
+            gfx.setPaint(Color.RED);
+            gfx.draw(outline);
+            gfx.setPaint(new GradientPaint(0, 0, Color.RED, 30, 20, new Color(128, 128, 255), true));
+            tl.draw(gfx, x, y);
+        }
+
+        gfx.dispose();
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ImageIO.write(bi, "PNG", bos);
+        return bos.toByteArray();
+    }
+
+    private void determineContentType() {
+        FileMagic fm = FileMagic.valueOf(plainSignature);
+        switch (fm) {
+            case GIF:
+                contentType = PictureType.GIF.contentType;
+                break;
+            case PNG:
+                contentType = PictureType.PNG.contentType;
+                break;
+            case JPEG:
+                contentType = PictureType.JPEG.contentType;
+                break;
+            case XML:
+                contentType = PictureType.SVG.contentType;
+                break;
+            case EMF:
+                contentType = PictureType.EMF.contentType;
+                break;
+            case WMF:
+                contentType = PictureType.WMF.contentType;
+                break;
+            default:
+                throw new IllegalArgumentException("unknown image type");
+        }
+    }
+
+}
index 0eff7e3186ccd2676a6c8087a992e9e6fa61cda7..669d315e605216083cbc2d12559d77bf94b433c6 100644 (file)
@@ -31,6 +31,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashSet;
@@ -62,6 +63,7 @@ import org.apache.poi.openxml4j.opc.PackageRelationship;
 import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
 import org.apache.poi.openxml4j.opc.PackagingURIHelper;
 import org.apache.poi.openxml4j.opc.TargetMode;
+import org.apache.poi.poifs.crypt.HashAlgorithm;
 import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
 import org.apache.poi.poifs.crypt.dsig.SignatureInfo;
 import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService;
@@ -256,10 +258,20 @@ public class OOXMLSignatureFacet implements SignatureFacet {
 
         SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance();
         CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1();
-        ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri());
+        if (signatureConfig.getDigestAlgo() != HashAlgorithm.sha1) {
+            ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri());
+        }
+
+        String desc = signatureConfig.getSignatureDescription();
+        if (desc != null) {
+            ctSigV1.setSignatureComments(desc);
+        }
 
-        if (signatureConfig.getSignatureDescription() != null) {
-            ctSigV1.setSignatureComments(signatureConfig.getSignatureDescription());
+        byte[] image = signatureConfig.getSignatureImage();
+        if (image != null) {
+            ctSigV1.setSetupID(signatureConfig.getSignatureImageSetupId().toString());
+            ctSigV1.setSignatureImage(image);
+            ctSigV1.setSignatureType(2);
         }
 
         Element n = (Element)document.importNode(ctSigV1.getDomNode(), true);
@@ -282,6 +294,27 @@ public class OOXMLSignatureFacet implements SignatureFacet {
 
         Reference reference = newReference(signatureInfo, "#" + objectId, null, XML_DIGSIG_NS+"Object", null, null);
         references.add(reference);
+
+        Base64.Encoder enc = Base64.getEncoder();
+        byte[] imageValid = signatureConfig.getSignatureImageValid();
+        if (imageValid != null) {
+            objectId = "idValidSigLnImg";
+            DOMStructure tn = new DOMStructure(document.createTextNode(enc.encodeToString(imageValid)));
+            objects.add(sigFac.newXMLObject(Collections.singletonList(tn), objectId, null, null));
+
+            reference = newReference(signatureInfo, "#" + objectId, null, XML_DIGSIG_NS+"Object", null, null);
+            references.add(reference);
+        }
+
+        byte[] imageInvalid = signatureConfig.getSignatureImageInvalid();
+        if (imageInvalid != null) {
+            objectId = "idInvalidSigLnImg";
+            DOMStructure tn = new DOMStructure(document.createTextNode(enc.encodeToString(imageInvalid)));
+            objects.add(sigFac.newXMLObject(Collections.singletonList(tn), objectId, null, null));
+
+            reference = newReference(signatureInfo, "#" + objectId, null, XML_DIGSIG_NS+"Object", null, null);
+            references.add(reference);
+        }
     }
 
     protected static String getRelationshipReferenceURI(String zipEntryName) {
index 9a722c59f691d70f2b8787bff03c3768e34f91f4..7cce4c64a776acdb0f1b0ed13b97f18f9b8cac15 100644 (file)
@@ -19,6 +19,8 @@
 
 package org.apache.poi.xslf.model;
 
+import static org.apache.poi.ooxml.util.XPathHelper.selectProperty;
+
 import java.util.function.Consumer;
 
 import javax.xml.namespace.QName;
@@ -113,7 +115,7 @@ public final class ParagraphPropertyFetcher<T> extends PropertyFetcher<T> {
 
     static CTTextParagraphProperties select(XSLFShape shape, int level) throws XmlException {
         QName[] lvlProp = { new QName(DML_NS, "lvl" + (level + 1) + "pPr") };
-        return shape.selectProperty(
+        return selectProperty(shape.getXmlObject(),
             CTTextParagraphProperties.class, ParagraphPropertyFetcher::parse, TX_BODY, LST_STYLE, lvlProp);
     }
 
index 9bb02504f84f3eae57dae0d3eafaf95a39f3adbe..2c0cfef0b809b576b7df79ff8b655a58b58cf59f 100644 (file)
@@ -19,6 +19,7 @@
 
 package org.apache.poi.xslf.model;
 
+import static org.apache.poi.ooxml.util.XPathHelper.selectProperty;
 import static org.apache.poi.xslf.model.ParagraphPropertyFetcher.DML_NS;
 import static org.apache.poi.xslf.model.ParagraphPropertyFetcher.PML_NS;
 
@@ -37,7 +38,7 @@ public abstract class TextBodyPropertyFetcher<T> extends PropertyFetcher<T> {
     public boolean fetch(XSLFShape shape) {
         CTTextBodyProperties props = null;
         try {
-            props = shape.selectProperty(
+            props = selectProperty(shape.getXmlObject(),
                     CTTextBodyProperties.class, TextBodyPropertyFetcher::parse, TX_BODY, BODY_PR);
             return (props != null) && fetch(props);
         } catch (XmlException e) {
index eb71e7ed0206ddc73cf560082ebca84384bce4fb..8ee5017d1f9b4cf7277e19680085ab33f690b1e3 100644 (file)
@@ -30,6 +30,7 @@ import javax.xml.stream.XMLStreamReader;
 import org.apache.poi.hpsf.ClassID;
 import org.apache.poi.ooxml.POIXMLDocumentPart.RelationPart;
 import org.apache.poi.ooxml.POIXMLException;
+import org.apache.poi.ooxml.util.XPathHelper;
 import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.openxml4j.opc.OPCPackage;
 import org.apache.poi.openxml4j.opc.PackagePart;
@@ -76,7 +77,7 @@ public class XSLFObjectShape extends XSLFGraphicFrame implements ObjectShape<XSL
         // select oleObj potentially under AlternateContent
         // usually the mc:Choice element will be selected first
         try {
-            _oleObject = selectProperty(CTOleObject.class, null, GRAPHIC, GRAPHIC_DATA, OLE_OBJ);
+            _oleObject = XPathHelper.selectProperty(getXmlObject(), CTOleObject.class, null, GRAPHIC, GRAPHIC_DATA, OLE_OBJ);
         } catch (XmlException e) {
             // ole objects should be also inside AlternateContent tags, even with ECMA 376 edition 1
             throw new IllegalStateException(e);
@@ -146,8 +147,8 @@ public class XSLFObjectShape extends XSLFGraphicFrame implements ObjectShape<XSL
 
     protected CTBlipFillProperties getBlipFill() {
         try {
-            CTPicture pic = selectProperty
-                (CTPicture.class, XSLFObjectShape::parse, GRAPHIC, GRAPHIC_DATA, OLE_OBJ, CT_PICTURE);
+            CTPicture pic = XPathHelper.selectProperty
+                (getXmlObject(), CTPicture.class, XSLFObjectShape::parse, GRAPHIC, GRAPHIC_DATA, OLE_OBJ, CT_PICTURE);
             return (pic != null) ? pic.getBlipFill() : null;
         } catch (XmlException e) {
             return null;
index 1408bedb5ba11fab0fa8cfa70cf2e75e1508c54c..4d6fc246a54fa53ebf2a5ee7b91b07f2b440ac25 100644 (file)
@@ -35,6 +35,7 @@ import javax.imageio.ImageIO;
 import javax.xml.namespace.QName;
 import javax.xml.stream.XMLStreamReader;
 
+import org.apache.poi.ooxml.util.XPathHelper;
 import org.apache.poi.openxml4j.opc.PackagePart;
 import org.apache.poi.openxml4j.opc.PackageRelationship;
 import org.apache.poi.sl.usermodel.PictureData;
@@ -175,7 +176,7 @@ public class XSLFPictureShape extends XSLFSimpleShape
         }
 
         try {
-            return selectProperty(CTBlipFillProperties.class, XSLFPictureShape::parse, BLIP_FILL);
+            return XPathHelper.selectProperty(getXmlObject(), CTBlipFillProperties.class, XSLFPictureShape::parse, BLIP_FILL);
         } catch (XmlException xe) {
             return null;
         }
index b0f94e529f4fce28d061df30fa50604081b780da..ce75a19f61d9aeac198f5376d901bec79bda6cd5 100644 (file)
@@ -17,6 +17,7 @@
 
 package org.apache.poi.xslf.usermodel;
 
+import static org.apache.poi.ooxml.util.XPathHelper.selectProperty;
 import static org.apache.poi.xslf.usermodel.XSLFShape.PML_NS;
 
 import java.util.function.Consumer;
@@ -220,7 +221,7 @@ public class XSLFPlaceholderDetails implements PlaceholderDetails {
 
     private CTApplicationNonVisualDrawingProps getNvProps() {
         try {
-            return shape.selectProperty(CTApplicationNonVisualDrawingProps.class, null, NV_CONTAINER, NV_PROPS);
+            return selectProperty(shape.getXmlObject(), CTApplicationNonVisualDrawingProps.class, null, NV_CONTAINER, NV_PROPS);
         } catch (XmlException e) {
             return null;
         }
index a77ead66fab7e44885e396dc8a65bd0781953651..f84e30699ebb6cef16f3e9fed8926ad15de16598 100644 (file)
@@ -21,13 +21,11 @@ package org.apache.poi.xslf.usermodel;
 
 import java.awt.Graphics2D;
 import java.awt.geom.Rectangle2D;
-import java.util.Locale;
 
 import javax.xml.namespace.QName;
 import javax.xml.stream.XMLStreamReader;
 
-import com.microsoft.schemas.compatibility.AlternateContentDocument;
-import com.microsoft.schemas.compatibility.AlternateContentDocument.AlternateContent;
+import org.apache.poi.ooxml.util.XPathHelper;
 import org.apache.poi.openxml4j.opc.PackagePart;
 import org.apache.poi.sl.draw.DrawFactory;
 import org.apache.poi.sl.draw.DrawPaint;
@@ -45,7 +43,6 @@ import org.apache.poi.xslf.usermodel.XSLFPropertiesDelegate.XSLFFillProperties;
 import org.apache.xmlbeans.XmlCursor;
 import org.apache.xmlbeans.XmlException;
 import org.apache.xmlbeans.XmlObject;
-import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl;
 import org.openxmlformats.schemas.drawingml.x2006.main.CTBlipFillProperties;
 import org.openxmlformats.schemas.drawingml.x2006.main.CTGradientFillProperties;
 import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupShapeProperties;
@@ -76,10 +73,6 @@ public abstract class XSLFShape implements Shape<XSLFShape,XSLFTextParagraph> {
 
     static final String DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
     static final String PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main";
-    private static final String MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
-    private static final String MAC_DML_NS = "http://schemas.microsoft.com/office/mac/drawingml/2008/main";
-
-    private static final QName ALTERNATE_CONTENT_TAG = new QName(MC_NS, "AlternateContent");
 
     private static final QName[] NV_CONTAINER = {
         new QName(PML_NS, "nvSpPr"),
@@ -93,12 +86,6 @@ public abstract class XSLFShape implements Shape<XSLFShape,XSLFTextParagraph> {
         new QName(PML_NS, "cNvPr")
     };
 
-    private static final String OSGI_ERROR =
-        "Schemas (*.xsb) for <CLASS> can't be loaded - usually this happens when OSGI " +
-        "loading is used and the thread context classloader has no reference to " +
-        "the xmlbeans classes - please either verify if the <XSB>.xsb is on the " +
-        "classpath or alternatively try to use the full ooxml-schemas-x.x.jar";
-
     private final XmlObject _shape;
     private final XSLFSheet _sheet;
     private XSLFShapeContainer _parent;
@@ -239,7 +226,7 @@ public abstract class XSLFShape implements Shape<XSLFShape,XSLFTextParagraph> {
     protected CTNonVisualDrawingProps getCNvPr() {
         try {
             if (_nvPr == null) {
-                _nvPr = selectProperty(CTNonVisualDrawingProps.class, null, NV_CONTAINER, CNV_PROPS);
+                _nvPr = XPathHelper.selectProperty(getXmlObject(), CTNonVisualDrawingProps.class, null, NV_CONTAINER, CNV_PROPS);
             }
             return _nvPr;
         } catch (XmlException e) {
@@ -322,160 +309,6 @@ public abstract class XSLFShape implements Shape<XSLFShape,XSLFTextParagraph> {
         return (resultClass.isInstance(rs[0])) ? (T)rs[0] : null;
     }
 
-    /**
-     * Internal code - API may change any time!
-     * <p>
-     * The {@link #selectProperty(Class, String)} xquery method has some performance penalties,
-     * which can be workaround by using {@link XmlCursor}. This method also takes into account
-     * that {@code AlternateContent} tags can occur anywhere on the given path.
-     * <p>
-     * It returns the first element found - the search order is:
-     * <ul>
-     *     <li>searching for a direct child</li>
-     *     <li>searching for a AlternateContent.Choice child</li>
-     *     <li>searching for a AlternateContent.Fallback child</li>
-     * </ul>
-     * Currently POI OOXML is based on the first edition of the ECMA 376 schema, which doesn't
-     * allow AlternateContent tags to show up everywhere. The factory flag is
-     * a workaround to process files based on a later edition. But it comes with the drawback:
-     * any change on the returned XmlObject aren't saved back to the underlying document -
-     * so it's a non updatable clone. If factory is null, a XmlException is
-     * thrown if the AlternateContent is not allowed by the surrounding element or if the
-     * extracted object is of the generic type XmlAnyTypeImpl.
-     *
-     * @param resultClass the requested result class
-     * @param factory a factory parse method reference to allow reparsing of elements
-     *                extracted from AlternateContent elements. Usually the enclosing XmlBeans type needs to be used
-     *                to parse the stream
-     * @param path the elements path, each array must contain at least 1 QName,
-     *             but can contain additional alternative tags
-     * @return the xml object at the path location, or null if not found
-     *
-     * @throws XmlException If factory is null, a XmlException is
-     *      thrown if the AlternateContent is not allowed by the surrounding element or if the
-     *      extracted object is of the generic type XmlAnyTypeImpl.
-     *
-     * @since POI 4.1.2
-     */
-    @SuppressWarnings("unchecked")
-    @Internal
-    public <T extends XmlObject> T selectProperty(Class<T> resultClass, ReparseFactory<T> factory, QName[]... path)
-    throws XmlException {
-        XmlObject xo = getXmlObject();
-        XmlCursor cur = xo.newCursor();
-        XmlCursor innerCur = null;
-        try {
-            innerCur = selectProperty(cur, path, 0, factory != null, false);
-            if (innerCur == null) {
-                return null;
-            }
-
-            // Pesky XmlBeans bug - see Bugzilla #49934
-            // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas
-            xo = innerCur.getObject();
-            if (xo instanceof XmlAnyTypeImpl) {
-                String errorTxt = OSGI_ERROR
-                    .replace("<CLASS>", resultClass.getSimpleName())
-                    .replace("<XSB>", resultClass.getSimpleName().toLowerCase(Locale.ROOT)+"*");
-                if (factory == null) {
-                    throw new XmlException(errorTxt);
-                } else {
-                    xo = factory.parse(innerCur.newXMLStreamReader());
-                }
-            }
-
-            return (T)xo;
-        } finally {
-            cur.dispose();
-            if (innerCur != null) {
-                innerCur.dispose();
-            }
-        }
-    }
-
-    private XmlCursor selectProperty(final XmlCursor cur, final QName[][] path, final int offset, final boolean reparseAlternate, final boolean isAlternate)
-    throws XmlException {
-        // first try the direct children
-        for (QName qn : path[offset]) {
-            if (cur.toChild(qn)) {
-                if (offset == path.length-1) {
-                    return cur;
-                }
-                cur.push();
-                XmlCursor innerCur = selectProperty(cur, path, offset+1, reparseAlternate, false);
-                if (innerCur != null) {
-                    return innerCur;
-                }
-                cur.pop();
-            }
-        }
-        // if we were called inside an alternate content handling don't look for alternates again
-        if (isAlternate || !cur.toChild(ALTERNATE_CONTENT_TAG)) {
-            return null;
-        }
-
-        // otherwise check first the choice then the fallback content
-        XmlObject xo = cur.getObject();
-        AlternateContent alterCont;
-        if (xo instanceof AlternateContent) {
-            alterCont = (AlternateContent)xo;
-        } else {
-            // Pesky XmlBeans bug - see Bugzilla #49934
-            // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas
-            if (!reparseAlternate) {
-                throw new XmlException(OSGI_ERROR
-                    .replace("<CLASS>", "AlternateContent")
-                    .replace("<XSB>", "alternatecontentelement")
-                );
-            }
-            try {
-                AlternateContentDocument acd = AlternateContentDocument.Factory.parse(cur.newXMLStreamReader());
-                alterCont = acd.getAlternateContent();
-            } catch (XmlException e) {
-                throw new XmlException("unable to parse AlternateContent element", e);
-            }
-        }
-
-        final int choices = alterCont.sizeOfChoiceArray();
-        for (int i=0; i<choices; i++) {
-            // TODO: check [Requires] attribute of [Choice] element, if we can handle the content
-            AlternateContent.Choice choice = alterCont.getChoiceArray(i);
-            XmlCursor cCur = choice.newCursor();
-            XmlCursor innerCur = null;
-            try {
-                String requiresNS = cCur.namespaceForPrefix(choice.getRequires());
-                if (MAC_DML_NS.equalsIgnoreCase(requiresNS)) {
-                    // Mac DML usually contains PDFs ...
-                    continue;
-                }
-                innerCur = selectProperty(cCur, path, offset, reparseAlternate, true);
-                if (innerCur != null) {
-                    return innerCur;
-                }
-            } finally {
-                if (innerCur != cCur) {
-                    cCur.dispose();
-                }
-            }
-        }
-
-        if (!alterCont.isSetFallback()) {
-            return null;
-        }
-
-        XmlCursor fCur = alterCont.getFallback().newCursor();
-        XmlCursor innerCur = null;
-        try {
-            innerCur = selectProperty(fCur, path, offset, reparseAlternate, true);
-            return innerCur;
-        } finally {
-            if (innerCur != fCur) {
-                fCur.dispose();
-            }
-        }
-    }
-
-
     /**
      * Walk up the inheritance tree and fetch shape properties.<p>
      *
diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSignatureLine.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSignatureLine.java
new file mode 100644 (file)
index 0000000..d09c555
--- /dev/null
@@ -0,0 +1,117 @@
+package org.apache.poi.xssf.usermodel;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.xml.namespace.QName;
+
+import com.microsoft.schemas.office.excel.CTClientData;
+import com.microsoft.schemas.office.excel.STCF;
+import com.microsoft.schemas.office.excel.STObjectType;
+import com.microsoft.schemas.office.excel.STTrueFalseBlank;
+import com.microsoft.schemas.office.office.CTSignatureLine;
+import com.microsoft.schemas.vml.CTImageData;
+import com.microsoft.schemas.vml.CTShape;
+import org.apache.poi.common.usermodel.PictureType;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.ooxml.POIXMLException;
+import org.apache.poi.ooxml.POIXMLRelation;
+import org.apache.poi.ooxml.util.XPathHelper;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.poifs.crypt.dsig.SignatureLine;
+import org.apache.poi.schemas.vmldrawing.CTXML;
+import org.apache.xmlbeans.XmlException;
+
+public class XSSFSignatureLine extends SignatureLine {
+    private static final String MS_VML_URN = "urn:schemas-microsoft-com:vml";
+
+    public void parse(XSSFSheet sheet) throws XmlException {
+        XSSFVMLDrawing vml = sheet.getVMLDrawing(false);
+        if (vml == null) {
+            return;
+        }
+        CTSignatureLine line = XPathHelper.selectProperty(vml.getDocument(), CTSignatureLine.class, null,
+              new QName[]{XSSFVMLDrawing.QNAME_VMLDRAWING},
+              new QName[]{new QName(MS_VML_URN, "shape")},
+              new QName[]{QNAME_SIGNATURE_LINE});
+
+        if (line != null) {
+            setSignatureShape(line);
+            parse();
+        }
+    }
+
+    public void add(XSSFSheet sheet, XSSFClientAnchor anchor) {
+        XSSFVMLDrawing vml = sheet.getVMLDrawing(true);
+        CTXML root = vml.getDocument().getXml();
+        add(root, (image, type) -> addPicture(image,type,sheet));
+        CTShape shape = getSignatureShape();
+        CTClientData clientData = shape.addNewClientData();
+        // LeftColumn, LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, BottomOffset
+        String anchorStr =
+            anchor.getCol1()+", "+
+            anchor.getDx1()+", "+
+            anchor.getRow1()+", "+
+            anchor.getDy1()+", "+
+            anchor.getCol2()+", "+
+            anchor.getDx2()+", "+
+            anchor.getRow2()+", "+
+            anchor.getDy2();
+//        anchorStr = "2, 0, 3, 0, 5, 136, 9, 32";
+        clientData.addAnchor(anchorStr);
+        clientData.setObjectType(STObjectType.PICT);
+        clientData.addSizeWithCells(STTrueFalseBlank.X);
+        clientData.addCF(STCF.PICT);
+        clientData.addAutoPict(STTrueFalseBlank.X);
+    }
+
+    @Override
+    protected void setRelationId(CTImageData imageData, String relId) {
+        imageData.setRelid(relId);
+    }
+
+    private String addPicture(byte[] image, PictureType type, XSSFSheet sheet) throws InvalidFormatException {
+        XSSFWorkbook wb = sheet.getWorkbook();
+        XSSFVMLDrawing vml = sheet.getVMLDrawing(false);
+        POIXMLRelation xtype = mapType(type);
+        int idx = wb.getNextPartNumber(xtype, -1);
+        POIXMLDocumentPart.RelationPart rp = vml.createRelationship(xtype, XSSFFactory.getInstance(), idx, false);
+        POIXMLDocumentPart dp = rp.getDocumentPart();
+        try (OutputStream out = dp.getPackagePart().getOutputStream()) {
+            out.write(image);
+        } catch (IOException e) {
+            throw new POIXMLException(e);
+        }
+        return rp.getRelationship().getId();
+    }
+
+
+    private static POIXMLRelation mapType(PictureType type) throws InvalidFormatException {
+        switch (type) {
+            case BMP:
+                return XSSFRelation.IMAGE_BMP;
+            case DIB:
+                return XSSFRelation.IMAGE_DIB;
+            case EMF:
+                return XSSFRelation.IMAGE_EMF;
+            case EPS:
+                return XSSFRelation.IMAGE_EPS;
+            case GIF:
+                return XSSFRelation.IMAGE_GIF;
+            case JPEG:
+                return XSSFRelation.IMAGE_JPEG;
+            case PICT:
+                return XSSFRelation.IMAGE_PICT;
+            case PNG:
+                return XSSFRelation.IMAGE_PNG;
+            case TIFF:
+                return XSSFRelation.IMAGE_TIFF;
+            case WMF:
+                return XSSFRelation.IMAGE_WMF;
+            case WPG:
+                return XSSFRelation.IMAGE_WPG;
+            default:
+                throw new InvalidFormatException("Unsupported picture format "+type);
+        }
+    }
+}
\ No newline at end of file
index 46f301bc40d3738bd037a9c43d6f816e59c7a102..10ed7eae70a1bdcdd79b6860c8068d3d90914e40 100644 (file)
@@ -22,33 +22,23 @@ import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.StringReader;
 import java.math.BigInteger;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.xml.namespace.QName;
 
-import org.apache.poi.ooxml.POIXMLDocumentPart;
-import org.apache.poi.openxml4j.opc.PackagePart;
-import org.apache.poi.ooxml.util.DocumentHelper;
-import org.apache.poi.util.ReplacingInputStream;
-import org.apache.xmlbeans.XmlCursor;
-import org.apache.xmlbeans.XmlException;
-import org.apache.xmlbeans.XmlObject;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
 import com.microsoft.schemas.office.excel.CTClientData;
 import com.microsoft.schemas.office.excel.STObjectType;
 import com.microsoft.schemas.office.office.CTIdMap;
 import com.microsoft.schemas.office.office.CTShapeLayout;
 import com.microsoft.schemas.office.office.STConnectType;
 import com.microsoft.schemas.office.office.STInsetMode;
+import com.microsoft.schemas.office.office.ShapelayoutDocument;
+import com.microsoft.schemas.vml.CTGroup;
 import com.microsoft.schemas.vml.CTPath;
 import com.microsoft.schemas.vml.CTShadow;
 import com.microsoft.schemas.vml.CTShape;
@@ -56,6 +46,17 @@ import com.microsoft.schemas.vml.CTShapetype;
 import com.microsoft.schemas.vml.STExt;
 import com.microsoft.schemas.vml.STStrokeJoinStyle;
 import com.microsoft.schemas.vml.STTrueFalse;
+import org.apache.poi.ooxml.POIXMLDocumentPart;
+import org.apache.poi.ooxml.util.DocumentHelper;
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.schemas.vmldrawing.XmlDocument;
+import org.apache.poi.util.ReplacingInputStream;
+import org.apache.xmlbeans.XmlCursor;
+import org.apache.xmlbeans.XmlException;
+import org.apache.xmlbeans.XmlObject;
+import org.apache.xmlbeans.XmlOptions;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
 
 /**
  * Represents a SpreadsheetML VML drawing.
@@ -71,29 +72,31 @@ import com.microsoft.schemas.vml.STTrueFalse;
  * considered a deprecated format included in Office Open XML for legacy reasons only and new applications that
  * need a file format for drawings are strongly encouraged to use preferentially DrawingML
  * </p>
- * 
+ *
  * <p>
  * Warning - Excel is known to put invalid XML into these files!
  *  For example, &gt;br&lt; without being closed or escaped crops up.
  * </p>
  *
  * See 6.4 VML - SpreadsheetML Drawing in Office Open XML Part 4 - Markup Language Reference.pdf
- *
- * @author Yegor Kozlov
  */
 public final class XSSFVMLDrawing extends POIXMLDocumentPart {
-    private static final QName QNAME_SHAPE_LAYOUT = new QName("urn:schemas-microsoft-com:office:office", "shapelayout");
-    private static final QName QNAME_SHAPE_TYPE = new QName("urn:schemas-microsoft-com:vml", "shapetype");
-    private static final QName QNAME_SHAPE = new QName("urn:schemas-microsoft-com:vml", "shape");
-    private static final String COMMENT_SHAPE_TYPE_ID = "_x0000_t202"; // this ID value seems to have significance to Excel >= 2010; see https://issues.apache.org/bugzilla/show_bug.cgi?id=55409
+    // this ID value seems to have significance to Excel >= 2010;
+    // see https://issues.apache.org/bugzilla/show_bug.cgi?id=55409
+    private static final String COMMENT_SHAPE_TYPE_ID = "_x0000_t202";
+
+    /**
+     * to actually process the namespace-less vmldrawing, we've introduced a proxy namespace.
+     * this namespace is active in-memory, but will be removed on saving to the file
+     */
+    public static final QName QNAME_VMLDRAWING = new QName("urn:schemas-poi-apache-org:vmldrawing", "xml");
 
     /**
      * regexp to parse shape ids, in VML they have weird form of id="_x0000_s1026"
      */
     private static final Pattern ptrn_shapeId = Pattern.compile("_x0000_s(\\d+)");
 
-    private List<QName> _qnames = new ArrayList<>();
-    private List<XmlObject> _items = new ArrayList<>();
+    private XmlDocument root;
     private String _shapeTypeId;
     private int _shapeId = 1024;
 
@@ -112,7 +115,7 @@ public final class XSSFVMLDrawing extends POIXMLDocumentPart {
      *
      * @param part the package part holding the drawing data,
      * the content type must be <code>application/vnd.openxmlformats-officedocument.drawing+xml</code>
-     * 
+     *
      * @since POI 3.14-Beta1
      */
     protected XSSFVMLDrawing(PackagePart part) throws IOException, XmlException {
@@ -120,6 +123,11 @@ public final class XSSFVMLDrawing extends POIXMLDocumentPart {
         read(getPackagePart().getInputStream());
     }
 
+    public XmlDocument getDocument() {
+        return root;
+    }
+
+
     protected void read(InputStream is) throws IOException, XmlException {
         Document doc;
         try {
@@ -133,92 +141,84 @@ public final class XSSFVMLDrawing extends POIXMLDocumentPart {
         } catch (SAXException e) {
             throw new XmlException(e.getMessage(), e);
         }
-        XmlObject root = XmlObject.Factory.parse(doc, DEFAULT_XML_OPTIONS);
-
-        _qnames = new ArrayList<>();
-        _items = new ArrayList<>();
-        for(XmlObject obj : root.selectPath("$this/xml/*")) {
-            Node nd = obj.getDomNode();
-            QName qname = new QName(nd.getNamespaceURI(), nd.getLocalName());
-            if (qname.equals(QNAME_SHAPE_LAYOUT)) {
-                _items.add(CTShapeLayout.Factory.parse(obj.xmlText(), DEFAULT_XML_OPTIONS));
-            } else if (qname.equals(QNAME_SHAPE_TYPE)) {
-                CTShapetype st = CTShapetype.Factory.parse(obj.xmlText(), DEFAULT_XML_OPTIONS);
-                _items.add(st);
-                _shapeTypeId = st.getId();
-            } else if (qname.equals(QNAME_SHAPE)) {
-                CTShape shape = CTShape.Factory.parse(obj.xmlText(), DEFAULT_XML_OPTIONS);
-                String id = shape.getId();
-                if(id != null) {
-                    Matcher m = ptrn_shapeId.matcher(id);
-                    if(m.find()) {
-                        _shapeId = Math.max(_shapeId, Integer.parseInt(m.group(1)));
+
+        XmlOptions xopt = new XmlOptions(DEFAULT_XML_OPTIONS);
+        xopt.setLoadSubstituteNamespaces(Collections.singletonMap("", QNAME_VMLDRAWING.getNamespaceURI()));
+
+        root = XmlDocument.Factory.parse(doc, xopt);
+        XmlCursor cur = root.getXml().newCursor();
+
+        try {
+            for (boolean found = cur.toFirstChild(); found; found = cur.toNextSibling()) {
+                XmlObject xo = cur.getObject();
+                if (xo instanceof CTShapetype) {
+                    _shapeTypeId = ((CTShapetype)xo).getId();
+                } else if (xo instanceof CTShape) {
+                    CTShape shape = (CTShape)xo;
+                    String id = shape.getId();
+                    if(id != null) {
+                        Matcher m = ptrn_shapeId.matcher(id);
+                        if(m.find()) {
+                            _shapeId = Math.max(_shapeId, Integer.parseInt(m.group(1)));
+                        }
                     }
                 }
-                _items.add(shape);
-            } else {
-                Document doc2;
-                try {
-                    InputSource is2 = new InputSource(new StringReader(obj.xmlText()));
-                    doc2 = DocumentHelper.readDocument(is2);
-                } catch (SAXException e) {
-                    throw new XmlException(e.getMessage(), e);
-                }
-                
-                _items.add(XmlObject.Factory.parse(doc2, DEFAULT_XML_OPTIONS));
             }
-            _qnames.add(qname);
+        } finally {
+            cur.dispose();
         }
     }
 
     protected List<XmlObject> getItems(){
-        return _items;
-    }
+        List<XmlObject> items = new ArrayList<>();
 
-    protected void write(OutputStream out) throws IOException {
-        XmlObject rootObject = XmlObject.Factory.newInstance();
-        XmlCursor rootCursor = rootObject.newCursor();
-        rootCursor.toNextToken();
-        rootCursor.beginElement("xml");
-
-        for(int i=0; i < _items.size(); i++){
-            XmlCursor xc = _items.get(i).newCursor();
-            rootCursor.beginElement(_qnames.get(i));
-            while(xc.toNextToken() == XmlCursor.TokenType.ATTR) {
-                Node anode = xc.getDomNode();
-                rootCursor.insertAttributeWithValue(anode.getLocalName(), anode.getNamespaceURI(), anode.getNodeValue());
+        XmlCursor cur = root.getXml().newCursor();
+        try {
+            for (boolean found = cur.toFirstChild(); found; found = cur.toNextSibling()) {
+                items.add(cur.getObject());
             }
-            xc.toStartDoc();
-            xc.copyXmlContents(rootCursor);
-            rootCursor.toNextToken();
-            xc.dispose();
+        } finally {
+            cur.dispose();
         }
-        rootCursor.dispose();
 
-        rootObject.save(out, DEFAULT_XML_OPTIONS);
+        return items;
+    }
+
+    protected void write(OutputStream out) throws IOException {
+        XmlOptions xopt = new XmlOptions(DEFAULT_XML_OPTIONS);
+        xopt.setSaveImplicitNamespaces(Collections.singletonMap("", QNAME_VMLDRAWING.getNamespaceURI()));
+        root.save(out, xopt);
     }
 
     @Override
     protected void commit() throws IOException {
         PackagePart part = getPackagePart();
-        OutputStream out = part.getOutputStream();
-        write(out);
-        out.close();
+        try (OutputStream out = part.getOutputStream()) {
+            write(out);
+        }
     }
 
     /**
      * Initialize a new Speadsheet VML drawing
      */
     private void newDrawing(){
-        CTShapeLayout layout = CTShapeLayout.Factory.newInstance();
+        root = XmlDocument.Factory.newInstance();
+        XmlCursor xml = root.addNewXml().newCursor();
+
+        ShapelayoutDocument layDoc = ShapelayoutDocument.Factory.newInstance();
+        CTShapeLayout layout = layDoc.addNewShapelayout();
         layout.setExt(STExt.EDIT);
         CTIdMap idmap = layout.addNewIdmap();
         idmap.setExt(STExt.EDIT);
         idmap.setData("1");
-        _items.add(layout);
-        _qnames.add(QNAME_SHAPE_LAYOUT);
 
-        CTShapetype shapetype = CTShapetype.Factory.newInstance();
+        xml.toEndToken();
+        XmlCursor layCur = layDoc.newCursor();
+        layCur.copyXmlContents(xml);
+        layCur.dispose();
+
+        CTGroup grp = CTGroup.Factory.newInstance();
+        CTShapetype shapetype = grp.addNewShapetype();
         _shapeTypeId = COMMENT_SHAPE_TYPE_ID;
         shapetype.setId(_shapeTypeId);
         shapetype.setCoordsize("21600,21600");
@@ -228,12 +228,17 @@ public final class XSSFVMLDrawing extends POIXMLDocumentPart {
         CTPath path = shapetype.addNewPath();
         path.setGradientshapeok(STTrueFalse.T);
         path.setConnecttype(STConnectType.RECT);
-        _items.add(shapetype);
-        _qnames.add(QNAME_SHAPE_TYPE);
+
+        xml.toEndToken();
+        XmlCursor grpCur = grp.newCursor();
+        grpCur.copyXmlContents(xml);
+        grpCur.dispose();
     }
 
     protected CTShape newCommentShape(){
-        CTShape shape = CTShape.Factory.newInstance();
+        CTGroup grp = CTGroup.Factory.newInstance();
+
+        CTShape shape = grp.addNewShape();
         shape.setId("_x0000_s" + (++_shapeId));
         shape.setType("#" + _shapeTypeId);
         shape.setStyle("position:absolute; visibility:hidden");
@@ -254,8 +259,16 @@ public final class XSSFVMLDrawing extends POIXMLDocumentPart {
         cldata.addNewAutoFill().setStringValue("False");
         cldata.addNewRow().setBigIntegerValue(BigInteger.valueOf(0));
         cldata.addNewColumn().setBigIntegerValue(BigInteger.valueOf(0));
-        _items.add(shape);
-        _qnames.add(QNAME_SHAPE);
+
+        XmlCursor xml = root.getXml().newCursor();
+        xml.toEndToken();
+        XmlCursor grpCur = grp.newCursor();
+        grpCur.copyXmlContents(xml);
+        xml.toPrevSibling();
+        shape = (CTShape)xml.getObject();
+        grpCur.dispose();
+        xml.dispose();
+
         return shape;
     }
 
@@ -265,26 +278,45 @@ public final class XSSFVMLDrawing extends POIXMLDocumentPart {
      * @return the comment shape or <code>null</code>
      */
     public CTShape findCommentShape(int row, int col){
-        for(XmlObject itm : _items){
-            if(itm instanceof CTShape){
-                CTShape sh = (CTShape)itm;
-                if(sh.sizeOfClientDataArray() > 0){
-                    CTClientData cldata = sh.getClientDataArray(0);
-                    if(cldata.getObjectType() == STObjectType.NOTE){
-                        int crow = cldata.getRowArray(0).intValue();
-                        int ccol = cldata.getColumnArray(0).intValue();
-                        if(crow == row && ccol == col) {
-                            return sh;
-                        }
-                    }
-                }
+        XmlCursor cur = root.getXml().newCursor();
+        for (boolean found = cur.toFirstChild(); found; found = cur.toNextSibling()) {
+            XmlObject itm = cur.getObject();
+            if (matchCommentShape(itm, row, col)) {
+                return (CTShape)itm;
             }
         }
         return null;
     }
 
+    private boolean matchCommentShape(XmlObject itm, int row, int col) {
+        if (!(itm instanceof CTShape)) {
+            return false;
+        }
+
+        CTShape sh = (CTShape)itm;
+        if (sh.sizeOfClientDataArray() == 0) {
+            return false;
+        }
+
+        CTClientData cldata = sh.getClientDataArray(0);
+        if(cldata.getObjectType() != STObjectType.NOTE) {
+            return false;
+        }
+
+        int crow = cldata.getRowArray(0).intValue();
+        int ccol = cldata.getColumnArray(0).intValue();
+        return (crow == row && ccol == col);
+    }
+
     protected boolean removeCommentShape(int row, int col){
-        CTShape shape = findCommentShape(row, col);
-        return shape != null && _items.remove(shape);
+        XmlCursor cur = root.getXml().newCursor();
+        for (boolean found = cur.toFirstChild(); found; found = cur.toNextSibling()) {
+            XmlObject itm = cur.getObject();
+            if (matchCommentShape(itm, row, col)) {
+                cur.removeXml();
+                return true;
+            }
+        }
+        return false;
     }
 }
\ No newline at end of file
diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java
new file mode 100644 (file)
index 0000000..95b88ba
--- /dev/null
@@ -0,0 +1,91 @@
+/* ====================================================================
+   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.poi.xwpf.usermodel;
+
+import javax.xml.namespace.QName;
+
+import com.microsoft.schemas.office.office.CTSignatureLine;
+import com.microsoft.schemas.vml.CTImageData;
+import org.apache.poi.common.usermodel.PictureType;
+import org.apache.poi.ooxml.util.XPathHelper;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.poifs.crypt.dsig.SignatureLine;
+import org.apache.xmlbeans.XmlException;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPicture;
+
+public class XWPFSignatureLine extends SignatureLine {
+    static final String NS_OOXML_WP_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
+    private static final String MS_VML_URN = "urn:schemas-microsoft-com:vml";
+
+    private CTSignatureLine line;
+
+    public void parse(XWPFDocument doc) throws XmlException {
+        line = XPathHelper.selectProperty(doc.getDocument(), CTSignatureLine.class, null,
+            new QName[]{new QName(NS_OOXML_WP_MAIN, "body")},
+            new QName[]{new QName(NS_OOXML_WP_MAIN, "p")},
+            new QName[]{new QName(NS_OOXML_WP_MAIN, "r")},
+            new QName[]{new QName(NS_OOXML_WP_MAIN, "pict")},
+            new QName[]{new QName(MS_VML_URN, "shape")},
+            new QName[]{QNAME_SIGNATURE_LINE});
+        if (line != null) {
+            setSignatureShape(line);
+            parse();
+        }
+    }
+
+    public void add(XWPFParagraph paragraph) {
+        XWPFRun r = paragraph.createRun();
+        CTPicture pict = r.getCTR().addNewPict();
+        add(pict, (image, type) -> paragraph.getDocument().addPictureData(image, mapType(type)));
+    }
+
+    @Override
+    protected void setRelationId(CTImageData imageData, String relId) {
+        imageData.setId2(relId);
+    }
+
+    private static int mapType(PictureType type) throws InvalidFormatException {
+        switch (type) {
+            case BMP:
+                return Document.PICTURE_TYPE_BMP;
+            case DIB:
+                return Document.PICTURE_TYPE_DIB;
+            case EMF:
+                return Document.PICTURE_TYPE_EMF;
+            case EPS:
+                return Document.PICTURE_TYPE_EPS;
+            case GIF:
+                return Document.PICTURE_TYPE_GIF;
+            case JPEG:
+                return Document.PICTURE_TYPE_JPEG;
+            case PICT:
+                return Document.PICTURE_TYPE_PICT;
+            case PNG:
+                return Document.PICTURE_TYPE_PNG;
+            case TIFF:
+                return Document.PICTURE_TYPE_TIFF;
+            case WMF:
+                return Document.PICTURE_TYPE_WMF;
+            case WPG:
+                return Document.PICTURE_TYPE_WPG;
+            default:
+                throw new InvalidFormatException("Unsupported picture format "+type);
+        }
+    }
+}
index 19d48ab0ff31821e05a7c11bd38a2b5244bd0e2e..a567df5816d6c854cd6cc9a6e37d737e9b95254e 100644 (file)
@@ -43,4 +43,8 @@
     <xb:package>com.microsoft.schemas.compatibility</xb:package>\r
   </xb:namespace>\r
 \r
+  <xb:namespace uri="urn:schemas-poi-apache-org:vmldrawing">\r
+    <xb:package>org.apache.poi.schemas.vmldrawing</xb:package>\r
+  </xb:namespace>\r
+\r
 </xb:config>
\ No newline at end of file
diff --git a/src/ooxml/resources/org/apache/poi/schemas/vmlDrawing.xsd b/src/ooxml/resources/org/apache/poi/schemas/vmlDrawing.xsd
new file mode 100644 (file)
index 0000000..83e0cf2
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xsd:schema
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+  xmlns="urn:schemas-poi-apache-org:vmldrawing"
+  targetNamespace="urn:schemas-poi-apache-org:vmldrawing"
+>
+  <xsd:import namespace="urn:schemas-microsoft-com:vml" schemaLocation="vml-main.xsd"/>
+  <xsd:import namespace="urn:schemas-microsoft-com:office:office" schemaLocation="vml-officeDrawing.xsd"/>
+  <xsd:import namespace="urn:schemas-microsoft-com:office:excel" schemaLocation="vml-spreadsheetDrawing.xsd"/>
+  <xsd:element name="xml" type="CT_XML"/>
+  <xsd:complexType name="CT_XML">
+      <xsd:choice maxOccurs="unbounded">
+        <xsd:any namespace="urn:schemas-microsoft-com:office:office"/>
+        <xsd:any namespace="urn:schemas-microsoft-com:vml"/>
+        <xsd:any namespace="urn:schemas-microsoft-com:office:excel"/>
+      </xsd:choice>
+  </xsd:complexType>
+</xsd:schema>
index 012890264adb150caaf8b8e35acc3ed650d20d58..94fbf72fed435f6f0cab9c0e06203aafd4d32e4b 100644 (file)
@@ -62,11 +62,14 @@ import java.security.cert.X509Certificate;
 import java.security.interfaces.RSAPublicKey;
 import java.security.spec.RSAKeyGenParameterSpec;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
 
 import javax.xml.crypto.MarshalException;
 import javax.xml.crypto.dsig.CanonicalizationMethod;
@@ -76,6 +79,7 @@ import javax.xml.crypto.dsig.dom.DOMSignContext;
 import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo;
 import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.POIDataSamples;
+import org.apache.poi.ooxml.POIXMLDocument;
 import org.apache.poi.ooxml.util.DocumentHelper;
 import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.openxml4j.opc.OPCPackage;
@@ -98,9 +102,16 @@ import org.apache.poi.util.IOUtils;
 import org.apache.poi.util.LocaleUtil;
 import org.apache.poi.util.POILogFactory;
 import org.apache.poi.util.POILogger;
+import org.apache.poi.util.TempFile;
 import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFSignatureLine;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFSignatureLine;
 import org.apache.xmlbeans.SystemProperties;
+import org.apache.xmlbeans.XmlException;
 import org.apache.xmlbeans.XmlObject;
 import org.bouncycastle.asn1.DEROctetString;
 import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
@@ -855,6 +866,100 @@ public class TestSignatureInfo {
         assertEquals(CanonicalizationMethod.INCLUSIVE, sic.getCanonicalizationMethod());
     }
 
+    private interface XmlDocumentPackageInit {
+        POIXMLDocument init(SignatureLine line, OPCPackage pkg) throws IOException, XmlException;
+    }
+
+    @Test
+    public void testSignatureImage() throws Exception {
+        initKeyPair();
+
+        List<Supplier<SignatureLine>> lines = Arrays.asList(XSSFSignatureLine::new, XWPFSignatureLine::new);
+        for (Supplier<SignatureLine> sup : lines) {
+            SignatureLine line = sup.get();
+            line.setSuggestedSigner("Jack Sparrow");
+            line.setSuggestedSigner2("Captain");
+            line.setSuggestedSignerEmail("jack.bl@ck.perl");
+            line.setInvalidStamp("Bungling!");
+            line.setPlainSignature(testdata.readFile("jack-sign.emf"));
+
+            String[] ext = { "" };
+            BiFunction<SignatureLine,String[],POIXMLDocument> init =
+                (line instanceof XSSFSignatureLine)
+                ? this::initSignatureImageXSSF
+                : this::initSignatureImageXWPF;
+
+            File signDoc;
+            try (POIXMLDocument xmlDoc = init.apply(line,ext)) {
+                signDoc = TempFile.createTempFile("visual-signature", ext[0]);
+                try (FileOutputStream fos = new FileOutputStream(signDoc)) {
+                    xmlDoc.write(fos);
+                }
+            }
+
+            try (OPCPackage pkg = OPCPackage.open(signDoc, PackageAccess.READ_WRITE)) {
+                SignatureConfig sic = new SignatureConfig();
+                sic.setKey(keyPair.getPrivate());
+                sic.setSigningCertificateChain(Collections.singletonList(x509));
+
+                line.updateSignatureConfig(sic);
+
+                sic.setDigestAlgo(HashAlgorithm.sha1);
+                SignatureInfo si = new SignatureInfo();
+                si.setOpcPackage(pkg);
+                si.setSignatureConfig(sic);
+                // hash > sha1 doesn't work in excel viewer ...
+                si.confirmSignature();
+            }
+
+            XmlDocumentPackageInit reinit =
+                (line instanceof XSSFSignatureLine)
+                ? this::initSignatureImageXSSF
+                : this::initSignatureImageXWPF;
+
+            try (OPCPackage pkg = OPCPackage.open(signDoc, PackageAccess.READ)) {
+                SignatureLine line2 = sup.get();
+                try (POIXMLDocument doc = reinit.init(line2, pkg)) {
+                    line2.parse();
+                    assertEquals(line.getSuggestedSigner(), line2.getSuggestedSigner());
+                    assertEquals(line.getSuggestedSigner2(), line2.getSuggestedSigner2());
+                    assertEquals(line.getSuggestedSignerEmail(), line2.getSuggestedSignerEmail());
+                }
+
+                pkg.revert();
+            }
+        }
+    }
+
+    private XWPFDocument initSignatureImageXWPF(SignatureLine line, String[] ext) {
+        XWPFDocument doc = new XWPFDocument();
+        ((XWPFSignatureLine)line).add(doc.createParagraph());
+        ext[0] = ".docx";
+        return doc;
+    }
+
+    private XWPFDocument initSignatureImageXWPF(SignatureLine line, OPCPackage pkg) throws IOException, XmlException {
+        XWPFDocument doc = new XWPFDocument(pkg);
+        ((XWPFSignatureLine)line).parse(doc);
+        return doc;
+    }
+
+    private XSSFWorkbook initSignatureImageXSSF(SignatureLine line, String[] ext) {
+        XSSFWorkbook xls = new XSSFWorkbook();
+        XSSFSheet sheet = xls.createSheet();
+        XSSFClientAnchor anchor = new XSSFClientAnchor(0,0,0,0,3,3,8,13);
+        ((XSSFSignatureLine)line).add(sheet, anchor);
+        ext[0] = ".xlsx";
+        return xls;
+    }
+
+    private XSSFWorkbook initSignatureImageXSSF(SignatureLine line, OPCPackage pkg) throws IOException, XmlException {
+        XSSFWorkbook xls = new XSSFWorkbook(pkg);
+        ((XSSFSignatureLine)line).parse(xls.getSheetAt(0));
+        return xls;
+    }
+
+
     private SignatureConfig prepareConfig(String pfxInput) throws Exception {
         initKeyPair(pfxInput);
 
index 4bc178d2ca27054d428b436a0f9fbb89f40b08c2..b57c255b403bd0cf0bbae4b03fe1b3241068dddf 100644 (file)
@@ -16,6 +16,8 @@
 ==================================================================== */
 package org.apache.poi.xssf.usermodel;
 
+import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS;
+import static org.apache.poi.xssf.usermodel.XSSFVMLDrawing.QNAME_VMLDRAWING;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -27,14 +29,10 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.math.BigInteger;
+import java.util.Collections;
 import java.util.List;
 import java.util.regex.Pattern;
 
-import org.apache.poi.POIDataSamples;
-import org.apache.xmlbeans.XmlException;
-import org.apache.xmlbeans.XmlObject;
-import org.junit.Test;
-
 import com.microsoft.schemas.office.excel.CTClientData;
 import com.microsoft.schemas.office.excel.STObjectType;
 import com.microsoft.schemas.office.excel.STTrueFalseBlank;
@@ -46,6 +44,11 @@ import com.microsoft.schemas.vml.CTShape;
 import com.microsoft.schemas.vml.CTShapetype;
 import com.microsoft.schemas.vml.STExt;
 import com.microsoft.schemas.vml.STTrueFalse;
+import org.apache.poi.POIDataSamples;
+import org.apache.xmlbeans.XmlException;
+import org.apache.xmlbeans.XmlObject;
+import org.apache.xmlbeans.XmlOptions;
+import org.junit.Test;
 
 public class TestXSSFVMLDrawing {
 
@@ -59,7 +62,7 @@ public class TestXSSFVMLDrawing {
         assertEquals(STExt.EDIT, layout.getExt());
         assertEquals(STExt.EDIT, layout.getIdmap().getExt());
         assertEquals("1", layout.getIdmap().getData());
-    
+
         assertTrue(items.get(1) instanceof CTShapetype);
         CTShapetype type = (CTShapetype)items.get(1);
         assertEquals("21600,21600", type.getCoordsize());
@@ -70,6 +73,7 @@ public class TestXSSFVMLDrawing {
         assertEquals(STConnectType.RECT, type.getPathArray(0).getConnecttype());
 
         CTShape shape = vml.newCommentShape();
+        items = vml.getItems();
         assertEquals(3, items.size());
         assertSame(items.get(2),  shape);
         assertEquals("#_x0000_t202", shape.getType());
@@ -110,7 +114,7 @@ public class TestXSSFVMLDrawing {
 
     @Test
     public void testFindCommentShape() throws IOException, XmlException {
-        
+
         XSSFVMLDrawing vml = new XSSFVMLDrawing();
         try (InputStream stream = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("vmlDrawing1.vml")) {
             vml.read(stream);
@@ -158,17 +162,21 @@ public class TestXSSFVMLDrawing {
         assertNull(vml.findCommentShape(0, 0));
 
     }
-    
+
     @Test
     public void testEvilUnclosedBRFixing() throws IOException, XmlException {
         XSSFVMLDrawing vml = new XSSFVMLDrawing();
         try (InputStream stream = POIDataSamples.getOpenXML4JInstance().openResourceAsStream("bug-60626.vml")) {
             vml.read(stream);
         }
+
+        XmlOptions xopt = new XmlOptions(DEFAULT_XML_OPTIONS);
+        xopt.setSaveImplicitNamespaces(Collections.singletonMap("", QNAME_VMLDRAWING.getNamespaceURI()));
+
         Pattern p = Pattern.compile("<br/>");
         int count = 0;
         for (XmlObject xo : vml.getItems()) {
-            String[] split = p.split(xo.toString());
+            String[] split = p.split(xo.xmlText(xopt));
             count += split.length-1;
         }
         assertEquals(16, count);
diff --git a/test-data/xmldsign/jack-sign.emf b/test-data/xmldsign/jack-sign.emf
new file mode 100644 (file)
index 0000000..dafd361
Binary files /dev/null and b/test-data/xmldsign/jack-sign.emf differ