c# - 如何使用 HTML 输入文档或使用 ABCPDF 导出为 PDF 后创建空白 PDF 签名字段?

标签 c# pdf pdf-generation digital-signature html-to-pdf

我有一个从 ASPX 生成的源 HTML 文档。

然后我使用 ABCPDF 将 html 转换为 PDF。

html 有一个签名区域,它只是一个带边框的文本框。

我需要在 PDF 中添加一个带有我可以传递的名称的签名字段。

PDF 将发送给第三方,第三方将与客户联系,然后对 PDF 进行数字签名并将其发回。

假设我有一个 html 文档或 PDF,如何以编程方式在原始 html 签名区域周围添加空白 PDF 签名字段或以某种方式将两者关联起来?

这是一个示例应用程序,用于演示我正在做的一些事情:

namespace ABCPDFHtmlSignatureTest
{
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using WebSupergoo.ABCpdf8;
    using WebSupergoo.ABCpdf8.Objects;

    /// <summary>
    /// The program.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// The file name.
        /// </summary>
        private const string FileName = @"c:\temp\pdftest.pdf";

        /// <summary>
        /// The application entry point.
        /// </summary>
        /// <param name="args">
        /// The args.
        /// </param>
        public static void Main(string[] args)
        {
            var html = GetHtml();

            var pdf = GetPdf(html);

            /* save the PDF to disk */
            File.WriteAllBytes(FileName, pdf);

            /* open the PDF */
            Process.Start(FileName);
        }

        /// <summary>
        /// The get PDF.
        /// </summary>
        /// <param name="html">
        /// The html.
        /// </param>
        /// <returns>
        /// The <see cref="byte"/>.
        /// </returns>
        public static byte[] GetPdf(string html)
        {
            var document = new Doc();

            /* Yes, generate PDF fields for the html form inputs */
            document.HtmlOptions.AddForms = true;

            document.AddImageHtml(html);

            /* We can determine the location of the field */
            var signatureRect = document.Form["Signature"].Rect;

            MakeFieldsReadOnly(document.Form.Fields);

            return document.GetData();
        }

        /// <summary>
        /// The get html.
        /// </summary>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        public static string GetHtml()
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ABCPDFHtmlSignatureTest.HTMLPage1.html"))
            {
                using (var streamReader = new StreamReader(stream))
                {
                    return streamReader.ReadToEnd();
                }
            }
        }

        /// <summary>
        /// The make fields read only.
        /// </summary>
        /// <param name="fields">
        /// The fields.
        /// </param>
        private static void MakeFieldsReadOnly(Fields fields)
        {
            foreach (var field in fields)
            {
                if (field.Name == "Signature") continue;

                field.Stamp();
            }
        }
    }
}

最佳答案

希望这可以帮助其他人。

我最终使用了 ABCPDF 和 iTextSharp。

我使用 ABCPDF 将 HTML 转换为 PDF,并告诉我该元素在哪里,然后我使用 iTextSharp 将空白签名字段放在它上面。

这个项目中有一些“陷阱”:

  1. ABCPDF 更擅长将 HTML 转换为 PDF,因为它对非标准 html 更宽容,而且当物理路径包含“/”而不是“\”等小错误时,它更擅长读取物理路径。
  2. 将 html 发送到 PDF 转换器(iTextSharp 或 ABCPDF)时,需要将相对路径更改为物理路径,因为转换器不知道您正在运行哪个网站或在哪个虚拟目录中查找图像、脚本和样式表。 (请参阅下面的转换器,可以帮助解决这个问题)
  3. ABCPDF 能够更好地解释样式表,并且最终结果看起来更好,代码更少。
  4. 当尝试找出 ABCPDF 放置字段或标记元素的位置时,请记住,添加第一页后,您仍然需要进入循环来“链接”或注册其余页面,然后仅您能否解析该字段或标记的元素。

这是一个演示解决方案的示例项目。

示例 html:(注意签名字段样式中的 abcpdf-tag-visible: true 部分,这将帮助我们查看该元素在 PDF 中的位置)

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Test Document</title>
</head>
<body>
    <form method="POST">
        Sample Report Data: <br />
        <table style="border: solid 1px red; margin: 5px" cellpadding="5px">
            <tr>
                <td>Field 1:</td>
                <td><input type="text" id="field1" name="field1" value="FIELD1VALUE" /></td>
            </tr>
            <tr>
                <td>Field 2:</td>
                <td><input type="text" id="Text2" value="FIELD2VALUE" /></td>
            </tr>
            <tr>
                <td>Field 3:</td>
                <td><input type="text" id="Text3" value="FIELD3VALUE" /></td>
            </tr>
            <tr>
                <td>Field 4:</td>
                <td><input type="text" id="Text4" value="FIELD4VALUE" /></td>
            </tr>
            <tr>
                <td>Signature:</td>
                <td><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"></textarea></td>
            </tr>
        </table>       
    </form>
</body>
</html>

这是带有空白签名字段的 PDF 的屏幕截图,随后使用 Adob​​e 打开。

pdfresultscreenshot

帮助测试 PDF 转换器的示例控制台应用程序:

namespace ABCPDFHtmlSignatureTest
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;

    using iTextSharp.text;
    using iTextSharp.text.pdf;

    using WebSupergoo.ABCpdf8;

    /// <summary>
    /// The program.
    /// </summary>
    public class Program
    {
        /// <summary>
        /// The file name.
        /// </summary>
        private const string PdfFileName = @"c:\temp\pdftest.pdf";

        /// <summary>
        /// Adds a blank signature field at the specified location.
        /// </summary>
        /// <param name="pdf">The PDF.</param>
        /// <param name="signatureRect">The signature location.</param>
        /// <param name="signaturePage">the page on which the signature appears</param>
        /// <returns>The new PDF.</returns>
        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();
            }
        }

        /// <summary>
        /// The application entry point.
        /// </summary>
        /// <param name="args">
        /// The args.
        /// </param>
        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);
        }

        /// <summary>
        /// Returns the PDF for the specified html. The conversion is done using ABCPDF.
        /// </summary>
        /// <param name="html">The html.</param>
        /// <param name="pdf">the PDF</param>
        /// <param name="signatureRect">the location of the signature field</param>
        /// <param name="signaturePage">the page of the signature field</param>
        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();
        }

        /// <summary>
        /// The try identify signature location on current page.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <param name="currentPageId">The current page id.</param>
        /// <param name="currentPageNumber">The current page number.</param>
        /// <param name="signatureRect">The signature location.</param>
        /// <param name="signaturePage">The signature page.</param>
        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 > 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;
                    }
                }                
            }
        }

        /// <summary>
        /// The get html.
        /// </summary>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        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();
                }
            }
        }
    }
}

当在 Web 服务器内运行并仍然生成 HTML 时,您可以使用此类将相对(虚拟)路径更改为物理(UNC)路径:

namespace YourNameSpace
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using System.Web;

    /// <summary>
    /// 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.
    /// </summary>
    public sealed class HtmlRelativeToPhysicalPathConverter
    {
        #region FIELDS

        /// <summary>
        /// The _server.
        /// </summary>
        private readonly HttpServerUtility _server;

        /// <summary>
        /// The _html.
        /// </summary>
        private readonly string _html;

        #endregion

        #region CONSTRUCTOR

        /// <summary>
        /// Initialises a new instance of the <see cref="HtmlRelativeToPhysicalPathConverter"/> class.
        /// </summary>
        /// <param name="server">
        /// The server.
        /// </param>
        /// <param name="html">
        /// The html.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// when <paramref name="server"/> or <paramref name="html"/> is null or empty.
        /// </exception>
        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

        /// <summary>
        /// Convert the html.
        /// </summary>
        /// <param name="leaveUrisIfFileCannotBeFound">an additional validation can be performed before changing the uri to a directory path</param>
        /// <returns>The converted html with physical paths in all uris.</returns>
        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

        /// <summary>
        /// Convert a uri to the physical path.
        /// </summary>
        /// <param name="htmlBuilder">The html builder.</param>
        /// <param name="relativePath">The relative path or uri string.</param>
        /// <param name="leaveUrisIfFileCannotBeFound">an additional validation can be performed before changing the uri to a directory path</param>
        private void ReplaceRelativePath(StringBuilder htmlBuilder, string relativePath, bool leaveUrisIfFileCannotBeFound)
        {
            try
            {
                var parts = relativePath.Split('?');
                var mappedPath = _server.MapPath(parts[0]);
                if ((leaveUrisIfFileCannotBeFound && File.Exists(mappedPath)) || !leaveUrisIfFileCannotBeFound)
                {
                    if (parts.Length > 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
        /// <summary>
        /// They are NOT guaranteed to be valid uris, simply values between quote characters.
        /// </summary>
        /// <param name="html">the html builder</param>
        /// <param name="quoteChar">the quote character to use, e.g. " or '</param>
        /// <returns>each of the relative paths</returns>
        private IEnumerable<string> 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)
                    && uriString != previousUriString
                    /* as far as I know we never reference a file without an extension, so avoid the IDs this way */
                    && uriString.Contains(".") && !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

    }
}

您可以像这样使用该类:

var html = textWriter.ToString();

// change relative paths to be absolute
html = new HtmlRelativeToPhysicalPathConverter(server, html).ConvertHtml();

关于c# - 如何使用 HTML 输入文档或使用 ABCPDF 导出为 PDF 后创建空白 PDF 签名字段?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20640490/

相关文章:

c# - 强制派生类调用带有实现的基方法

c# - string.Format() 给出 "Input string is not in correct format"

c# - 时间测量 CUDA 和 C#

c# - iTextSharp 是否处理 PDF 压缩?

javascript - 如何使用 Javascript 提交 PDF 格式的表单?

php - 动态PHP网页保存为PDF

c# - 在哪里放置代码以在 View 中创建大量按钮?

grails - Grails 3.x 中的 Jasper

java - 让印地语显示在使用 JSP 创建的 pdf 上

java - 如何将 JPanel 高级转换为pdf