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