''
'' This code is part of Document Solutions for PDF demos.
'' Copyright (c) MESCIUS inc. All rights reserved.
''
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
'' This sample is almost the same as TimeSheet, with one significant difference:
'' unlike the other sample, in this one the filled form is digitally signed by
'' the employee, and the signed PDF is signed again by the supervisor using
'' incremental update (the only way to sign an already signed PDF while
'' preserving the validity of the first signature).
''
'' NOTE: if you download this sample and run it locally on your own system,
'' you will need to have a valid license for it to work as expected, because
'' in an unlicensed version the automatically added nag page caption will
'' invalidate the employee's signature.
Public Class TimeSheetIncremental
'' Font collection to hold the fonts we need:
Private _fc As FontCollection = New FontCollection()
'' The text layout used to render input fields when flattening the document:
Private _inputTl As TextLayout = New TextLayout(72)
'' The text format used for input fields:
Private _inputTf As TextFormat = New TextFormat()
Private _inputFont As GCTEXT.Font = FontCollection.SystemFonts.FindFamilyName("Segoe UI", True)
Private _inputFontSize As Single = 12
'' Input fields margin:
Private _inputMargin As Single = 5
'' Space for employee's signature:
Private _empSignRect As RectangleF
''
Private _logo As GCDRAW.Image
'' Main entry point of this sample:
Function CreatePDF(ByVal stream As Stream) As Integer
'' Set up a font collection with the fonts we need:
_fc.RegisterDirectory(Path.Combine("Resources", "Fonts"))
'' Set that font collection on input fields' text layout
'' (we will also set it on all text layouts that we'll use):
_inputTl.FontCollection = _fc
'' Set up layout and formatting for input fields:
_inputTl.ParagraphAlignment = ParagraphAlignment.Center
_inputTf.Font = _inputFont
_inputTf.FontSize = _inputFontSize
'' Create the time sheet input form
'' (in a real-life scenario, we probably would only create it once,
'' And then re-use the form PDF):
Dim doc = MakeTimeSheetForm()
'' At this point, 'doc' is an empty AcroForm.
'' In a real-life app it would be distributed to employees
'' for them to fill and send back.
Using empSignedStream = FillEmployeeData(doc)
'' At this point 'empSignedStream' contains the form filled with employee's data and signed by them.
''
'' Load the employee-signed document
doc.Load(empSignedStream)
'' Fill in supervisor data
Dim supName = "Jane Donahue"
Dim supSignDate = Util.TimeNow().ToShortDateString()
SetFieldValue(doc, _Names.EmpSuper, supName)
SetFieldValue(doc, _Names.SupSignDate, supSignDate)
'' Digitally sign the document on behalf of the supervisor
Dim pfxPath = Path.Combine("Resources", "Misc", "DsPdfTest.pfx")
Dim cert = New X509Certificate2(File.ReadAllBytes(pfxPath), "qq",
X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.Exportable)
'' Connect the signature field And signature props
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)
}
'' Any changes to the document would invalidate the employee's signature, so we cannot do this:
'' supSign.Widget.ButtonAppearance.Caption = supName;
''
'' Done, now save the document with supervisor signature
'' NOTE in order to Not invalidate the employee's signature,
'' we MUST use incremental update here (which Is true by default in Sign() method)
doc.Sign(sp, stream)
End Using
_logo.Dispose()
Return doc.Pages.Count
End Function
'' Replaces any text fields in the document with regular text,
'' except fields listed in '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
'' Data field names:
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
'' Creates the Time Sheet form:
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_ - caption
'' b_ - bounds
'' f_ - field name, null means no field
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
'' Vertically align date fields (compensate for different DOW widths):
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)
'' Done:
Return doc
End Function
'' Simple table drawing method. Returns the array of table cell rectangles.
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()))
'' Draw left borders (except for 1st one):
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
'' Draw top borders (except for 1st one):
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
'' Draw outer border:
g.DrawRectangle(r, p)
'' Done:
Return cells
End Function
'' Fill in employee info and working hours with sample data:
Private Function FillEmployeeData(ByVal doc As GcPdfDocument) As Stream
'' For the purposes of this sample, we fill the form with random data:
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
'' Start time:
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())
'' End time:
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())
'' Digitally sign the document on behalf of the 'employee':
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()
}
'' Connect the signature field and signature props:
Dim empSign = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.EmpSign), SignatureField)
sp.SignatureField = empSign
empSign.Widget.ButtonAppearance.Caption = empName
'' Some browser PDF viewers do not show form fields, so we render a placeholder:
empSign.Widget.Page.Graphics.DrawString("digitally signed", New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}, empSign.Widget.Rect)
'' We now 'flatten' the form: loop over document AcroForm's fields,
'' drawing their current values in place, and then remove the fields.
'' This produces a PDF with text fields' values as part of the regular
'' (non-editable) content (we leave fields filled by the supervisor):
FlattenDoc(doc, _Names.EmpSuper, _Names.SupSignDate)
'' Done, now save the document with employee's signature:
Dim ms = New MemoryStream()
'' Note that we do NOT use incremental update here (3rd parameter is false)
'' as this is not needed yet (but will be needed/used when signing by supervisor later)
doc.Sign(sp, ms, False)
Return ms
End Function
'' Sets the value of a field with the specified name
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