From 2a292cb42daa66f193e2c18aebd0c5d2e6ab365c Mon Sep 17 00:00:00 2001
From: Andreas Beeker
Date: Sat, 10 Oct 2020 23:33:26 +0000
Subject: [PATCH] #64773 - Visual signatures for .xlsx/.docx
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1882394 13f79535-47bb-0310-9956-ffa450edef68
---
build.xml | 2 +-
.../poi/common/usermodel/PictureType.java | 51 ++
.../ooxml-lite/java9/module-info.class | Bin 1483 -> 1528 bytes
.../ooxml-lite/java9/module-info.java | 2 +
.../ooxml-schemas/java9/module-info.class | Bin 2235 -> 2280 bytes
.../ooxml-schemas/java9/module-info.java | 2 +
.../apache/poi/ooxml/POIXMLDocumentPart.java | 3 +-
.../apache/poi/ooxml/util/XPathHelper.java | 187 ++++++-
.../poi/poifs/crypt/dsig/SignatureConfig.java | 61 ++-
.../poi/poifs/crypt/dsig/SignatureLine.java | 489 ++++++++++++++++++
.../dsig/facets/OOXMLSignatureFacet.java | 39 +-
.../xslf/model/ParagraphPropertyFetcher.java | 4 +-
.../xslf/model/TextBodyPropertyFetcher.java | 3 +-
.../poi/xslf/usermodel/XSLFObjectShape.java | 7 +-
.../poi/xslf/usermodel/XSLFPictureShape.java | 3 +-
.../usermodel/XSLFPlaceholderDetails.java | 3 +-
.../apache/poi/xslf/usermodel/XSLFShape.java | 171 +-----
.../poi/xssf/usermodel/XSSFSignatureLine.java | 117 +++++
.../poi/xssf/usermodel/XSSFVMLDrawing.java | 236 +++++----
.../poi/xwpf/usermodel/XWPFSignatureLine.java | 91 ++++
.../apache/poi/schemas/ooxmlSchemas.xsdconfig | 4 +
.../org/apache/poi/schemas/vmlDrawing.xsd | 18 +
.../poifs/crypt/dsig/TestSignatureInfo.java | 105 ++++
.../xssf/usermodel/TestXSSFVMLDrawing.java | 26 +-
test-data/xmldsign/jack-sign.emf | Bin 0 -> 29868 bytes
25 files changed, 1323 insertions(+), 301 deletions(-)
create mode 100644 src/java/org/apache/poi/common/usermodel/PictureType.java
create mode 100644 src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java
create mode 100644 src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSignatureLine.java
create mode 100644 src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java
create mode 100644 src/ooxml/resources/org/apache/poi/schemas/vmlDrawing.xsd
create mode 100644 test-data/xmldsign/jack-sign.emf
diff --git a/build.xml b/build.xml
index 46308cc2b5..717b064a07 100644
--- a/build.xml
+++ b/build.xml
@@ -792,7 +792,7 @@ under the License.
-
+
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
index 0000000000..f0fef79f31
--- /dev/null
+++ b/src/java/org/apache/poi/common/usermodel/PictureType.java
@@ -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;
+ }
+}
diff --git a/src/multimodule/ooxml-lite/java9/module-info.class b/src/multimodule/ooxml-lite/java9/module-info.class
index 78ddc1f01281747b9eda2de66edf395cc37b05c4..0eb072af3d0d9d892ac9617fcc0742f78a26fc1a 100644
GIT binary patch
delta 238
zcmX@j{ezq9)W2Q(7#J8#8LT#PEo5X=oLtK&&Rdq7lTwseo|%^}!XP_&C8G?BJR^g_
zCi=ONn1{MYdpyQbt*cyNo0|%JL1!6LYPj+N&
K<+cE`!2|$GH8+(2
delta 218
zcmeyteVUu=)W2Q(7#J8#87ww(Eo7X0iBXD0mXX0=vLTD~eEij3xyWm)wYmreFzm1Y#1oWa`43iJyD0{~x8H4Oj&
diff --git a/src/multimodule/ooxml-lite/java9/module-info.java b/src/multimodule/ooxml-lite/java9/module-info.java
index 77a3bb34b3..6736f8669c 100644
--- a/src/multimodule/ooxml-lite/java9/module-info.java
+++ b/src/multimodule/ooxml-lite/java9/module-info.java
@@ -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;
diff --git a/src/multimodule/ooxml-schemas/java9/module-info.class b/src/multimodule/ooxml-schemas/java9/module-info.class
index 339c9d793ed7d34bb1cdf4721ae65a906c08efa6..f2a86deda0182602db0f564679563a3e2cb44da6 100644
GIT binary patch
delta 101
zcmdlj_(G8D)W2Q(7#J8#8G<%)t!K9uVF+YoP|Pn%*H0`+OwLHvFUZf-F9tGl6N~lB
ra&uCO63a95(i<2U7=gMVfRTY2$oMk(EW0$L^5mE7XSoAlQVa|LFXS3w
delta 78
zcmaDMxLc6x)W2Q(7#J8#83HzPt!FoAU|?Vbav^|`ff>kn$H2m%0Hm22*cyNo0|%JL
Q1!6KNO%~ud%L--!0Ae8vCjbBd
diff --git a/src/multimodule/ooxml-schemas/java9/module-info.java b/src/multimodule/ooxml-schemas/java9/module-info.java
index 974467061f..102ab6ad69 100644
--- a/src/multimodule/ooxml-schemas/java9/module-info.java
+++ b/src/multimodule/ooxml-schemas/java9/module-info.java
@@ -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
diff --git a/src/ooxml/java/org/apache/poi/ooxml/POIXMLDocumentPart.java b/src/ooxml/java/org/apache/poi/ooxml/POIXMLDocumentPart.java
index 9346942e6c..fd9b032ef6 100644
--- a/src/ooxml/java/org/apache/poi/ooxml/POIXMLDocumentPart.java
+++ b/src/ooxml/java/org/apache/poi/ooxml/POIXMLDocumentPart.java
@@ -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 {
diff --git a/src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java b/src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java
index ef492bd223..0e1700317f 100644
--- a/src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java
+++ b/src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java
@@ -17,14 +17,35 @@
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 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 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!
+ *
+ * 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.
+ *
+ * It returns the first element found - the search order is:
+ *
+ * - searching for a direct child
+ * - searching for a AlternateContent.Choice child
+ * - searching for a AlternateContent.Fallback child
+ *
+ * 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 selectProperty(XmlObject startObject, Class resultClass, XSLFShape.ReparseFactory 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("", resultClass.getSimpleName())
+ .replace("", 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("", "AlternateContent")
+ .replace("", "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 opcPackage = new ThreadLocal<>();
- private ThreadLocal signatureFactory = new ThreadLocal<>();
- private ThreadLocal keyInfoFactory = new ThreadLocal<>();
- private ThreadLocal provider = new ThreadLocal<>();
+ private final ThreadLocal opcPackage = new ThreadLocal<>();
+ private final ThreadLocal signatureFactory = new ThreadLocal<>();
+ private final ThreadLocal keyInfoFactory = new ThreadLocal<>();
+ private final ThreadLocal provider = new ThreadLocal<>();
private List 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
index 0000000000..8d9ffe4024
--- /dev/null
+++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java
@@ -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");
+ }
+ }
+
+}
diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
index 0eff7e3186..669d315e60 100644
--- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
+++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
@@ -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) {
diff --git a/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java b/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java
index 9a722c59f6..7cce4c64a7 100644
--- a/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java
+++ b/src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java
@@ -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 extends PropertyFetcher {
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);
}
diff --git a/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java b/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java
index 9bb02504f8..2c0cfef0b8 100644
--- a/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java
+++ b/src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java
@@ -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 extends PropertyFetcher {
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) {
diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java
index eb71e7ed02..8ee5017d1f 100644
--- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java
+++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java
@@ -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 {
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 {
new QName(PML_NS, "cNvPr")
};
- private static final String OSGI_ERROR =
- "Schemas (*.xsb) for 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 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 {
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 {
return (resultClass.isInstance(rs[0])) ? (T)rs[0] : null;
}
- /**
- * Internal code - API may change any time!
- *
- * 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.
- *
- * It returns the first element found - the search order is:
- *
- * - searching for a direct child
- * - searching for a AlternateContent.Choice child
- * - searching for a AlternateContent.Fallback child
- *
- * 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 selectProperty(Class resultClass, ReparseFactory 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("", resultClass.getSimpleName())
- .replace("", 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("", "AlternateContent")
- .replace("", "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
*
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
index 0000000000..d09c555462
--- /dev/null
+++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSignatureLine.java
@@ -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
diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFVMLDrawing.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFVMLDrawing.java
index 46f301bc40..10ed7eae70 100644
--- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFVMLDrawing.java
+++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFVMLDrawing.java
@@ -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
*
- *
+ *
*
* Warning - Excel is known to put invalid XML into these files!
* For example, >br< without being closed or escaped crops up.
*
*
* 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 _qnames = new ArrayList<>();
- private List _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 application/vnd.openxmlformats-officedocument.drawing+xml
- *
+ *
* @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 getItems(){
- return _items;
- }
+ List 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 null
*/
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
index 0000000000..95b88bac04
--- /dev/null
+++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java
@@ -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);
+ }
+ }
+}
diff --git a/src/ooxml/resources/org/apache/poi/schemas/ooxmlSchemas.xsdconfig b/src/ooxml/resources/org/apache/poi/schemas/ooxmlSchemas.xsdconfig
index 19d48ab0ff..a567df5816 100644
--- a/src/ooxml/resources/org/apache/poi/schemas/ooxmlSchemas.xsdconfig
+++ b/src/ooxml/resources/org/apache/poi/schemas/ooxmlSchemas.xsdconfig
@@ -43,4 +43,8 @@
com.microsoft.schemas.compatibility
+
+ org.apache.poi.schemas.vmldrawing
+
+
\ 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
index 0000000000..83e0cf2073
--- /dev/null
+++ b/src/ooxml/resources/org/apache/poi/schemas/vmlDrawing.xsd
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java
index 012890264a..94fbf72fed 100644
--- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java
+++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java
@@ -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> lines = Arrays.asList(XSSFSignatureLine::new, XWPFSignatureLine::new);
+ for (Supplier 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 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);
diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFVMLDrawing.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFVMLDrawing.java
index 4bc178d2ca..b57c255b40 100644
--- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFVMLDrawing.java
+++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFVMLDrawing.java
@@ -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("
");
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
index 0000000000000000000000000000000000000000..dafd361fb3adf5279fcde664dc187e0dff33ebb1
GIT binary patch
literal 29868
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
literal 0
HcmV?d00001
--
2.39.5