/**
* 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);
} catch (Exception ex) {
logger.error("error", ex);
throw new PresentableException(ErrorCode.CANNOT_WRITE_PDF,
"error writing structured signature content", ex);
}
}
/**
* 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 { // 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);
}
}