123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- /* ====================================================================
- 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");
- }
- }
-
- }
|