TimeSheetIncremental.vb
'' 完毕:
Imports System.IO
Imports System.Drawing
Imports GrapeCity.Documents.Pdf
Imports GrapeCity.Documents.Pdf.AcroForms
Imports GrapeCity.Documents.Text
Imports GrapeCity.Documents.Common
Imports GrapeCity.Documents.Drawing
Imports GrapeCity.Documents.Pdf.Security
Imports System.Security.Cryptography.X509Certificates
Imports GCTEXT = GrapeCity.Documents.Text
Imports GCDRAW = GrapeCity.Documents.Drawing
'' 这个示例与TimeSheet几乎相同,但有一个显着差异:
'' 与其他示例不同,在此示例中,填写的表格由以下人员进行数字签名
'' 员工,并且主管使用签名的 PDF 再次签名
'' 增量更新(在已签名的 PDF 上签名的唯一方法
'' 保留第一个签名的有效性)。
''
'' 注意:如果您下载此示例并在您自己的系统上本地运行它,
'' 您需要拥有有效的许可证才能使其按预期工作,因为
'' 在未经许可的版本中,自动添加的导航页面标题将
'' 使员工签名无效。
Public Class TimeSheetIncremental
'' 字体集合来保存我们需要的字体:
Private _fc As FontCollection = New FontCollection()
'' 展平文档时用于呈现输入字段的文本布局:
Private _inputTl As TextLayout = New TextLayout(72)
'' 用于输入字段的文本格式:
Private _inputTf As TextFormat = New TextFormat()
Private _inputFont As GCTEXT.Font = FontCollection.SystemFonts.FindFamilyName("Segoe UI", True)
Private _inputFontSize As Single = 12
'' 输入字段边距:
Private _inputMargin As Single = 5
'' 员工签名空间:
Private _empSignRect As RectangleF
''
Private _logo As GCDRAW.Image
'' 该示例的主要入口点:
Function CreatePDF(ByVal stream As Stream) As Integer
'' 使用我们需要的字体设置字体集合:
_fc.RegisterDirectory(Path.Combine("Resources", "Fonts"))
'' 在输入字段的文本布局上设置该字体集合
'' (我们还将在我们将使用的所有文本布局上设置它):
_inputTl.FontCollection = _fc
'' 设置输入字段的布局和格式:
_inputTl.ParagraphAlignment = ParagraphAlignment.Center
_inputTf.Font = _inputFont
_inputTf.FontSize = _inputFontSize
'' 创建时间表输入表单
'' (在现实生活中,我们可能只会创建一次,
'' 然后重新使用 PDF 表单):
Dim doc = MakeTimeSheetForm()
'' 此时,“doc”是一个空的 AcroForm。
'' 在现实生活中的应用程序中,它将分发给员工
'' 让他们填写并寄回。
Using empSignedStream = FillEmployeeData(doc)
'' 此时,“empSignedStream”包含填写了员工数据并由他们签名的表单。
'' 加载员工签名的文档:
doc.Load(empSignedStream)
'' 填写主管数据:
Dim supName = "Jane Donahue"
Dim supSignDate = Util.TimeNow().ToShortDateString()
SetFieldValue(doc, _Names.EmpSuper, supName)
SetFieldValue(doc, _Names.SupSignDate, supSignDate)
'' 代表主管对文档进行数字签名:
Dim pfxPath = Path.Combine("Resources", "Misc", "DsPdfTest.pfx")
Dim cert = New X509Certificate2(File.ReadAllBytes(pfxPath), "qq",
X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.Exportable)
'' 连接签名字段和签名属性:
Dim sp = New SignatureProperties() With {
.SignatureBuilder = New Pkcs7SignatureBuilder() With {
.CertificateChain = New X509Certificate2() {cert},
.HashAlgorithm = OID.HashAlgorithms.SHA512
},
.Location = "DsPdfWeb - TimeSheet Incremental",
.SignerName = supName,
.SigningDateTime = Util.TimeNow(),
.SignatureField = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.SupSign), SignatureField)
}
'' 对文档的任何更改都会使员工的签名无效,因此我们不能这样做:
'' supSign.Widget.ButtonAppearance.Caption =supName;
''
'' 完成后,现在保存带有主管签名的文档:
'' 注意:为了不使员工的签名无效,
'' 我们必须在这里使用增量更新(在 Sign() 方法中默认是这样):
doc.Sign(sp, stream)
End Using
_logo.Dispose()
Return doc.Pages.Count
End Function
'' 将文档中的任何文本字段替换为常规文本,
'' 除了“excludeFields”中列出的字段:
Private Sub FlattenDoc(ByVal doc As GcPdfDocument, ParamArray excludeFields As String())
For Each f In doc.AcroForm.Fields
If TypeOf f Is TextField AndAlso Not excludeFields.Contains(f.Name) Then
Dim fld = DirectCast(f, TextField)
Dim w = fld.Widget
Dim 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)
End If
Next
For i = doc.AcroForm.Fields.Count - 1 To 0 Step -1
If TypeOf doc.AcroForm.Fields(i) Is TextField AndAlso Not excludeFields.Contains(doc.AcroForm.Fields(i).Name) Then
doc.AcroForm.Fields.RemoveAt(i)
End If
Next
End Sub
'' 数据字段名称:
Private Structure _Names
Shared ReadOnly Dows As String() = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
}
Const EmpName = "empName"
Const EmpTitle = "empTitle"
Const EmpNum = "empNum"
Const EmpStatus = "empStatus"
Const EmpDep = "empDep"
Const EmpSuper = "empSuper"
Shared ReadOnly DtNames = New Dictionary(Of String, String()) From {
{"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"}}
}
Const TotalReg = "totReg"
Const TotalOvr = "totOvr"
Const TotalHours = "totHours"
Const EmpSign = "empSign"
Const EmpSignDate = "empSignDate"
Const SupSign = "supSign"
Const SupSignDate = "supSignDate"
End Structure
'' 创建考勤表表单:
Private Function MakeTimeSheetForm() As GcPdfDocument
Const marginH = 72.0F, marginV = 48.0F
Dim doc = New GcPdfDocument()
Dim page = doc.NewPage()
Dim g = page.Graphics
Dim ip = New PointF(marginH, marginV)
Dim tl = New TextLayout(g.Resolution) With {.FontCollection = _fc}
tl.Append("TIME SHEET", New TextFormat() With {.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"))
Dim s = New SizeF(250.0F * 0.75F, 64.0F * 0.75F)
g.DrawImage(_logo, New RectangleF(ip, s), Nothing, ImageAlign.Default)
ip.Y += s.Height + 5
tl.Clear()
tl.Append("Where Business meets Technology",
New TextFormat() With {.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,{vbCrLf}Santa Clara, California – 95051-2553,{vbCrLf}United States",
New TextFormat() With {.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
Dim pen = New GCDRAW.Pen(Color.Gray, 0.5F)
Dim colw = (page.Size.Width - marginH * 2) / 2
Dim fields1 = DrawTable(ip,
New Single() {colw, colw},
New Single() {30, 30, 30},
g, pen)
Dim tf = New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}
With tl
.ParagraphAlignment = ParagraphAlignment.Center
.TextAlignment = TextAlignment.Leading
.MarginLeft = 4
.MarginRight = 4
.MarginTop = 4
.MarginBottom = 4
End With
'' t_ - 标题
'' b_ - 边界
'' f_ - 字段名称,null表示没有字段
Dim drawField As Action(Of String, RectangleF, String) =
Sub(t_, b_, f_)
Dim tWidth As Single
If Not String.IsNullOrEmpty(t_) Then
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
End If
If Not String.IsNullOrEmpty(f_) Then
Dim fld = New TextField() With {.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)
End If
End Sub
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
Dim col0 = 100.0F
colw = (page.Size.Width - marginH * 2 - col0) / 5
Dim rowh = 25.0F
Dim fields2 = DrawTable(ip,
New Single() {col0, colw, colw, colw, colw, colw},
New Single() {50, rowh, rowh, rowh, rowh, rowh, rowh, rowh, rowh},
g, pen)
tl.ParagraphAlignment = ParagraphAlignment.Far
drawField("DATE", fields2(0, 0), Nothing)
drawField("START TIME", fields2(1, 0), Nothing)
drawField("END TIME", fields2(2, 0), Nothing)
drawField("REGULAR HOURS", fields2(3, 0), Nothing)
drawField("OVERTIME HOURS", fields2(4, 0), Nothing)
tf.FontBold = True
drawField("TOTAL HOURS", fields2(5, 0), Nothing)
tf.FontBold = False
tl.ParagraphAlignment = ParagraphAlignment.Center
tf.ForeColor = Color.Gray
For i = 0 To 6
drawField(_Names.Dows(i), fields2(0, i + 1), _Names.DtNames(_Names.Dows(i))(0))
Next
'' 垂直对齐日期字段(补偿不同的 DOW 宽度):
Dim dowFields = doc.AcroForm.Fields.TakeLast(7)
Dim minW = dowFields.Min(Function(f_) CType(f_, TextField).Widget.Rect.Width)
dowFields.ToList().ForEach(
Sub(f_)
Dim r_ = CType(f_, TextField).Widget.Rect
r_.Offset(r_.Width - minW, 0)
r_.Width = minW
CType(f_, TextField).Widget.Rect = r_
End Sub
)
tf.ForeColor = Color.Black
For row = 1 To 7
For col = 1 To 5
drawField(Nothing, fields2(col, row), _Names.DtNames(_Names.Dows(row - 1))(col))
Next
Next
tf.FontBold = True
drawField("WEEKLY TOTALS", fields2(0, 8), Nothing)
tf.FontBold = False
drawField(Nothing, fields2(3, 8), _Names.TotalReg)
drawField(Nothing, fields2(4, 8), _Names.TotalOvr)
drawField(Nothing, fields2(5, 8), _Names.TotalHours)
ip.Y = fields2(0, 8).Bottom
col0 = 72 * 4
colw = page.Size.Width - marginH * 2 - col0
Dim fields3 = DrawTable(ip,
New Single() {col0, colw},
New Single() {rowh + 10, rowh, rowh},
g, pen)
drawField("EMPLOYEE SIGNATURE: ", fields3(0, 1), Nothing)
Dim r = fields3(0, 1)
_empSignRect = New RectangleF(r.X + r.Width / 2, r.Y, r.Width / 2 - _inputMargin * 2, r.Height)
Dim sfEmp = New SignatureField() With {.Name = _Names.EmpSign}
sfEmp.Widget.Rect = New RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2)
sfEmp.Widget.Page = page
sfEmp.Widget.BackColor = Color.LightSeaGreen
doc.AcroForm.Fields.Add(sfEmp)
drawField("DATE: ", fields3(1, 1), _Names.EmpSignDate)
drawField("SUPERVISOR SIGNATURE: ", fields3(0, 2), Nothing)
r = fields3(0, 2)
Dim sfSup = New SignatureField() With {.Name = _Names.SupSign}
sfSup.Widget.Rect = New RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2)
sfSup.Widget.Page = page
sfSup.Widget.BackColor = Color.LightYellow
doc.AcroForm.Fields.Add(sfSup)
drawField("DATE: ", fields3(1, 2), _Names.SupSignDate)
'' 完毕:
Return doc
End Function
'' 简单的表格绘制方法。返回表格单元格矩形数组。
Private Function DrawTable(ByVal loc As PointF, ByVal widths As Single(), ByVal heights As Single(), ByVal g As GcGraphics, ByVal p As GCDRAW.Pen) As RectangleF(,)
If widths.Length = 0 OrElse heights.Length = 0 Then
Throw New Exception("Table must have some columns and rows.")
End If
Dim cells(widths.Length, heights.Length) As RectangleF
Dim r = New RectangleF(loc, New SizeF(widths.Sum(), heights.Sum()))
'' 绘制左边框(第一个除外):
Dim x = loc.X
For i = 0 To widths.Length - 1
For j = 0 To heights.Length - 1
cells(i, j).X = x
cells(i, j).Width = widths(i)
Next
If (i > 0) Then
g.DrawLine(x, r.Top, x, r.Bottom, p)
End If
x += widths(i)
Next
'' 绘制顶部边框(第一个除外):
Dim y = loc.Y
For j = 0 To heights.Length - 1
For i = 0 To widths.Length - 1
cells(i, j).Y = y
cells(i, j).Height = heights(j)
Next
If (j > 0) Then
g.DrawLine(r.Left, y, r.Right, y, p)
End If
y += heights(j)
Next
'' 绘制外边框:
g.DrawRectangle(r, p)
'' 完毕:
Return cells
End Function
'' 使用示例数据填写员工信息和工作时间:
Private Function FillEmployeeData(ByVal doc As GcPdfDocument) As Stream
'' 出于本示例的目的,我们使用随机数据填写表格:
Dim 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")
Dim rand = Util.NewRandom()
Dim workday = Util.TimeNow().AddDays(-15)
While workday.DayOfWeek <> DayOfWeek.Sunday
workday = workday.AddDays(1)
End While
Dim wkTot = TimeSpan.Zero, wkReg = TimeSpan.Zero, wkOvr = TimeSpan.Zero
For i = 0 To 6
'' 开始时间:
Dim 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())
'' 时间结束:
Dim endd = start.AddHours(rand.Next(8, 14)).AddMinutes(rand.Next(0, 59))
SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(2), endd.ToShortTimeString())
Dim tot = endd - start
Dim reg = TimeSpan.FromHours(If(start.DayOfWeek <> DayOfWeek.Saturday AndAlso start.DayOfWeek <> DayOfWeek.Sunday, 8, 0))
Dim 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)
Next
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())
'' 代表“员工”对文档进行数字签名:
Dim pfxPath = Path.Combine("Resources", "Misc", "JohnDoe.pfx")
Dim cert = New X509Certificate2(File.ReadAllBytes(pfxPath), "secret",
X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.Exportable)
Dim sp = New SignatureProperties() With {
.SignatureBuilder = New Pkcs7SignatureBuilder() With {
.CertificateChain = New X509Certificate2() {cert}
},
.DocumentAccessPermissions = AccessPermissions.FormFillingAndAnnotations,
.Reason = "I confirm time sheet is correct.",
.Location = "TimeSheetIncremental sample",
.SignerName = empName,
.SigningDateTime = Util.TimeNow()
}
'' 连接签名字段和签名属性:
Dim empSign = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.EmpSign), SignatureField)
sp.SignatureField = empSign
empSign.Widget.ButtonAppearance.Caption = empName
'' 某些浏览器 PDF 查看器不显示表单字段,因此我们渲染一个占位符:
empSign.Widget.Page.Graphics.DrawString("digitally signed", New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}, empSign.Widget.Rect)
'' 我们现在“展平”表单:循环遍历文档 AcroForm 的字段,
'' 将其当前值绘制到位,然后删除字段。
'' 这将生成一个 PDF,其中文本字段的值作为常规的一部分
'' (不可编辑)内容(我们留下由主管填写的字段):
FlattenDoc(doc, _Names.EmpSuper, _Names.SupSignDate)
'' 完成后,现在保存带有员工签名的文档:
Dim ms = New MemoryStream()
'' 请注意,我们在这里不使用增量更新(第三个参数为 false)
'' 因为这还不需要(但稍后主管签名时将需要/使用):
doc.Sign(sp, ms, False)
Return ms
End Function
'' 设置具有指定名称的字段的值:
Private Sub SetFieldValue(ByVal doc As GcPdfDocument, ByVal name As String, ByVal value As String)
Dim fld = doc.AcroForm.Fields.First(Function(f_) f_.Name = name)
If fld IsNot Nothing Then
fld.Value = value
End If
End Sub
End Class