Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>Hopefully this can help someone else.</p> <p>I ended up using both ABCPDF and iTextSharp.</p> <p>I used ABCPDF to convert the HTML to PDF, and to tell me where the element is, and then I used iTextSharp to put the blank signature field over it.</p> <p>There were a couple of "gotchas" in this project:</p> <ol> <li>ABCPDF was better at converting HTML to PDF because it is more forgiving with non-standard html, and it was better at reading the physical paths when they contained small mistakes like "/" instead of "\".</li> <li>When sending your html to the PDF converter (iTextSharp or ABCPDF), the relative paths need to be changed into physical paths because the converter won't know in which web-site you are running or in which virtual directories to find the images, scripts and stylesheets. (See a converter below that can help with that)</li> <li>ABCPDF was better at interpreting the style sheets, and the end result looked much better with less code.</li> <li>When trying to figure out where ABCPDF placed the fields or tagged elements, bear in mind that after you add the first page, you still have to go into a loop to "chain" or register the rest of the pages, then only will you be able to resolve the field or the tagged element.</li> </ol> <p>Here is a sample project to demonstrate the solution.</p> <p>The sample html: (notice the <strong>abcpdf-tag-visible: true</strong> part in the style of the signature field, this will help us to see where the element is placed in the PDF)</p> <pre><code>&lt;!DOCTYPE html&gt; &lt;html lang="en" xmlns="http://www.w3.org/1999/xhtml"&gt; &lt;head&gt; &lt;meta charset="utf-8" /&gt; &lt;title&gt;Test Document&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;form method="POST"&gt; Sample Report Data: &lt;br /&gt; &lt;table style="border: solid 1px red; margin: 5px" cellpadding="5px"&gt; &lt;tr&gt; &lt;td&gt;Field 1:&lt;/td&gt; &lt;td&gt;&lt;input type="text" id="field1" name="field1" value="FIELD1VALUE" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Field 2:&lt;/td&gt; &lt;td&gt;&lt;input type="text" id="Text2" value="FIELD2VALUE" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Field 3:&lt;/td&gt; &lt;td&gt;&lt;input type="text" id="Text3" value="FIELD3VALUE" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Field 4:&lt;/td&gt; &lt;td&gt;&lt;input type="text" id="Text4" value="FIELD4VALUE" /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;Signature:&lt;/td&gt; &lt;td&gt;&lt;textarea id="ClientSignature" style="background-color:LightCyan;border-color:Gray;border-width:1px;border-style:Solid;height:50px;width:200px;abcpdf-tag-visible: true" rows="2" cols="20"&gt;&lt;/textarea&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/table&gt; &lt;/form&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>Here is a screen shot of the PDF with a blank signature field, opened with Adobe afterwards.</p> <p><img src="https://i.stack.imgur.com/x7PIF.jpg" alt="pdfresultscreenshot"></p> <p>A sample console application to help test out the PDF converters:</p> <pre><code>namespace ABCPDFHtmlSignatureTest { using System; using System.Diagnostics; using System.IO; using System.Reflection; using iTextSharp.text; using iTextSharp.text.pdf; using WebSupergoo.ABCpdf8; /// &lt;summary&gt; /// The program. /// &lt;/summary&gt; public class Program { /// &lt;summary&gt; /// The file name. /// &lt;/summary&gt; private const string PdfFileName = @"c:\temp\pdftest.pdf"; /// &lt;summary&gt; /// Adds a blank signature field at the specified location. /// &lt;/summary&gt; /// &lt;param name="pdf"&gt;The PDF.&lt;/param&gt; /// &lt;param name="signatureRect"&gt;The signature location.&lt;/param&gt; /// &lt;param name="signaturePage"&gt;the page on which the signature appears&lt;/param&gt; /// &lt;returns&gt;The new PDF.&lt;/returns&gt; private static byte[] AddBlankSignatureField(byte[] pdf, Rectangle signatureRect, int signaturePage) { var pdfReader = new PdfReader(pdf); using (var ms = new MemoryStream()) { var pdfStamper = new PdfStamper(pdfReader, ms); var signatureField = PdfFormField.CreateSignature(pdfStamper.Writer); signatureField.SetWidget(signatureRect, null); signatureField.Flags = PdfAnnotation.FLAGS_PRINT; signatureField.Put(PdfName.DA, new PdfString("/Helv 0 Tf 0 g")); signatureField.FieldName = "ClientSignature"; signatureField.Page = signaturePage; pdfStamper.AddAnnotation(signatureField, signaturePage); pdfStamper.Close(); return ms.ToArray(); } } /// &lt;summary&gt; /// The application entry point. /// &lt;/summary&gt; /// &lt;param name="args"&gt; /// The args. /// &lt;/param&gt; public static void Main(string[] args) { var html = GetHtml(); XRect signatureRect; int signaturePage; byte[] pdf; GetPdfUsingAbc(html, out pdf, out signatureRect, out signaturePage); /* convert to type that iTextSharp needs */ var signatureRect2 = new Rectangle( Convert.ToSingle(signatureRect.Left), Convert.ToSingle(signatureRect.Top), Convert.ToSingle(signatureRect.Right), Convert.ToSingle(signatureRect.Bottom)); pdf = AddBlankSignatureField(pdf, signatureRect2, signaturePage); /* save the PDF to disk */ File.WriteAllBytes(PdfFileName, pdf); /* open the PDF */ Process.Start(PdfFileName); } /// &lt;summary&gt; /// Returns the PDF for the specified html. The conversion is done using ABCPDF. /// &lt;/summary&gt; /// &lt;param name="html"&gt;The html.&lt;/param&gt; /// &lt;param name="pdf"&gt;the PDF&lt;/param&gt; /// &lt;param name="signatureRect"&gt;the location of the signature field&lt;/param&gt; /// &lt;param name="signaturePage"&gt;the page of the signature field&lt;/param&gt; public static void GetPdfUsingAbc(string html, out byte[] pdf, out XRect signatureRect, out int signaturePage) { var document = new Doc(); document.MediaBox.String = "A4"; document.Color.String = "255 255 255"; document.FontSize = 7; /* tag elements marked with "abcpdf-tag-visible: true" */ document.HtmlOptions.AddTags = true; int pageId = document.AddImageHtml(html, true, 950, true); int pageNumber = 1; signatureRect = null; signaturePage = -1; TryIdentifySignatureLocationOnCurrentPage(document, pageId, pageNumber, ref signatureRect, ref signaturePage); while (document.Chainable(pageId)) { document.Page = document.AddPage(); pageId = document.AddImageToChain(pageId); pageNumber++; TryIdentifySignatureLocationOnCurrentPage(document, pageId, pageNumber, ref signatureRect, ref signaturePage); } pdf = document.GetData(); } /// &lt;summary&gt; /// The try identify signature location on current page. /// &lt;/summary&gt; /// &lt;param name="document"&gt;The document.&lt;/param&gt; /// &lt;param name="currentPageId"&gt;The current page id.&lt;/param&gt; /// &lt;param name="currentPageNumber"&gt;The current page number.&lt;/param&gt; /// &lt;param name="signatureRect"&gt;The signature location.&lt;/param&gt; /// &lt;param name="signaturePage"&gt;The signature page.&lt;/param&gt; private static void TryIdentifySignatureLocationOnCurrentPage(Doc document, int currentPageId, int currentPageNumber, ref XRect signatureRect, ref int signaturePage) { if (null != signatureRect) return; var tagIds = document.HtmlOptions.GetTagIDs(currentPageId); if (tagIds.Length &gt; 0) { int index = -1; foreach (var tagId in tagIds) { index++; if (tagId.Contains("ClientSignature")) { var rects = document.HtmlOptions.GetTagRects(currentPageId); signatureRect = rects[index]; signaturePage = currentPageNumber; break; } } } } /// &lt;summary&gt; /// The get html. /// &lt;/summary&gt; /// &lt;returns&gt; /// The &lt;see cref="string"/&gt;. /// &lt;/returns&gt; public static string GetHtml() { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ABCPDFHtmlSignatureTest.HTMLPage1.html")) { if (null == stream) { throw new InvalidOperationException("Unable to resolve the html"); } using (var streamReader = new StreamReader(stream)) { return streamReader.ReadToEnd(); } } } } } </code></pre> <p>When running inside the web server and still generating the HTML, you can use this class to change the relative (virtual) paths to physical (UNC) paths:</p> <pre><code>namespace YourNameSpace { using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Web; /// &lt;summary&gt; /// Replaces all uris within in an html document to physical paths, making it valid /// html outside the context of a web site. This is necessary because outside the /// context of a web site root folder, the uris are meaningless, and the html cannot /// be interpreted correctly by external components, like ABCPDF or iTextSharp. /// Without this step, the images and other 'SRC' references cannot be resolved. /// &lt;/summary&gt; public sealed class HtmlRelativeToPhysicalPathConverter { #region FIELDS /// &lt;summary&gt; /// The _server. /// &lt;/summary&gt; private readonly HttpServerUtility _server; /// &lt;summary&gt; /// The _html. /// &lt;/summary&gt; private readonly string _html; #endregion #region CONSTRUCTOR /// &lt;summary&gt; /// Initialises a new instance of the &lt;see cref="HtmlRelativeToPhysicalPathConverter"/&gt; class. /// &lt;/summary&gt; /// &lt;param name="server"&gt; /// The server. /// &lt;/param&gt; /// &lt;param name="html"&gt; /// The html. /// &lt;/param&gt; /// &lt;exception cref="ArgumentNullException"&gt; /// when &lt;paramref name="server"/&gt; or &lt;paramref name="html"/&gt; is null or empty. /// &lt;/exception&gt; public HtmlRelativeToPhysicalPathConverter(HttpServerUtility server, string html) { if (null == server) throw new ArgumentNullException("server"); if (string.IsNullOrWhiteSpace(html)) throw new ArgumentNullException("html"); _server = server; _html = html; } #endregion #region Convert Html /// &lt;summary&gt; /// Convert the html. /// &lt;/summary&gt; /// &lt;param name="leaveUrisIfFileCannotBeFound"&gt;an additional validation can be performed before changing the uri to a directory path&lt;/param&gt; /// &lt;returns&gt;The converted html with physical paths in all uris.&lt;/returns&gt; public string ConvertHtml(bool leaveUrisIfFileCannotBeFound = false) { var htmlBuilder = new StringBuilder(_html); // Double quotes foreach (var relativePath in this.GetRelativePaths(htmlBuilder, '"')) { this.ReplaceRelativePath(htmlBuilder, relativePath, leaveUrisIfFileCannotBeFound); } // Single quotes foreach (var relativePath in this.GetRelativePaths(htmlBuilder, '\'')) { this.ReplaceRelativePath(htmlBuilder, relativePath, leaveUrisIfFileCannotBeFound); } return htmlBuilder.ToString(); } #endregion #region Replace Relative Path /// &lt;summary&gt; /// Convert a uri to the physical path. /// &lt;/summary&gt; /// &lt;param name="htmlBuilder"&gt;The html builder.&lt;/param&gt; /// &lt;param name="relativePath"&gt;The relative path or uri string.&lt;/param&gt; /// &lt;param name="leaveUrisIfFileCannotBeFound"&gt;an additional validation can be performed before changing the uri to a directory path&lt;/param&gt; private void ReplaceRelativePath(StringBuilder htmlBuilder, string relativePath, bool leaveUrisIfFileCannotBeFound) { try { var parts = relativePath.Split('?'); var mappedPath = _server.MapPath(parts[0]); if ((leaveUrisIfFileCannotBeFound &amp;&amp; File.Exists(mappedPath)) || !leaveUrisIfFileCannotBeFound) { if (parts.Length &gt; 1) { mappedPath += "?" + parts[1]; } htmlBuilder.Replace(relativePath, mappedPath); } else { /* decide what you want to do with these */ } } catch (ArgumentException) { /* ignore these */ } } #endregion #region Get Relative Paths /// &lt;summary&gt; /// They are NOT guaranteed to be valid uris, simply values between quote characters. /// &lt;/summary&gt; /// &lt;param name="html"&gt;the html builder&lt;/param&gt; /// &lt;param name="quoteChar"&gt;the quote character to use, e.g. " or '&lt;/param&gt; /// &lt;returns&gt;each of the relative paths&lt;/returns&gt; private IEnumerable&lt;string&gt; GetRelativePaths(StringBuilder html, char quoteChar) { var position = 0; var oldPosition = -1; var htmlString = html.ToString(); var previousUriString = string.Empty; while (oldPosition != position) { oldPosition = position; position = htmlString.IndexOf(quoteChar, position + 1); if (position == -1) break; var uriString = htmlString.Substring(oldPosition + 1, (position - oldPosition) - 1); if (Uri.IsWellFormedUriString(uriString, UriKind.Relative) &amp;&amp; uriString != previousUriString /* as far as I know we never reference a file without an extension, so avoid the IDs this way */ &amp;&amp; uriString.Contains(".") &amp;&amp; !uriString.EndsWith(".")) { yield return uriString; /* refresh the html string, and reiterate again */ htmlString = html.ToString(); position = oldPosition; oldPosition = position - 1; /* don't exit yet */ previousUriString = uriString; } } } #endregion } } </code></pre> <p>You can use the class like this:</p> <pre><code>var html = textWriter.ToString(); // change relative paths to be absolute html = new HtmlRelativeToPhysicalPathConverter(server, html).ConvertHtml(); </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload