--- /dev/null
+package org.simantics.export.core.pdf;\r
+\r
+import java.awt.Graphics2D;\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.security.KeyStore;\r
+import java.security.KeyStoreException;\r
+import java.security.NoSuchAlgorithmException;\r
+import java.security.PrivateKey;\r
+import java.security.UnrecoverableKeyException;\r
+import java.security.cert.Certificate;\r
+import java.security.cert.CertificateException;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+import org.simantics.Simantics;\r
+import org.simantics.databoard.binding.mutable.Variant;\r
+import org.simantics.export.core.ExportContext;\r
+import org.simantics.export.core.error.ExportException;\r
+import org.simantics.export.core.intf.Format;\r
+import org.simantics.export.core.manager.Content;\r
+import org.simantics.utils.page.MarginUtils.Margins;\r
+import org.simantics.utils.page.PageDesc;\r
+import org.simantics.utils.page.PageOrientation;\r
+\r
+import com.lowagie.text.Document;\r
+import com.lowagie.text.DocumentException;\r
+import com.lowagie.text.Element;\r
+import com.lowagie.text.ExceptionConverter;\r
+import com.lowagie.text.Font;\r
+import com.lowagie.text.PageSize;\r
+import com.lowagie.text.Phrase;\r
+import com.lowagie.text.Rectangle;\r
+import com.lowagie.text.pdf.AcroFields;\r
+import com.lowagie.text.pdf.BadPdfFormatException;\r
+import com.lowagie.text.pdf.ColumnText;\r
+import com.lowagie.text.pdf.FontMapper;\r
+import com.lowagie.text.pdf.PdfContentByte;\r
+import com.lowagie.text.pdf.PdfCopy;\r
+import com.lowagie.text.pdf.PdfDictionary;\r
+import com.lowagie.text.pdf.PdfFileSpecification;\r
+import com.lowagie.text.pdf.PdfImportedPage;\r
+import com.lowagie.text.pdf.PdfReader;\r
+import com.lowagie.text.pdf.PdfSignatureAppearance;\r
+import com.lowagie.text.pdf.PdfStamper;\r
+import com.lowagie.text.pdf.PdfTemplate;\r
+import com.lowagie.text.pdf.PdfWriter;\r
+\r
+/**\r
+ * A PDF writer object. \r
+ *\r
+ * @author toni.kalajainen@semantum.fi\r
+ */\r
+public class ExportPdfWriter {\r
+\r
+ /** PDF Document */\r
+ public Document document;\r
+\r
+ /** PDF Output stream */\r
+ public PdfCopy pdfCopy; \r
+ \r
+ /** Open output stream */\r
+ public FileOutputStream fos;\r
+\r
+ /** The direct content byte of the document */\r
+ public PdfContentByte cb;\r
+ \r
+ /** Contains Pdf Templates, e.g. symbols. Resource Uri -> Template mapping */\r
+ public Map<String, PdfTemplate> templates = new HashMap<String, PdfTemplate>();\r
+ \r
+ /** Pages */\r
+ public List<Page> pages = new ArrayList<Page>(); \r
+ \r
+ /** Suggested Page Desc */\r
+ public PageDesc defaultPageDesc;\r
+ \r
+ /** PageDesc as PDF rectangle */\r
+ public Rectangle defaultRectangle;\r
+ \r
+ /** Initialized FontMapper */\r
+ public FontMapper fontMapper;\r
+ \r
+ /** The output file */\r
+ public File outputFile;\r
+ \r
+ /** All options */\r
+ public Variant options;\r
+ \r
+ /** Export Context */\r
+ public ExportContext ctx;\r
+ \r
+ /** The margins the user selected. */\r
+ public Margins margins;\r
+ \r
+ /** Compression Level */\r
+ public int compressionLevel;\r
+\r
+ /**\r
+ * Create new page.\r
+ * \r
+ * @param (Optional) page description. If null is used, the default size is used.\r
+ * @return Page for writing\r
+ * @throws ExportException\r
+ */\r
+ public Page createPage( PageDesc pd ) throws ExportException {\r
+ \r
+ Rectangle rect; \r
+ if ( pd == null || pd.isInfinite() ) {\r
+ pd = defaultPageDesc;\r
+ rect = defaultRectangle;\r
+ } else {\r
+ rect = toRectangle( pd );\r
+ }\r
+ \r
+ Page page = new Page( pd, rect, pages.size() ); \r
+ pages.add(page); \r
+ return page;\r
+ }\r
+ \r
+ /**\r
+ * Create a new template.\r
+ * \r
+ * Note, template is not visible on the document until you add it with \r
+ * cb.addTemplate( template.tp, 0, 0);\r
+ * \r
+ * @param name (Optional) Template identifier \r
+ * @param pd (Optional) template size description. If null is used, the default size is used.\r
+ * @return Template handle\r
+ * @throws ExportException\r
+ */\r
+ public Template createTemplate( String name, PageDesc pd ) throws ExportException {\r
+ Rectangle rect; \r
+ if ( pd == null || pd.isInfinite() ) {\r
+ pd = defaultPageDesc;\r
+ rect = defaultRectangle;\r
+ } else {\r
+ rect = toRectangle( pd );\r
+ }\r
+ \r
+ int w = (int) pd.getWidth();\r
+ int h = (int) pd.getHeight();\r
+ PdfTemplate tp = cb.createTemplate(w, h); \r
+ Template canvas = new Template( name, pd, rect, tp );\r
+ canvas.name = name;\r
+ if ( name!=null ) templates.put(name, tp); \r
+ return canvas;\r
+ } \r
+ \r
+ /**\r
+ * Sign the file with a private+public key pair (PPK).\r
+ * The file must be closed already. \r
+ * \r
+ * @param keystoreFile the keystore file\r
+ * @param keystorePassword (optional) password \r
+ * @param privateKeyPassword (optional) password\r
+ * @param signLocation (optional) sign locaiton, e.g. "Helsinki"\r
+ * @param signReason (optional) e.g. "approved"\r
+ * @throws ExportException \r
+ */\r
+ public void sign( File keystoreFile, String keystorePassword, String privateKeyPassword, String signLocation, String signReason) throws ExportException {\r
+ // Add Bouncycastle, if found. If not, try anyway.\r
+ /*\r
+ if (providerAdded.compareAndSet(false, true)) {\r
+ try {\r
+ String className = "org.bouncycastle.jce.provider.BouncyCastleProvider";\r
+ Class<?> clazz = Class.forName(className);\r
+ Provider provide = (Provider) clazz.newInstance();\r
+ Security.addProvider( provide );\r
+ } catch (SecurityException se) {\r
+ se.printStackTrace();\r
+ } catch (NullPointerException npe) {\r
+ npe.printStackTrace();\r
+ } catch (ClassNotFoundException e) {\r
+ e.printStackTrace();\r
+ } catch (InstantiationException e) {\r
+ e.printStackTrace();\r
+ } catch (IllegalAccessException e) {\r
+ e.printStackTrace();\r
+ }\r
+ }*/\r
+\r
+ // Sign\r
+ FileInputStream ksfis = null;\r
+ FileInputStream fis = null;\r
+ FileOutputStream fos = null;\r
+ File signedFile = null;\r
+ try {\r
+ KeyStore ks = KeyStore.getInstance("pkcs12");\r
+ signedFile = new File( outputFile.getCanonicalPath()+".signed" );\r
+ if (signedFile.exists()) signedFile.delete();\r
+ ksfis = new FileInputStream(keystoreFile);\r
+ fis = new FileInputStream(outputFile);\r
+ fos = new FileOutputStream( signedFile );\r
+ ks.load(ksfis, keystorePassword != null ? keystorePassword.toCharArray() : null);\r
+ \r
+ List<String> aliases = Collections.list(ks.aliases());\r
+ String alias = aliases.get(0);\r
+ PrivateKey key = (PrivateKey)ks.getKey(alias, privateKeyPassword != null ? privateKeyPassword.toCharArray() : null);\r
+ Certificate[] chain = ks.getCertificateChain(alias);\r
+\r
+ PdfReader reader = new PdfReader( fis );\r
+ PdfStamper stp = PdfStamper.createSignature(reader, fos, '\0');\r
+ PdfSignatureAppearance sap = stp.getSignatureAppearance();\r
+\r
+ /// Signature\r
+ String fieldName = "sign"; //signReason==null?"sign":URIUtil.encodeFilename( signReason );\r
+ AcroFields af = stp.getAcroFields();\r
+ AcroFields.Item item = af.getFieldItem(fieldName);\r
+ if (signReason!=null) sap.setReason( signReason );\r
+ if (signLocation!=null) sap.setLocation( signLocation );\r
+ sap.setCrypto(key, chain, null, PdfSignatureAppearance.SELF_SIGNED);\r
+ sap.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);\r
+ sap.setRender(PdfSignatureAppearance.SignatureRenderNameAndDescription);\r
+ \r
+ // Make field the signature\r
+ //sap.setVisibleSignature(fieldName);\r
+ \r
+ // Visible signature\r
+ //AcroFields af = stp.getAcroFields();\r
+ //AcroFields.Item item = af.getFieldItem(fieldName);\r
+ //sap.setVisibleSignature(new Rectangle(0, 0, 100, 10), 0, fieldName);\r
+ \r
+ // comment next line to have an invisible signature\r
+ //sap.setVisibleSignature(new Rectangle(682, 130, 822, 145), 1, "approved_by");\r
+ \r
+ //stp.getAcroFields().setField(fieldName, "someValue");\r
+ \r
+ stp.close(); \r
+ reader.close();\r
+ fis.close();\r
+ } catch (DocumentException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage(), e );\r
+ } catch (UnrecoverableKeyException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage(), e );\r
+ } catch (NoSuchAlgorithmException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage(), e );\r
+ } catch (CertificateException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage(),e );\r
+ } catch (IOException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage(), e );\r
+ } catch (KeyStoreException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage(), e );\r
+ } finally {\r
+ if ( ksfis != null ) try { ksfis.close(); } catch (IOException e) {}\r
+ if ( fis != null ) try { fis.close(); } catch (IOException e) {}\r
+ if ( fos != null ) try { fos.close(); } catch (IOException e) {}\r
+ \r
+ if ( signedFile!=null && signedFile.exists() && outputFile.exists() ) {\r
+ outputFile.delete();\r
+ signedFile.renameTo( outputFile );\r
+ }\r
+ \r
+ }\r
+ }\r
+ \r
+ public void addAttachment(Content content) throws ExportException {\r
+ try {\r
+ if ( content.tmpFile == null ) throw new ExportException("Could not export "+content.filename+", null file.");\r
+ if ( !content.tmpFile.exists() ) throw new ExportException("Could not export "+content.filename+", file not found.");\r
+ \r
+ Format format = ctx.eep.getFormat( content.formatId );\r
+ //byte[] data = StreamUtil.readFully( content.tmpFile );\r
+ PdfDictionary fileParameter = new PdfDictionary(); \r
+ PdfFileSpecification spec = PdfFileSpecification.fileEmbedded(\r
+ pdfCopy,\r
+ content.tmpFile.getAbsolutePath(),\r
+ content.filename, \r
+ null, \r
+ true, \r
+ "application/simantics/"+format.id(), \r
+ fileParameter);\r
+ \r
+ pdfCopy.addFileAttachment( content.filename, spec ); \r
+ \r
+ \r
+ } catch (IOException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage() );\r
+ }\r
+ }\r
+ \r
+ public void close() throws ExportException {\r
+ // Flush & close\r
+ try {\r
+ if ( pages.isEmpty() ) {\r
+ Page page = createPage(null); \r
+ Graphics2D g2d = page.createGraphics(true);\r
+ try {\r
+ g2d.drawString("This page is intentionally left blank.", 100, 100);\r
+ } finally {\r
+ g2d.dispose();\r
+ }\r
+ }\r
+ \r
+ for ( Page page : pages ) page.close();\r
+ \r
+ Font f = new Font(Font.HELVETICA, 8);\r
+\r
+ int totalPages = 0;\r
+ int currentPage = 1;\r
+ \r
+ for ( Page page : pages ) {\r
+ PdfReader reader = new PdfReader( page.tmpFile.getAbsolutePath() );\r
+ try {\r
+ totalPages += reader.getNumberOfPages();\r
+ } finally {\r
+ reader.close();\r
+ }\r
+ }\r
+ \r
+ for ( Page page : pages ) {\r
+ PdfReader reader = new PdfReader( page.tmpFile.getAbsolutePath() );\r
+ try {\r
+ int n = reader.getNumberOfPages();\r
+\r
+ for (int i = 0; i < n; ) {\r
+ Rectangle pageSize = reader.getPageSizeWithRotation(n);\r
+\r
+ PdfImportedPage imp = pdfCopy.getImportedPage(reader, ++i);\r
+ PdfCopy.PageStamp ps = pdfCopy.createPageStamp(imp);\r
+\r
+ PdfContentByte over = ps.getOverContent();\r
+\r
+ ColumnText.showTextAligned(over, Element.ALIGN_RIGHT,\r
+ new Phrase(\r
+ String.format("%d / %d", currentPage++, totalPages), f),\r
+ pageSize.getWidth()-12, 12, 0);\r
+ ps.alterContents();\r
+ pdfCopy.addPage(imp);\r
+ \r
+ }\r
+ } finally {\r
+ reader.close();\r
+ }\r
+ } \r
+ } catch (IOException e) {\r
+ throw new ExportException( e );\r
+ } catch (ExceptionConverter e) {\r
+ throw new ExportException( e );\r
+ } catch (BadPdfFormatException e) {\r
+ throw new ExportException( e );\r
+ } catch (Exception e) {\r
+ throw new ExportException( e );\r
+ } finally {\r
+ for ( Page page : pages ) {\r
+ if ( page.tmpFile != null ) { page.tmpFile.delete(); page.tmpFile = null; } \r
+ }\r
+ pages.clear();\r
+ \r
+ if ( document != null ) { document.close(); document = null; }\r
+ if ( pdfCopy != null ) { pdfCopy.close(); pdfCopy = null; }\r
+ if ( fos != null ) { try {\r
+ fos.close();\r
+ } catch (IOException e) {\r
+ throw new ExportException(e);\r
+ } fos = null; }\r
+ }\r
+ }\r
+ \r
+ public class Page {\r
+\r
+ /** PDF Output stream */\r
+ public PdfWriter pdfWriter;\r
+ \r
+ /** PDF Document */\r
+ public Document document;\r
+ \r
+ /** Open output stream */\r
+ public FileOutputStream fos;\r
+\r
+ /** The direct content byte of the document */\r
+ public PdfContentByte cb;\r
+ \r
+ /** Tmp-file where the page is written to */\r
+ public File tmpFile;\r
+ \r
+ /** Suggested Page Desc */\r
+ public PageDesc pageDesc;\r
+ \r
+ /** PageDesc as PDF rectangle */\r
+ public Rectangle rectangle;\r
+ \r
+ /** Page number */\r
+ public int pageNumber;\r
+ \r
+ Page(PageDesc pageDesc, Rectangle rect, int pageNumber) throws ExportException {\r
+ try {\r
+ this.pageDesc = pageDesc;\r
+ this.rectangle = rect;\r
+ this.pageNumber = pageNumber;\r
+ this.tmpFile = Simantics.getTempfile("export.core", "pdf");\r
+ this.fos = new FileOutputStream( tmpFile, false ); \r
+ this.document = new Document(rectangle); \r
+ this.document.setPageSize( rect ); // redundant?\r
+ this.pdfWriter = PdfWriter.getInstance(document, fos);\r
+ this.pdfWriter.setPdfVersion(PdfWriter.PDF_VERSION_1_7);\r
+ this.pdfWriter.setCompressionLevel( compressionLevel );\r
+ this.document.open();\r
+ this.cb = this.pdfWriter.getDirectContent();\r
+ if (!this.document.newPage()) throw new ExportException("Failed to create new page.");\r
+ } catch (IOException e) {\r
+ throw new ExportException( e );\r
+ } catch (DocumentException e) {\r
+ throw new ExportException( e );\r
+ }\r
+ }\r
+ \r
+ /**\r
+ * Create a graphics 2d Context that uses millimeters.\r
+ * \r
+ * @param applyMargins top left position of margins is applied\r
+ * @return graphics 2d context\r
+ */\r
+ public Graphics2D createGraphics(boolean applyMargins) {\r
+ float w = rectangle.getWidth();\r
+ float h = rectangle.getHeight();\r
+ double pw = pageDesc.getOrientedWidth();\r
+ double ph = pageDesc.getOrientedHeight();\r
+ Graphics2D g2d = cb.createGraphics(w, h, fontMapper);\r
+ \r
+ if ( applyMargins ) {\r
+ Margins m = pageDesc.getMargins();\r
+\r
+ double mw = pw - m.left.diagramAbsolute - m.right.diagramAbsolute;\r
+ double mh = ph - m.top.diagramAbsolute - m.bottom.diagramAbsolute;\r
+ double sx = m.left.diagramAbsolute;\r
+ double sy = m.top.diagramAbsolute;\r
+ \r
+ // Convert to points\r
+ mw = PageDesc.toPoints( mw );\r
+ mh = PageDesc.toPoints( mh );\r
+ sx = PageDesc.toPoints( sx );\r
+ sy = PageDesc.toPoints( sy );\r
+ \r
+ g2d.translate(sx, sy);\r
+ }\r
+ \r
+ g2d.scale(w/pw, h/ph); \r
+ return g2d;\r
+ }\r
+ \r
+ /**\r
+ * Of area inside the margins, return the width of the page in millimeters. \r
+ * \r
+ * @return width (mm)\r
+ */\r
+ public double getWidth() {\r
+ Margins m = pageDesc.getMargins();\r
+ return pageDesc.getOrientedWidth() - m.left.diagramAbsolute - m.right.diagramAbsolute; \r
+ }\r
+ \r
+ public double getHeight() {\r
+ Margins m = pageDesc.getMargins();\r
+ return pageDesc.getOrientedHeight() - m.top.diagramAbsolute - m.bottom.diagramAbsolute; \r
+ }\r
+ \r
+ /**\r
+ * Add attachment to this page\r
+ * @param content\r
+ * @throws ExportException\r
+ */\r
+ public void addAttachment(Content content) throws ExportException {\r
+ /*\r
+ try {\r
+ if ( content.tmpFile == null ) throw new ExportException("Could not export "+content.filename+", null file.");\r
+ if ( !content.tmpFile.exists() ) throw new ExportException("Could not export "+content.filename+", file not found.");\r
+ \r
+ Format format = ctx.eep.getFormat( content.formatId );\r
+ //byte[] data = StreamUtil.readFully( content.tmpFile );\r
+ PdfDictionary fileParameter = new PdfDictionary(); \r
+ PdfFileSpecification spec = PdfFileSpecification.fileEmbedded(\r
+ pdfWriter,\r
+ content.tmpFile.getAbsolutePath(),\r
+ content.filename, \r
+ null, \r
+ true, \r
+ "application/simantics/"+format.id(), \r
+ fileParameter);\r
+ \r
+ pdfWriter.addFileAttachment( content.filename, spec ); \r
+ } catch (IOException e) {\r
+ throw new ExportException( e.getClass().getName()+": "+e.getMessage() );\r
+ }*/\r
+ ExportPdfWriter.this.addAttachment(content);\r
+ }\r
+ \r
+ public void close() throws ExportException {\r
+ try {\r
+ if ( document != null ) { document.close(); document = null; }\r
+ if ( pdfWriter != null ) { pdfWriter.close(); pdfWriter = null; }\r
+ if ( fos != null ) { fos.close(); fos = null; }\r
+ if ( cb != null ) { cb = null; }\r
+ } catch (IOException e) {\r
+ throw new ExportException(e);\r
+ }\r
+ }\r
+ \r
+ }\r
+ \r
+ public class Template {\r
+\r
+ /** Suggested Page Desc */\r
+ public PageDesc pageDesc;\r
+ \r
+ /** PageDesc as PDF rectangle */\r
+ public Rectangle rectangle;\r
+ \r
+ /** Template name */\r
+ public String name;\r
+ \r
+ /** PdfTemplate */\r
+ public PdfTemplate tp;\r
+\r
+ Template(String name, PageDesc pd, Rectangle rect, PdfTemplate tp) {\r
+ this.pageDesc = pd;\r
+ this.rectangle = rect;\r
+ this.name = name;\r
+ this.tp = tp;\r
+ }\r
+ \r
+ public Graphics2D createGraphics() { \r
+ double w = pageDesc.getWidth();\r
+ double h = pageDesc.getHeight();\r
+ return tp.createGraphics((float) w, (float) h, fontMapper);\r
+ }\r
+ \r
+ }\r
+ \r
+ public static Rectangle toRectangle(PageDesc pageDesc) {\r
+ String arg = PageDesc.toPoints(pageDesc.getWidth()) + " " + PageDesc.toPoints(pageDesc.getHeight());\r
+ Rectangle r = PageSize.getRectangle(arg);\r
+\r
+ if (PageOrientation.Landscape == pageDesc.getOrientation())\r
+ r = r.rotate();\r
+\r
+ // Disable inherent borders from the PDF writer.\r
+ r.setBorder(0);\r
+\r
+ return r;\r
+ }\r
+ \r
+}\r