You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SignatureLine.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.poifs.crypt.dsig;
  16. import java.awt.AlphaComposite;
  17. import java.awt.Color;
  18. import java.awt.Font;
  19. import java.awt.GradientPaint;
  20. import java.awt.Graphics2D;
  21. import java.awt.RenderingHints;
  22. import java.awt.Shape;
  23. import java.awt.font.FontRenderContext;
  24. import java.awt.font.LineBreakMeasurer;
  25. import java.awt.font.TextAttribute;
  26. import java.awt.font.TextLayout;
  27. import java.awt.geom.AffineTransform;
  28. import java.awt.geom.Dimension2D;
  29. import java.awt.geom.Rectangle2D;
  30. import java.awt.image.BufferedImage;
  31. import java.io.ByteArrayOutputStream;
  32. import java.io.IOException;
  33. import java.text.AttributedCharacterIterator;
  34. import java.text.AttributedString;
  35. import java.util.UUID;
  36. import javax.imageio.ImageIO;
  37. import javax.xml.namespace.QName;
  38. import com.microsoft.schemas.office.office.CTSignatureLine;
  39. import com.microsoft.schemas.office.office.STTrueFalse;
  40. import com.microsoft.schemas.vml.CTGroup;
  41. import com.microsoft.schemas.vml.CTImageData;
  42. import com.microsoft.schemas.vml.CTShape;
  43. import com.microsoft.schemas.vml.STExt;
  44. import org.apache.poi.common.usermodel.PictureType;
  45. import org.apache.poi.hpsf.ClassID;
  46. import org.apache.poi.ooxml.POIXMLException;
  47. import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
  48. import org.apache.poi.poifs.filesystem.FileMagic;
  49. import org.apache.poi.sl.draw.DrawPictureShape;
  50. import org.apache.poi.sl.draw.ImageRenderer;
  51. import org.apache.xmlbeans.XmlCursor;
  52. import org.apache.xmlbeans.XmlObject;
  53. /**
  54. * Base class for SignatureLines (XSSF,XWPF only)
  55. */
  56. public abstract class SignatureLine {
  57. private static final String MS_OFFICE_URN = "urn:schemas-microsoft-com:office:office";
  58. protected static final QName QNAME_SIGNATURE_LINE = new QName(MS_OFFICE_URN, "signatureline");
  59. private ClassID setupId;
  60. private Boolean allowComments;
  61. private String signingInstructions = "Before signing the document, verify that the content you are signing is correct.";
  62. private String suggestedSigner;
  63. private String suggestedSigner2;
  64. private String suggestedSignerEmail;
  65. private String caption;
  66. private String invalidStamp = "invalid";
  67. private byte[] plainSignature;
  68. private String contentType;
  69. private CTShape signatureShape;
  70. public ClassID getSetupId() {
  71. return setupId;
  72. }
  73. public void setSetupId(ClassID setupId) {
  74. this.setupId = setupId;
  75. }
  76. public Boolean getAllowComments() {
  77. return allowComments;
  78. }
  79. public void setAllowComments(Boolean allowComments) {
  80. this.allowComments = allowComments;
  81. }
  82. public String getSigningInstructions() {
  83. return signingInstructions;
  84. }
  85. public void setSigningInstructions(String signingInstructions) {
  86. this.signingInstructions = signingInstructions;
  87. }
  88. public String getSuggestedSigner() {
  89. return suggestedSigner;
  90. }
  91. public void setSuggestedSigner(String suggestedSigner) {
  92. this.suggestedSigner = suggestedSigner;
  93. }
  94. public String getSuggestedSigner2() {
  95. return suggestedSigner2;
  96. }
  97. public void setSuggestedSigner2(String suggestedSigner2) {
  98. this.suggestedSigner2 = suggestedSigner2;
  99. }
  100. public String getSuggestedSignerEmail() {
  101. return suggestedSignerEmail;
  102. }
  103. public void setSuggestedSignerEmail(String suggestedSignerEmail) {
  104. this.suggestedSignerEmail = suggestedSignerEmail;
  105. }
  106. /**
  107. * The default caption
  108. * @return "[suggestedSigner] \n [suggestedSigner2] \n [suggestedSignerEmail]"
  109. */
  110. public String getDefaultCaption() {
  111. return suggestedSigner+"\n"+suggestedSigner2+"\n"+suggestedSignerEmail;
  112. }
  113. public String getCaption() {
  114. return caption;
  115. }
  116. /**
  117. * Set the caption - use maximum of three lines separated by "\n".
  118. * Defaults to {@link #getDefaultCaption()}
  119. * @param caption the signature caption
  120. */
  121. public void setCaption(String caption) {
  122. this.caption = caption;
  123. }
  124. public String getInvalidStamp() {
  125. return invalidStamp;
  126. }
  127. /**
  128. * Sets the text stamped over the signature image when the document got tampered with
  129. * @param invalidStamp the invalid stamp text
  130. */
  131. public void setInvalidStamp(String invalidStamp) {
  132. this.invalidStamp = invalidStamp;
  133. }
  134. /** the plain signature without caption */
  135. public byte[] getPlainSignature() {
  136. return plainSignature;
  137. }
  138. /**
  139. * Sets the plain signature
  140. * supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF.
  141. * for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath
  142. *
  143. * @param plainSignature the plain signature - if {@code null}, the signature is not rendered
  144. * and only the caption is visible
  145. */
  146. public void setPlainSignature(byte[] plainSignature) {
  147. this.plainSignature = plainSignature;
  148. this.contentType = null;
  149. }
  150. public String getContentType() {
  151. return contentType;
  152. }
  153. public void setContentType(String contentType) {
  154. this.contentType = contentType;
  155. }
  156. public CTShape getSignatureShape() {
  157. return signatureShape;
  158. }
  159. public void setSignatureShape(CTShape signatureShape) {
  160. this.signatureShape = signatureShape;
  161. }
  162. public void setSignatureShape(CTSignatureLine signatureLine) {
  163. XmlCursor cur = signatureLine.newCursor();
  164. cur.toParent();
  165. this.signatureShape = (CTShape)cur.getObject();
  166. cur.dispose();
  167. }
  168. public void updateSignatureConfig(SignatureConfig config) throws IOException {
  169. if (plainSignature == null) {
  170. throw new IllegalStateException("Plain signature not initialized");
  171. }
  172. if (contentType == null) {
  173. determineContentType();
  174. }
  175. byte[] signValid = generateImage(true, false);
  176. byte[] signInvalid = generateImage(true, true);
  177. config.setSignatureImageSetupId(getSetupId());
  178. config.setSignatureImage(plainPng());
  179. config.setSignatureImageValid(signValid);
  180. config.setSignatureImageInvalid(signInvalid);
  181. }
  182. protected void parse() {
  183. if (signatureShape == null) {
  184. return;
  185. }
  186. CTSignatureLine signatureLine = signatureShape.getSignaturelineArray(0);
  187. setSetupId(new ClassID(signatureLine.getId()));
  188. setAllowComments(signatureLine.isSetAllowcomments() ? STTrueFalse.TRUE.equals(signatureLine.getAllowcomments()) : null);
  189. setSuggestedSigner(signatureLine.getSuggestedsigner());
  190. setSuggestedSigner2(signatureLine.getSuggestedsigner2());
  191. setSuggestedSignerEmail(signatureLine.getSuggestedsigneremail());
  192. XmlCursor cur = signatureLine.newCursor();
  193. try {
  194. // the signinginstructions are actually qualified, but our schema version is too old
  195. setSigningInstructions(cur.getAttributeText(new QName(MS_OFFICE_URN, "signinginstructions")));
  196. } finally {
  197. cur.dispose();
  198. }
  199. }
  200. protected interface AddPictureData {
  201. /**
  202. * Add picture data to the document
  203. * @param imageData the image bytes
  204. * @param pictureType the picture type - typically PNG
  205. * @return the relation id of the newly add picture
  206. */
  207. String addPictureData(byte[] imageData, PictureType pictureType) throws InvalidFormatException;
  208. }
  209. protected abstract void setRelationId(CTImageData imageData, String relId);
  210. protected void add(XmlObject signatureContainer, AddPictureData addPictureData) {
  211. byte[] inputImage;
  212. try {
  213. inputImage = generateImage(false, false);
  214. CTGroup grp = CTGroup.Factory.newInstance();
  215. grp.addNewShape();
  216. XmlCursor contCur = signatureContainer.newCursor();
  217. contCur.toEndToken();
  218. XmlCursor otherC = grp.newCursor();
  219. otherC.copyXmlContents(contCur);
  220. otherC.dispose();
  221. contCur.toPrevSibling();
  222. signatureShape = (CTShape)contCur.getObject();
  223. contCur.dispose();
  224. signatureShape.setAlt("Microsoft Office Signature Line...");
  225. signatureShape.setStyle("width:191.95pt;height:96.05pt");
  226. // signatureShape.setStyle("position:absolute;margin-left:100.8pt;margin-top:43.2pt;width:192pt;height:96pt;z-index:1");
  227. signatureShape.setType("rect");
  228. String relationId = addPictureData.addPictureData(inputImage, PictureType.PNG);
  229. CTImageData imgData = signatureShape.addNewImagedata();
  230. setRelationId(imgData, relationId);
  231. imgData.setTitle("");
  232. CTSignatureLine xsl = signatureShape.addNewSignatureline();
  233. if (suggestedSigner != null) {
  234. xsl.setSuggestedsigner(suggestedSigner);
  235. }
  236. if (suggestedSigner2 != null) {
  237. xsl.setSuggestedsigner2(suggestedSigner2);
  238. }
  239. if (suggestedSignerEmail != null) {
  240. xsl.setSuggestedsigneremail(suggestedSignerEmail);
  241. }
  242. if (setupId == null) {
  243. setupId = new ClassID("{"+ UUID.randomUUID().toString()+"}");
  244. }
  245. xsl.setId(setupId.toString());
  246. xsl.setAllowcomments(STTrueFalse.T);
  247. xsl.setIssignatureline(STTrueFalse.T);
  248. xsl.setProvid("{00000000-0000-0000-0000-000000000000}");
  249. xsl.setExt(STExt.EDIT);
  250. xsl.setSigninginstructionsset(STTrueFalse.T);
  251. XmlCursor cur = xsl.newCursor();
  252. cur.setAttributeText(new QName(MS_OFFICE_URN, "signinginstructions"), signingInstructions);
  253. cur.dispose();
  254. } catch (IOException | InvalidFormatException e) {
  255. // shouldn't happen ...
  256. throw new POIXMLException("Can't generate signature line image", e);
  257. }
  258. }
  259. protected void update() {
  260. }
  261. /**
  262. * Word and Excel a regenerating the valid and invalid signature line based on the
  263. * plain signature. Both are picky about the input format.
  264. * Especially EMF images need to a specific device dimension (dpi)
  265. * instead of fiddling around with the input image, we generate/register a bitmap image instead
  266. *
  267. * @return the converted PNG image
  268. */
  269. protected byte[] plainPng() throws IOException {
  270. byte[] plain = getPlainSignature();
  271. PictureType pictureType;
  272. switch (FileMagic.valueOf(plain)) {
  273. case PNG:
  274. return plain;
  275. case BMP:
  276. pictureType = PictureType.BMP;
  277. break;
  278. case EMF:
  279. pictureType = PictureType.EMF;
  280. break;
  281. case GIF:
  282. pictureType = PictureType.GIF;
  283. break;
  284. case JPEG:
  285. pictureType = PictureType.JPEG;
  286. break;
  287. case XML:
  288. pictureType = PictureType.SVG;
  289. break;
  290. case TIFF:
  291. pictureType = PictureType.TIFF;
  292. break;
  293. default:
  294. throw new IllegalArgumentException("Unsupported picture format");
  295. }
  296. ImageRenderer rnd = DrawPictureShape.getImageRenderer(null, pictureType.contentType);
  297. if (rnd == null) {
  298. throw new UnsupportedOperationException(pictureType + " can't be rendered - did you provide poi-scratchpad and its dependencies (batik et al.)");
  299. }
  300. rnd.loadImage(getPlainSignature(), pictureType.contentType);
  301. Dimension2D dim = rnd.getDimension();
  302. int defaultWidth = 300;
  303. int defaultHeight = (int)(defaultWidth * dim.getHeight() / dim.getWidth());
  304. BufferedImage bi = new BufferedImage(defaultWidth, defaultHeight, BufferedImage.TYPE_INT_ARGB);
  305. Graphics2D gfx = bi.createGraphics();
  306. gfx.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
  307. gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
  308. gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  309. gfx.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  310. rnd.drawImage(gfx, new Rectangle2D.Double(0, 0, defaultWidth, defaultHeight));
  311. gfx.dispose();
  312. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  313. ImageIO.write(bi, "PNG", bos);
  314. return bos.toByteArray();
  315. }
  316. /**
  317. * Generate the image for a signature line
  318. * @param caption three lines separated by "\n" - usually something like "First name Last name\nRole\nname of the key"
  319. * @param inputImage the plain signature - supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF.
  320. * for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath
  321. * if {@code null}, the inputImage is not rendered
  322. * @param invalidText for invalid signature images, use the given text
  323. * @return the signature image in PNG format as byte array
  324. */
  325. protected byte[] generateImage(boolean showSignature, boolean showInvalidStamp) throws IOException {
  326. BufferedImage bi = new BufferedImage(400, 150, BufferedImage.TYPE_INT_ARGB);
  327. Graphics2D gfx = bi.createGraphics();
  328. gfx.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
  329. gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
  330. gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  331. gfx.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  332. String markX = "X\n";
  333. String lineX = (new String(new char[500]).replace("\0", " ")) +"\n";
  334. String cap = (getCaption() == null) ? getDefaultCaption() : getCaption();
  335. String text = markX+lineX+cap.replaceAll("(?m)^", " ");
  336. AttributedString as = new AttributedString(text);
  337. as.addAttribute(TextAttribute.FAMILY, Font.SANS_SERIF);
  338. as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, markX.length(), text.indexOf('\n', markX.length()));
  339. as.addAttribute(TextAttribute.SIZE, 15, 0, markX.length());
  340. as.addAttribute(TextAttribute.SIZE, 12, markX.length(), text.length());
  341. gfx.setColor(Color.BLACK);
  342. AttributedCharacterIterator chIter = as.getIterator();
  343. FontRenderContext frc = gfx.getFontRenderContext();
  344. LineBreakMeasurer measurer = new LineBreakMeasurer(chIter, frc);
  345. float y = 80, x = 5;
  346. for (int lineNr = 0; measurer.getPosition() < chIter.getEndIndex(); lineNr++) {
  347. int mpos = measurer.getPosition();
  348. int limit = text.indexOf('\n', mpos);
  349. limit = (limit == -1) ? text.length() : limit+1;
  350. TextLayout textLayout = measurer.nextLayout(bi.getWidth()-10, limit, false);
  351. if (lineNr != 1) {
  352. y += textLayout.getAscent();
  353. }
  354. textLayout.draw(gfx, x, y);
  355. y += textLayout.getDescent() + textLayout.getLeading();
  356. }
  357. if (showSignature && plainSignature != null && contentType != null) {
  358. ImageRenderer renderer = DrawPictureShape.getImageRenderer(gfx, contentType);
  359. renderer.loadImage(plainSignature, contentType);
  360. double targetX = 10;
  361. double targetY = 100;
  362. double targetWidth = bi.getWidth() - targetX;
  363. double targetHeight = targetY - 5;
  364. Dimension2D dim = renderer.getDimension();
  365. double scale = Math.min(targetWidth / dim.getWidth(), targetHeight / dim.getHeight());
  366. double effWidth = dim.getWidth() * scale;
  367. double effHeight = dim.getHeight() * scale;
  368. renderer.drawImage(gfx, new Rectangle2D.Double(targetX + ((bi.getWidth() - effWidth) / 2), targetY - effHeight, effWidth, effHeight));
  369. }
  370. if (showInvalidStamp && invalidStamp != null && !invalidStamp.isEmpty()) {
  371. gfx.setFont(new Font("Lucida Bright", Font.ITALIC, 60));
  372. gfx.rotate(Math.toRadians(-15), bi.getWidth()/2., bi.getHeight()/2.);
  373. TextLayout tl = new TextLayout(invalidStamp, gfx.getFont(), gfx.getFontRenderContext());
  374. Rectangle2D bounds = tl.getBounds();
  375. x = (float)((bi.getWidth()-bounds.getWidth())/2 - bounds.getX());
  376. y = (float)((bi.getHeight()-bounds.getHeight())/2 - bounds.getY());
  377. Shape outline = tl.getOutline(AffineTransform.getTranslateInstance(x+2, y+1));
  378. gfx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f));
  379. gfx.setPaint(Color.RED);
  380. gfx.draw(outline);
  381. gfx.setPaint(new GradientPaint(0, 0, Color.RED, 30, 20, new Color(128, 128, 255), true));
  382. tl.draw(gfx, x, y);
  383. }
  384. gfx.dispose();
  385. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  386. ImageIO.write(bi, "PNG", bos);
  387. return bos.toByteArray();
  388. }
  389. private void determineContentType() {
  390. FileMagic fm = FileMagic.valueOf(plainSignature);
  391. switch (fm) {
  392. case GIF:
  393. contentType = PictureType.GIF.contentType;
  394. break;
  395. case PNG:
  396. contentType = PictureType.PNG.contentType;
  397. break;
  398. case JPEG:
  399. contentType = PictureType.JPEG.contentType;
  400. break;
  401. case XML:
  402. contentType = PictureType.SVG.contentType;
  403. break;
  404. case EMF:
  405. contentType = PictureType.EMF.contentType;
  406. break;
  407. case WMF:
  408. contentType = PictureType.WMF.contentType;
  409. break;
  410. default:
  411. throw new IllegalArgumentException("unknown image type");
  412. }
  413. }
  414. }