/** * Copyright 2006 by Know-Center, Graz, Austria * PDF-AS has been contracted by the E-Government Innovation Center EGIZ, a * joint initiative of the Federal Chancellery Austria and Graz University of * Technology. * * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * http://www.osor.eu/eupl/ * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and * limitations under the Licence. * * This product combines work with different licenses. See the "NOTICE" text * file for details on the various modules and licenses. * The "NOTICE" text file is part of the distribution. Any derivative works * that you distribute must include a readable copy of the "NOTICE" text file. */ package at.knowcenter.wag.egov.egiz.pdf; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.apache.log4j.Logger; import at.gv.egiz.pdfas.exceptions.ErrorCode; import at.knowcenter.wag.egov.egiz.exceptions.PresentableException; import at.knowcenter.wag.egov.egiz.sig.SignatureObject; import com.lowagie.text.Rectangle; import com.lowagie.text.pdf.PdfArray; import com.lowagie.text.pdf.PdfContentByte; import com.lowagie.text.pdf.PdfDictionary; import com.lowagie.text.pdf.PdfFormField; import com.lowagie.text.pdf.PdfIndirectReference; import com.lowagie.text.pdf.PdfName; import com.lowagie.text.pdf.PdfNumber; import com.lowagie.text.pdf.PdfObject; import com.lowagie.text.pdf.PdfStamper; import com.lowagie.text.pdf.PdfStamperImp; import com.lowagie.text.pdf.PdfString; import com.lowagie.text.pdf.PdfTemplate; import com.lowagie.text.pdfas.StructContentWriter; import com.lowagie.text.pdfas.StructContentWriterHolder; import com.lowagie.text.pdfas.UrlInTextFinder; /** * Helper class for writing the structure hierarchy of the signature elements. * Everything is written with the PdfObject low level API because there is no better support. * The structured content is only written for structured (==tagged) input documents. The methods have to be called in the * defined order. The object cannot be reused for several signatures.
* See pdf spec "Logical Structure" for details.
* The struct writing could be a little more abstracted, but this would include quite some itext extension work. And like this it * fits better to PDF-AS / wprinz coding style :-( * @author exthex * */ public class StructContentHelper implements StructContentWriter { private static final Logger logger = Logger.getLogger(StructContentHelper.class); private static final String SIGBLOCK_STRUCT_TYPE = "P"; private static final PdfName PARENTTREENEXTKEY = new PdfName("ParentTreeNextKey"); private static final String ALT_TEXT_DEFAULT = "Signaturbildmarke"; private final static String ALT_TEXT_CONF_KEY = "sigLogoAltText"; private int nextMcid = 0; /** * MCID value used for the sigblock marked contend identifier */ private int sigBlockMcid =-1; /** * MCID value for "Bildmarke" marked content sequence */ private int figureMcid = -1; /** * MCID value for verify link marked content sequence */ private int linkMcid = -1; private String linkUrlString = null; private boolean isTagged = false; private Map linkPosMap = new HashMap(); private Map tmpMap = new HashMap(); private PdfStamper stamper; private PdfStamperImp stamperImp; private PdfContentByte content; private PdfDictionary page; private PdfNumber parentTreeNextKey = null; private PdfNumber annotationParentTreeKey = null; /** * Temporary save a pos */ private Rectangle tempMarkedPos = null; /** * Cell position of the signature verify link overlay */ private Rectangle verifyLinkCellPos = null; /** * Kids array (K) of the StructTreeRoot */ private PdfArray structTreeRootKids = null; /** * Entry in the ParentTree.Nums array used for sigtable structs */ private PdfArray mainParentTreeNumEntry; /** * Create new helper for one signature, and bind it to {@link StructContentWriterHolder} * for thread local access from itext. * * @param stamper * @param content * @param pageNr */ StructContentHelper(PdfStamper stamper, PdfContentByte content, int pageNr) { this.stamper = stamper; this.content = content; stamperImp = ((PdfStamperImp) stamper.getWriter()); page = stamper.getReader().getPageN(pageNr); StructContentWriterHolder.setThreadLocalWriter(this); } /** * Remove thread local helper */ public void removeCurrent() { StructContentWriterHolder.removeThreadLocalWriter(); } /** * Prepare structured content for signature block. This method initializes the whole StructTreeRoot stuff. * @param sigBlockObj * @throws PresentableException */ void prepareStructData(PdfTemplate sigBlockObj) throws PresentableException { try { checkTagging(); if (!isTagged) { return; } doAnnoTabOrder(); PdfDictionary structTreeRoot = getStructTreeRoot(); stamperImp.markUsed(structTreeRoot); PdfArray parentTreeNums = getParentTreeNums(); PdfNumber structParentsNr = page.getAsNumber(PdfName.STRUCTPARENTS); // read StructParents entry from current page mainParentTreeNumEntry = obtainParentTreeEntry(structTreeRoot, parentTreeNums, structParentsNr, sigBlockObj); nextMcid = mainParentTreeNumEntry.size(); sigBlockMcid = nextMcid; nextMcid++; this.structTreeRootKids = obtainStructTreeRootKids(structTreeRoot); if(this.structTreeRootKids == null) { this.structTreeRootKids = this.createStructTreeRootKids(structTreeRoot); } } catch (Exception ex) { logger.error("error", ex); throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF, "error writing structured signature content", ex); } } PdfArray createStructTreeRootKids(PdfDictionary structTreeRoot) { PdfArray tmp = new PdfArray(); structTreeRoot.put(PdfName.K, tmp); return tmp; } /** * Create struct data for main signature block * @throws PresentableException */ void buildSigBlockStructData() throws PresentableException { if (!isTagged) return; try { PdfIndirectReference newStructRef = createStructElem(SIGBLOCK_STRUCT_TYPE, new PdfNumber( sigBlockMcid), getStructTreeRoot().getIndRef()); // ADD everything at the end because nothing can be written afterwards structTreeRootKids.add(newStructRef); mainParentTreeNumEntry.add(newStructRef); stamperImp.markUsed(mainParentTreeNumEntry); } catch (Exception ex) { logger.error("error", ex); throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF, "error writing structured signature content", ex); } } /** * Finish struct data for signblock and it's elements (NOT for the external link and annot!) * @throws PresentableException */ void finishMainStructData() throws PresentableException { try { if (isTagged && mainParentTreeNumEntry.getIndRef() == null) { getParentTreeNums().add( stamper.getWriter().addToBody(mainParentTreeNumEntry).getIndirectReference()); stamperImp.markUsed(getParentTreeNums()); stamperImp.markUsed(getStructTreeRoot().getAsDict(PdfName.PARENTTREE)); } } catch (Exception ex) { logger.error("error", ex); throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF, "error writing structured signature content", ex); } } /** * Build the structured content for the signature logo (bildmarke). {@link #beginFigureContent(PdfContentByte)} and * {@link #endFigureContent(PdfContentByte)} have to be called before this method to mark the logo in the stream. This * is done implicitly in the modified itext source (see {@link StructContentWriterHolder}). * @param so * @param sigBlockObj * @throws PresentableException */ void buildFigureStructData(SignatureObject so, PdfTemplate sigBlockObj) throws PresentableException { try { if (isTagged && isFigureMarked()) { PdfDictionary structTreeRoot = getStructTreeRoot(); PdfIndirectReference mcrRef = createMcrStructElem(this.figureMcid, sigBlockObj.getIndirectReference()); PdfIndirectReference figureRef = createStructElem("Figure", mcrRef, getAltText(so.getSignatureTypeDefinition().getType()), structTreeRoot.getIndRef()); structTreeRootKids.add(figureRef); mainParentTreeNumEntry.add(figureRef); stamperImp.markUsed(structTreeRootKids); stamperImp.markUsed(structTreeRoot); stamperImp.markUsed(mainParentTreeNumEntry); } } catch (Exception ex) { logger.error("error", ex); throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF, "error writing structured signature content", ex); } } /** * Build the link annotation for the signature verification link and the structured content accordingly.
* The tagging does NOT work if the link is placed in a binary signature replace cell (phlengh for this cell)!! * @param sigBlockObj * @param atp * @throws PresentableException */ void buildVerifyLinkStructData(PdfTemplate sigBlockObj, ActualTablePos atp) throws PresentableException { if (!this.isTagged || !this.isLinkMarked() || !isLinkFound()) return; try { PdfNumber parentTreeKey = getNewParentTreeKey(); PdfArray annots = obrainAnnotsFromPage(); PdfIndirectReference linkAnnotRef = createLinkAnnot(parentTreeKey, atp); annots.add(linkAnnotRef); PdfIndirectReference objr = createObjrStructElem(linkAnnotRef); PdfIndirectReference mcr = createMcrStructElem(this.linkMcid, sigBlockObj.getIndirectReference()); PdfDictionary structTreeRoot = getStructTreeRoot(); PdfArray linkKids = new PdfArray(); PdfIndirectReference linkKidsRef = stamper.getWriter().getPdfIndirectReference(); PdfIndirectReference linkRef = createStructElem("Link", linkKidsRef, structTreeRoot.getIndRef()); linkKids.add(objr); PdfIndirectReference span = createStructElem("Span", mcr, linkRef); linkKids.add(span); stamper.getWriter().addToBody(linkKids, linkKidsRef); structTreeRootKids.add(linkRef); // create new entry in ParentTree PdfArray parentTreeNums = getParentTreeNums(); parentTreeNums.add(parentTreeKey); parentTreeNums.add(linkRef); stamperImp.markUsed(parentTreeNums); stamperImp.markUsed(structTreeRoot.getAsDict(PdfName.PARENTTREE)); stamperImp.markUsed(structTreeRootKids); stamperImp.markUsed(linkKids); stamperImp.markUsed(structTreeRoot); } catch (IOException e) { logger.error("error", e); throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF, "error writing structured signature content", e); } } private boolean isLinkFound() { return this.linkUrlString != null && this.verifyLinkCellPos != null && this.linkPosMap.size() > 0 && this.linkMcid >= 0; } /** * Build new StructParent entry for signature annotation. * @return */ PdfNumber buildAdobeSigStructParent() { if (this.isTagged) { this.annotationParentTreeKey = getNewParentTreeKey(); return annotationParentTreeKey; } else { return null; } } /** * Build and write structured content for adobe signature annotation * * @param sigFormField * @param title * @throws PresentableException */ void buildAdobeSigStruct(PdfFormField sigFormField, String title) throws PresentableException { if (!isTagged) return; try { PdfDictionary root = getStructTreeRoot(); PdfIndirectReference objrRef = createObjrStructElem(sigFormField.getIndirectReference()); PdfIndirectReference adobeSigStructRef = createStructElem("Link", objrRef, root.getIndRef()); PdfArray parentTreeNums = getParentTreeNums(); // create new entry in ParentTree parentTreeNums.add(annotationParentTreeKey); parentTreeNums.add(adobeSigStructRef); structTreeRootKids.add(adobeSigStructRef); stamperImp.markUsed(structTreeRootKids); stamperImp.markUsed(parentTreeNums); stamperImp.markUsed(root.getAsDict(PdfName.PARENTTREE)); } catch (Exception ex) { logger.error("error", ex); throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF, "error writing structured signature content", ex); } } /** * Start tag for signature block content stream. Place this before the signature block is written to a content stream. * Call {@link #endSigBlockContent()} afterwards */ void beginSigBlockContent() { if (isTagged) { content.getInternalBuffer().append(new PdfName(SIGBLOCK_STRUCT_TYPE).getBytes()).append(" <> BDC").append('\n'); } } /** * End tag for signature block content stream. Place this after the signature block is written to a content stream */ void endSigBlockContent() { if (isTagged) { content.endMarkedContentSequence(); } } /** * Writes start tag for signature logo marked content sequence. */ public void beginFigureContent(PdfContentByte localContent) { if (isTagged) { if (!isFigureMarked()) { this.figureMcid = this.nextMcid++; localContent.getInternalBuffer().append("/Figure <> BDC\n"); } else { logger.warn("cannot tag multiple figures (bildmarken)"); } } } /** * Writes end tag for signature logo marked content sequence. */ public void endFigureContent(PdfContentByte localContent) { if (isTagged && isFigureMarked()) { localContent.endMarkedContentSequence(); } } /** * Writes start tag for verify link marked content sequence. */ public void beginLinkContent(PdfContentByte localContent, String urlString) { // it's called from here com.lowagie.text.pdf.PdfContentByte.showText(String) if (isTagged) { if (!isLinkMarked()) { this.linkUrlString = urlString; this.linkMcid = this.nextMcid++; localContent.getInternalBuffer().append("/Span <> BDC\n"); } else { logger.warn("cannot tag multiple verify links"); } } } /** * Writes end tag for verify link marked content sequence. */ public void endLinkContent(PdfContentByte localContent) { if (isTagged && isLinkMarked()) { localContent.endMarkedContentSequence(); } } /** * Implements {@link StructContentWriter#markPos(Rectangle)} */ public void markPos(Rectangle pos) { this.tempMarkedPos = pos; } /** * Implements {@link StructContentWriter#storeCurrentPosAsLink()} */ public void storeCurrentPosAsLink() { this.verifyLinkCellPos = new Rectangle(this.tempMarkedPos); } public void putVal(String key, Object val) { tmpMap.put(key, val); } public void storeVals() { linkPosMap = new HashMap(tmpMap); } /** * set explicit annotation tab order if missing */ private void doAnnoTabOrder() { if (page.getAsName(new PdfName("Tabs")) == null) { page.put(new PdfName("Tabs"), PdfName.S); // set explicit annotation TAB order stamperImp.markUsed(page); } } private void checkTagging() { PdfDictionary markDict = stamper.getReader().getCatalog().getAsDict(PdfName.MARKINFO); if (markDict != null) { isTagged = markDict.getAsBoolean(PdfName.MARKED).booleanValue(); } if (!isTagged) { logger.debug("input document is not tagged. no structure/wai information is written"); } logger.debug("Input is tagged. Writing structure/WAI data."); } private PdfIndirectReference createLinkAnnot(PdfNumber structParentNr, ActualTablePos atp) throws IOException { PdfDictionary linkAnnot = new PdfDictionary(); PdfDictionary a = new PdfDictionary(); a.put(PdfName.S, new PdfName("URI")); a.put(PdfName.TYPE, PdfName.ACTION); a.put(PdfName.URI, new PdfString(this.linkUrlString)); linkAnnot.put(PdfName.A, a); PdfDictionary bs = new PdfDictionary(); bs.put(PdfName.W, new PdfNumber(0)); linkAnnot.put(PdfName.BS, bs); linkAnnot.put(PdfName.F, new PdfNumber(4)); // iText "converts" 0.0f to an integer, therefore we cannot use 0, not nice... //linkAnnot.put(PdfName.RECT, new PdfArray(new float[] {0.01f, 0.01f, 0.01f, 0.01f})); // take cell pos as link pos linkAnnot.put(PdfName.RECT, new PdfArray(calcLinkPos(atp))); linkAnnot.put(PdfName.STRUCTPARENT, structParentNr); linkAnnot.put(PdfName.SUBTYPE, PdfName.LINK); return stamper.getWriter().addToBody(linkAnnot).getIndirectReference(); } private PdfArray calcLinkPos(ActualTablePos atp) { PdfArray res = new PdfArray(); float downY = atp.y - atp.height; float startX = atp.x + this.verifyLinkCellPos.getLeft(); float yLine = getPosMapVal("yLine"); float lineHigh = getPosMapVal("maxSize"); float lineWidth = getPosMapVal("lineWidth"); UrlInTextFinder finder = (UrlInTextFinder) this.linkPosMap.get("urlFinder"); // maybe one could calc the link pos even more exactly with char width counting // but this should be close enough (see BidiLine.processLine and chunk.getcharwith) float lineCorr = -2; float xCorr = 5; res.add(new PdfNumber(1 + startX + finder.calcLinkPosXStart(lineWidth))); res.add(new PdfNumber(downY + yLine + lineHigh + lineCorr)); res.add(new PdfNumber(xCorr + startX + finder.calcLinkPosXEnd(lineWidth))); res.add(new PdfNumber(downY + yLine + lineCorr)); return res; } private float getPosMapVal(String key) { return ((Float) this.linkPosMap.get(key)).floatValue(); } protected static PdfArray createPdfArrayFromTablePos(ActualTablePos pos) { return new PdfArray( new float[] {pos.x, pos.y, pos.x + pos.width, pos.y - pos.height}); } private PdfArray obrainAnnotsFromPage() throws IOException { PdfArray annots = this.page.getAsArray(PdfName.ANNOTS); if (annots == null) { annots = new PdfArray(); page.put(PdfName.ANNOTS, annots); stamperImp.markUsed(this.page); stamper.getWriter().addToBody(annots); } return annots; } private PdfArray obtainStructTreeRootKids(PdfDictionary structTreeRoot) { PdfArray rk = null; PdfObject root_k = structTreeRoot.getDirectObject(PdfName.K); stamperImp.markUsed(root_k); if (root_k instanceof PdfDictionary) { rk = new PdfArray(); stamperImp.markUsed(structTreeRootKids); rk.add(root_k.getIndRef()); structTreeRoot.put(PdfName.K, structTreeRootKids); } else if(root_k != null) { // has to be array rk = (PdfArray) root_k; } return rk; } private PdfArray obtainParentTreeEntry(PdfDictionary structTreeRoot, PdfArray parentTreeNums, PdfNumber structParentsNr, PdfTemplate sigBlockObj) { int numsIdx = -1; PdfArray parentTreeEntry = null; if (structParentsNr == null) { // no StructParents entry yet, make new one and add new parenttree entry PdfNumber parentTreeKey = null; parentTreeNextKey = structTreeRoot.getAsNumber(PARENTTREENEXTKEY); // read next proposed key if (parentTreeNextKey == null) { // this can be null if a non-perfect pdf creator was at work // find the next key by counting int nextI = ((int) parentTreeNums.size() / 2); // know the "Number Trees" data structure from pdf-ref this.parentTreeNextKey = new PdfNumber(nextI); structTreeRoot.put(PARENTTREENEXTKEY, this.parentTreeNextKey); // write ParentTreeNextKey entry } parentTreeKey = new PdfNumber(parentTreeNextKey.intValue()); parentTreeNextKey.increment(); page.put(PdfName.STRUCTPARENTS, parentTreeKey); // write /StructParents entry to page structParentsNr = parentTreeKey; stamperImp.markUsed(page); // create new entry in ParentTree parentTreeNums.add(parentTreeKey); parentTreeEntry = new PdfArray(); numsIdx = parentTreeNums.size() - 1; } else { // structparents entry already available, find parenttree entry //parentTreeKey = structParentsNr; parentTreeNextKey = structTreeRoot.getAsNumber(PARENTTREENEXTKEY); // read next proposed key if (parentTreeNextKey == null) { // this can be null if a non-perfact pdf creator was at work // find the next key by counting int nextI = 0; if (parentTreeNums != null) { nextI = ((int) parentTreeNums.size() / 2); } this.parentTreeNextKey = new PdfNumber(nextI); structTreeRoot.put(PARENTTREENEXTKEY, this.parentTreeNextKey); } } // add Structparents entry to xobject content stream sigBlockObj.addAttribute(PdfName.STRUCTPARENTS, structParentsNr); // find my structParentEntry if (numsIdx < 0) { // it's a weird data structure: "number tree", see pdf reference if you really want to understand // if the array has no gaps it is easy: numsIdx = structParentsNr.intValue() * 2; if (parentTreeNums.getAsNumber(numsIdx).intValue() != structParentsNr.intValue()) { // there seem to be gaps for (numsIdx = 0; numsIdx < parentTreeNums.size(); numsIdx += 2) { // search manually if (parentTreeNums.getAsNumber(numsIdx).intValue() == structParentsNr.intValue()) { break; } } } numsIdx += 1; } if (parentTreeEntry == null) { parentTreeEntry = parentTreeNums.getAsArray(numsIdx); } return parentTreeEntry; } // private PdfIndirectReference createStructElem(String structType, PdfObject kid) throws IOException { // return createStructElem(structType, kid, null); // } private PdfIndirectReference createStructElem(String structType, PdfObject kid, PdfIndirectReference parentRef) throws IOException { return createStructElem(structType, kid, null, parentRef); } private PdfIndirectReference createStructElem(String structType, PdfObject kid, String altText, PdfIndirectReference parentRef) throws IOException { PdfDictionary newStruct = new PdfDictionary(); newStruct.put(PdfName.S, new PdfName(structType)); //newStruct.put(PdfName.T, new PdfString("PDF-AS Signaturblock"));// eher nicht if (parentRef != null) { newStruct.put(PdfName.P, parentRef); } newStruct.put(PdfName.TYPE, new PdfName("StructElem")); newStruct.put(PdfName.PG, page.getIndRef()); if (altText != null) { newStruct.put(PdfName.ALT, new PdfString(altText)); } // newStruct.put(PdfName.ALT, new PdfString(getAltText(so.getSignatureTypeDefinition().getType()))); //newStruct.put(PdfName.K, new PdfNumber(nextMcid)); newStruct.put(PdfName.K, kid); return stamper.getWriter().addToBody(newStruct).getIndirectReference(); } private boolean isFigureMarked() { return this.figureMcid > -1; } private boolean isLinkMarked() { return this.linkMcid > -1; } private PdfNumber getNewParentTreeKey() { // new parent tree entry if (parentTreeNextKey == null) { parentTreeNextKey = getStructTreeRoot().getAsNumber(PARENTTREENEXTKEY); // read next proposed key } PdfNumber res = new PdfNumber(parentTreeNextKey.intValue()); parentTreeNextKey.increment(); return res; } private PdfIndirectReference createObjrStructElem(PdfIndirectReference objRef) throws IOException { PdfDictionary objr = new PdfDictionary(); objr.put(PdfName.TYPE, new PdfName("OBJR")); objr.put(PdfName.PG, page.getIndRef()); objr.put(new PdfName("Obj"), objRef); return stamper.getWriter().addToBody(objr).getIndirectReference(); } private PdfIndirectReference createMcrStructElem(int mcid, PdfIndirectReference streamRef) throws IOException { PdfDictionary objr = new PdfDictionary(); objr.put(PdfName.TYPE, new PdfName("MCR")); objr.put(PdfName.PG, page.getIndRef()); objr.put(PdfName.MCID, new PdfNumber(mcid)); objr.put(new PdfName("Stm"), streamRef); return stamper.getWriter().addToBody(objr).getIndirectReference(); } private PdfArray getParentTreeNums() { return getStructTreeRoot().getAsDict(PdfName.PARENTTREE).getAsArray(PdfName.NUMS); } private PdfDictionary getStructTreeRoot() { return stamper.getReader().getCatalog().getAsDict(PdfName.STRUCTTREEROOT); } private static String getAltText(String sigProfile) { return AdobeSignatureHelper.getDefaultableConfigProperty(sigProfile, ALT_TEXT_CONF_KEY, ALT_TEXT_DEFAULT); } }