TimeSheetIncremental.cs
// 完毕:
using System;
using System.IO;
using System.Linq;
using System.Drawing;
using System.Collections.Generic;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Pdf.AcroForms;
using GrapeCity.Documents.Text;
using GrapeCity.Documents.Common;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Pdf.Security;
using System.Security.Cryptography.X509Certificates;
using GCTEXT = GrapeCity.Documents.Text;
using GCDRAW = GrapeCity.Documents.Drawing;
namespace DsPdfWeb.Demos
{
// 这个示例与TimeSheet几乎相同,但有一个显着差异:
// 与其他示例不同,在此示例中,填写的表格由以下人员进行数字签名
// 员工,并且主管使用签名的 PDF 再次签名
// 增量更新(在已签名的 PDF 上签名的唯一方法
// 保留第一个签名的有效性)。
//
// 注意:如果您下载此示例并在您自己的系统上本地运行它,
// 您需要拥有有效的许可证才能使其按预期工作,因为
// 在未经许可的版本中,自动添加的导航页面标题将
// 使员工签名无效。
public class TimeSheetIncremental
{
// 字体集合来保存我们需要的字体:
private FontCollection _fc = new FontCollection();
// 展平文档时用于呈现输入字段的文本布局:
private TextLayout _inputTl = new TextLayout(72);
// 用于输入字段的文本格式:
private TextFormat _inputTf = new TextFormat();
private GCTEXT.Font _inputFont = FontCollection.SystemFonts.FindFamilyName("Segoe UI", true);
private float _inputFontSize = 12;
// 输入字段边距:
private float _inputMargin = 5;
// 员工签名空间:
private RectangleF _empSignRect;
//
private GCDRAW.Image _logo;
// 该示例的主要入口点:
public int CreatePDF(Stream stream)
{
// 使用我们需要的字体设置字体集合:
_fc.RegisterDirectory(Path.Combine("Resources", "Fonts"));
// 在输入字段的文本布局上设置该字体集合
// (我们还将在我们将使用的所有文本布局上设置它):
_inputTl.FontCollection = _fc;
// 设置输入字段的布局和格式:
_inputTl.ParagraphAlignment = ParagraphAlignment.Center;
_inputTf.Font = _inputFont;
_inputTf.FontSize = _inputFontSize;
// 创建时间表输入表单
// (在现实生活中,我们可能只会创建一次,
// 然后重新使用 PDF 表单):
var doc = MakeTimeSheetForm();
// 此时,“doc”是一个空的 AcroForm。
// 在现实生活中的应用程序中,它将分发给员工
// 让他们填写并寄回。
using (var empSignedStream = FillEmployeeData(doc))
{
// 此时,“empSignedStream”包含填写了员工数据并由他们签名的表单。
// 加载员工签名的文档:
doc.Load(empSignedStream);
// 填写主管数据:
var supName = "Jane Donahue";
var supSignDate = Common.Util.TimeNow().ToShortDateString();
SetFieldValue(doc, _Names.EmpSuper, supName);
SetFieldValue(doc, _Names.SupSignDate, supSignDate);
// 代表主管对文档进行数字签名:
var pfxPath = Path.Combine("Resources", "Misc", "DsPdfTest.pfx");
var cert = new X509Certificate2(File.ReadAllBytes(pfxPath), "qq",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
var sp = new SignatureProperties()
{
SignatureBuilder = new Pkcs7SignatureBuilder()
{
CertificateChain = new X509Certificate2[] { cert },
HashAlgorithm = OID.HashAlgorithms.SHA512
},
Location = "DsPdfWeb - TimeSheet Incremental",
SignerName = supName,
SigningDateTime = Common.Util.TimeNow(),
// 连接签名字段和签名属性:
SignatureField = doc.AcroForm.Fields.First(f_ => f_.Name == _Names.SupSign) as SignatureField,
};
// 对文档的任何更改都会使员工的签名无效,因此我们不能这样做:
// supSign.Widget.ButtonAppearance.Caption =supName;
//
// 完成后,现在保存带有主管签名的文档:
// 注意:为了不使员工的签名无效,
// 我们必须在这里使用增量更新(在 Sign() 方法中默认是这样):
doc.Sign(sp, stream);
_logo.Dispose();
return doc.Pages.Count;
}
}
// 将文档中的任何文本字段替换为常规文本,
// 除了“excludeFields”中列出的字段:
private void FlattenDoc(GcPdfDocument doc, params string[] excludeFields)
{
foreach (var f in doc.AcroForm.Fields)
{
if (f is TextField fld && !excludeFields.Contains(fld.Name))
{
var w = fld.Widget;
var g = w.Page.Graphics;
_inputTl.Clear();
_inputTl.Append(fld.Value, _inputTf);
_inputTl.MaxHeight = w.Rect.Height;
_inputTl.PerformLayout(true);
g.DrawTextLayout(_inputTl, w.Rect.Location);
}
}
for (int i = doc.AcroForm.Fields.Count - 1; i >= 0; --i)
if (doc.AcroForm.Fields[i] is TextField fld && !excludeFields.Contains(fld.Name))
doc.AcroForm.Fields.RemoveAt(i);
}
// 数据字段名称:
static class _Names
{
public static readonly string[] Dows = new string[]
{
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
};
public const string EmpName = "empName";
public const string EmpTitle = "empTitle";
public const string EmpNum = "empNum";
public const string EmpStatus = "empStatus";
public const string EmpDep = "empDep";
public const string EmpSuper = "empSuper";
public static Dictionary<string, string[]> DtNames = new Dictionary<string, string[]>()
{
{"Sun", new string[] { "dtSun", "tSunStart", "tSunEnd", "tSunReg", "tSunOvr", "tSunTotal" } },
{"Mon", new string[] { "dtMon", "tMonStart", "tMonEnd", "tMonReg", "tMonOvr", "tMonTotal" } },
{"Tue", new string[] { "dtTue", "tTueStart", "tTueEnd", "tTueReg", "tTueOvr", "tTueTotal" } },
{"Wed", new string[] { "dtWed", "tWedStart", "tWedEnd", "tWedReg", "tWedOvr", "tWedTotal" } },
{"Thu", new string[] { "dtThu", "tThuStart", "tThuEnd", "tThuReg", "tThuOvr", "tThuTotal" } },
{"Fri", new string[] { "dtFri", "tFriStart", "tFriEnd", "tFriReg", "tFriOvr", "tFriTotal" } },
{"Sat", new string[] { "dtSat", "tSatStart", "tSatEnd", "tSatReg", "tSatOvr", "tSatTotal" } },
};
public const string TotalReg = "totReg";
public const string TotalOvr = "totOvr";
public const string TotalHours = "totHours";
public const string EmpSign = "empSign";
public const string EmpSignDate = "empSignDate";
public const string SupSign = "supSign";
public const string SupSignDate = "supSignDate";
}
// 创建考勤表表单:
private GcPdfDocument MakeTimeSheetForm()
{
const float marginH = 72, marginV = 48;
var doc = new GcPdfDocument();
var page = doc.NewPage();
var g = page.Graphics;
var ip = new PointF(marginH, marginV);
var tl = new TextLayout(g.Resolution) { FontCollection = _fc };
tl.Append("TIME SHEET", new TextFormat() { FontName = "Segoe UI", FontSize = 18 });
tl.PerformLayout(true);
g.DrawTextLayout(tl, ip);
ip.Y += tl.ContentHeight + 15;
_logo = GCDRAW.Image.FromFile(Path.Combine("Resources", "ImagesBis", "AcmeLogo-vertical-250px.png"));
var s = new SizeF(250f * 0.75f, 64f * 0.75f);
g.DrawImage(_logo, new RectangleF(ip, s), null, ImageAlign.Default);
ip.Y += s.Height + 5;
tl.Clear();
tl.Append("Where Business meets Technology",
new TextFormat() { FontName = "Segoe UI", FontItalic = true, FontSize = 10 });
tl.PerformLayout(true);
g.DrawTextLayout(tl, ip);
ip.Y += tl.ContentHeight + 15;
tl.Clear();
tl.Append("1901, Halford Avenue,\r\nSanta Clara, California – 95051-2553,\r\nUnited States",
new TextFormat() { FontName = "Segoe UI", FontSize = 9 });
tl.MaxWidth = page.Size.Width - marginH * 2;
tl.TextAlignment = TextAlignment.Trailing;
tl.PerformLayout(true);
g.DrawTextLayout(tl, ip);
ip.Y += tl.ContentHeight + 25;
var pen = new GCDRAW.Pen(Color.Gray, 0.5f);
var colw = (page.Size.Width - marginH * 2) / 2;
var fields1 = DrawTable(ip,
new float[] { colw, colw },
new float[] { 30, 30, 30 },
g, pen);
var tf = new TextFormat() { FontName = "Segoe UI", FontSize = 9 };
tl.ParagraphAlignment = ParagraphAlignment.Center;
tl.TextAlignment = TextAlignment.Leading;
tl.MarginLeft = tl.MarginRight = tl.MarginTop = tl.MarginBottom = 4;
// t_ - 标题
// b_ - 边界
// f_ - 字段名称,null表示没有字段
Action<string, RectangleF, string> drawField = (t_, b_, f_) =>
{
float tWidth;
if (!string.IsNullOrEmpty(t_))
{
tl.Clear();
tl.MaxHeight = b_.Height;
tl.MaxWidth = b_.Width;
tl.Append(t_, tf);
tl.PerformLayout(true);
g.DrawTextLayout(tl, b_.Location);
tWidth = tl.ContentRectangle.Right;
}
else
tWidth = 0;
if (!string.IsNullOrEmpty(f_))
{
var fld = new TextField() { Name = f_ };
fld.Widget.Page = page;
fld.Widget.Rect = new RectangleF(
b_.X + tWidth + _inputMargin, b_.Y + _inputMargin,
b_.Width - tWidth - _inputMargin * 2, b_.Height - _inputMargin * 2);
fld.Widget.DefaultAppearance.Font = _inputFont;
fld.Widget.DefaultAppearance.FontSize = _inputFontSize;
fld.Widget.Border.Color = Color.LightSlateGray;
fld.Widget.Border.Width = 0.5f;
doc.AcroForm.Fields.Add(fld);
}
};
drawField("EMPLOYEE NAME: ", fields1[0, 0], _Names.EmpName);
drawField("TITLE: ", fields1[1, 0], _Names.EmpTitle);
drawField("EMPLOYEE NUMBER: ", fields1[0, 1], _Names.EmpNum);
drawField("STATUS: ", fields1[1, 1], _Names.EmpStatus);
drawField("DEPARTMENT: ", fields1[0, 2], _Names.EmpDep);
drawField("SUPERVISOR: ", fields1[1, 2], _Names.EmpSuper);
ip.Y = fields1[0, 2].Bottom;
float col0 = 100;
colw = (page.Size.Width - marginH * 2 - col0) / 5;
float rowh = 25;
var fields2 = DrawTable(ip,
new float[] { col0, colw, colw, colw, colw, colw },
new float[] { 50, rowh, rowh, rowh, rowh, rowh, rowh, rowh, rowh },
g, pen);
tl.ParagraphAlignment = ParagraphAlignment.Far;
drawField("DATE", fields2[0, 0], null);
drawField("START TIME", fields2[1, 0], null);
drawField("END TIME", fields2[2, 0], null);
drawField("REGULAR HOURS", fields2[3, 0], null);
drawField("OVERTIME HOURS", fields2[4, 0], null);
tf.FontBold = true;
drawField("TOTAL HOURS", fields2[5, 0], null);
tf.FontBold = false;
tl.ParagraphAlignment = ParagraphAlignment.Center;
tf.ForeColor = Color.Gray;
for (int i = 0; i < 7; ++i)
drawField(_Names.Dows[i], fields2[0, i + 1], _Names.DtNames[_Names.Dows[i]][0]);
// 垂直对齐日期字段(补偿不同的 DOW 宽度):
var dowFields = doc.AcroForm.Fields.TakeLast(7);
var minW = dowFields.Min(f_ => ((TextField)f_).Widget.Rect.Width);
dowFields.ToList().ForEach(f_ =>
{
var r_ = ((TextField)f_).Widget.Rect;
r_.Offset(r_.Width - minW, 0);
r_.Width = minW;
((TextField)f_).Widget.Rect = r_;
});
tf.ForeColor = Color.Black;
for (int row = 1; row <= 7; ++row)
for (int col = 1; col <= 5; ++col)
drawField(null, fields2[col, row], _Names.DtNames[_Names.Dows[row - 1]][col]);
tf.FontBold = true;
drawField("WEEKLY TOTALS", fields2[0, 8], null);
tf.FontBold = false;
drawField(null, fields2[3, 8], _Names.TotalReg);
drawField(null, fields2[4, 8], _Names.TotalOvr);
drawField(null, fields2[5, 8], _Names.TotalHours);
ip.Y = fields2[0, 8].Bottom;
col0 = 72 * 4;
colw = page.Size.Width - marginH * 2 - col0;
var fields3 = DrawTable(ip,
new float[] { col0, colw },
new float[] { rowh + 10, rowh, rowh },
g, pen);
drawField("EMPLOYEE SIGNATURE: ", fields3[0, 1], null);
// 员工签名:
var r = fields3[0, 1];
_empSignRect = new RectangleF(r.X + r.Width / 2, r.Y, r.Width / 2 - _inputMargin * 2, r.Height);
var sf = new SignatureField() { Name = _Names.EmpSign };
sf.Widget.Rect = new RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2);
sf.Widget.Page = page;
sf.Widget.BackColor = Color.LightSeaGreen;
doc.AcroForm.Fields.Add(sf);
drawField("DATE: ", fields3[1, 1], _Names.EmpSignDate);
drawField("SUPERVISOR SIGNATURE: ", fields3[0, 2], null);
// 主管签字:
r = fields3[0, 2];
sf = new SignatureField() { Name = _Names.SupSign };
sf.Widget.Rect = new RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2);
sf.Widget.Page = page;
sf.Widget.BackColor = Color.LightYellow;
doc.AcroForm.Fields.Add(sf);
drawField("DATE: ", fields3[1, 2], _Names.SupSignDate);
// 完毕:
return doc;
}
// 简单的表格绘制方法。返回表格单元格矩形数组。
private RectangleF[,] DrawTable(PointF loc, float[] widths, float[] heights, GcGraphics g, GCDRAW.Pen p)
{
if (widths.Length == 0 || heights.Length == 0)
throw new Exception("Table must have some columns and rows.");
RectangleF[,] cells = new RectangleF[widths.Length, heights.Length];
var r = new RectangleF(loc, new SizeF(widths.Sum(), heights.Sum()));
// 绘制左边框(第一个除外):
float x = loc.X;
for (int i = 0; i < widths.Length; ++i)
{
for (int j = 0; j < heights.Length; ++j)
{
cells[i, j].X = x;
cells[i, j].Width = widths[i];
}
if (i > 0)
g.DrawLine(x, r.Top, x, r.Bottom, p);
x += widths[i];
}
// 绘制顶部边框(第一个除外):
float y = loc.Y;
for (int j = 0; j < heights.Length; ++j)
{
for (int i = 0; i < widths.Length; ++i)
{
cells[i, j].Y = y;
cells[i, j].Height = heights[j];
}
if (j > 0)
g.DrawLine(r.Left, y, r.Right, y, p);
y += heights[j];
}
// 绘制外边框:
g.DrawRectangle(r, p);
// 完毕:
return cells;
}
// 使用示例数据填写员工信息和工作时间:
private Stream FillEmployeeData(GcPdfDocument doc)
{
// 出于本示例的目的,我们使用随机数据填写表格:
var empName = "Jaime Smith";
SetFieldValue(doc, _Names.EmpName, empName);
SetFieldValue(doc, _Names.EmpNum, "12345");
SetFieldValue(doc, _Names.EmpDep, "Research & Development");
SetFieldValue(doc, _Names.EmpTitle, "Senior Developer");
SetFieldValue(doc, _Names.EmpStatus, "Full Time");
var rand = new Random((int)Common.Util.TimeNow().Ticks);
DateTime workday = Common.Util.TimeNow().AddDays(-15);
while (workday.DayOfWeek != DayOfWeek.Sunday)
workday = workday.AddDays(1);
TimeSpan wkTot = TimeSpan.Zero, wkReg = TimeSpan.Zero, wkOvr = TimeSpan.Zero;
for (int i = 0; i < 7; ++i)
{
// 开始时间:
var start = new DateTime(workday.Year, workday.Month, workday.Day, rand.Next(6, 12), rand.Next(0, 59), 0);
SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][0], start.ToShortDateString());
SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][1], start.ToShortTimeString());
// 时间结束:
var end = start.AddHours(rand.Next(8, 14)).AddMinutes(rand.Next(0, 59));
SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][2], end.ToShortTimeString());
var tot = end - start;
var reg = TimeSpan.FromHours((start.DayOfWeek != DayOfWeek.Saturday && start.DayOfWeek != DayOfWeek.Sunday) ? 8 : 0);
var ovr = tot.Subtract(reg);
SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][3], reg.ToString(@"hh\:mm"));
SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][4], ovr.ToString(@"hh\:mm"));
SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][5], tot.ToString(@"hh\:mm"));
wkTot += tot;
wkOvr += ovr;
wkReg += reg;
//
workday = workday.AddDays(1);
}
SetFieldValue(doc, _Names.TotalReg, wkReg.TotalHours.ToString("F"));
SetFieldValue(doc, _Names.TotalOvr, wkOvr.TotalHours.ToString("F"));
SetFieldValue(doc, _Names.TotalHours, wkTot.TotalHours.ToString("F"));
SetFieldValue(doc, _Names.EmpSignDate, workday.ToShortDateString());
// 代表“员工”对文档进行数字签名:
var pfxPath = Path.Combine("Resources", "Misc", "JohnDoe.pfx");
var cert = new X509Certificate2(File.ReadAllBytes(pfxPath), "secret",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
var sp = new SignatureProperties()
{
SignatureBuilder = new Pkcs7SignatureBuilder()
{
CertificateChain = new X509Certificate2[] { cert }
},
DocumentAccessPermissions = AccessPermissions.FormFillingAndAnnotations,
Reason = "I confirm time sheet is correct.",
Location = "TimeSheetIncremental sample",
SignerName = empName,
SigningDateTime = Common.Util.TimeNow(),
};
// 连接签名字段和签名属性:
SignatureField empSign = doc.AcroForm.Fields.First(f_ => f_.Name == _Names.EmpSign) as SignatureField;
sp.SignatureField = empSign;
empSign.Widget.ButtonAppearance.Caption = empName;
// 某些浏览器 PDF 查看器不显示表单字段,因此我们渲染一个占位符:
empSign.Widget.Page.Graphics.DrawString("digitally signed", new TextFormat() { FontName = "Segoe UI", FontSize = 9 }, empSign.Widget.Rect);
// 我们现在“展平”表单:循环遍历文档 AcroForm 的字段,
// 将其当前值绘制到位,然后删除字段。
// 这将生成一个 PDF,其中文本字段的值作为常规的一部分
// (不可编辑)内容(我们留下由主管填写的字段):
FlattenDoc(doc, _Names.EmpSuper, _Names.SupSignDate);
// 完成后,现在保存带有员工签名的文档:
var ms = new MemoryStream();
// 请注意,我们在这里不使用增量更新(第三个参数为 false)
// 因为这还不需要(但稍后主管签名时将需要/使用):
doc.Sign(sp, ms, false);
return ms;
}
// 设置具有指定名称的字段的值:
private void SetFieldValue(GcPdfDocument doc, string name, string value)
{
var fld = doc.AcroForm.Fields.First(f_ => f_.Name == name);
if (fld != null)
fld.Value = value;
}
}
}