TimeSheetIncremental.vb
  1. ''
  2. '' This code is part of Document Solutions for PDF demos.
  3. '' Copyright (c) MESCIUS inc. All rights reserved.
  4. ''
  5. Imports System.IO
  6. Imports System.Drawing
  7. Imports GrapeCity.Documents.Pdf
  8. Imports GrapeCity.Documents.Pdf.AcroForms
  9. Imports GrapeCity.Documents.Text
  10. Imports GrapeCity.Documents.Common
  11. Imports GrapeCity.Documents.Drawing
  12. Imports GrapeCity.Documents.Pdf.Security
  13. Imports System.Security.Cryptography.X509Certificates
  14. Imports GCTEXT = GrapeCity.Documents.Text
  15. Imports GCDRAW = GrapeCity.Documents.Drawing
  16.  
  17. '' This sample is almost the same as TimeSheet, with one significant difference:
  18. '' unlike the other sample, in this one the filled form is digitally signed by
  19. '' the employee, and the signed PDF is signed again by the supervisor using
  20. '' incremental update (the only way to sign an already signed PDF while
  21. '' preserving the validity of the first signature).
  22. ''
  23. '' NOTE: if you download this sample and run it locally on your own system,
  24. '' you will need to have a valid license for it to work as expected, because
  25. '' in an unlicensed version the automatically added nag page caption will
  26. '' invalidate the employee's signature.
  27. Public Class TimeSheetIncremental
  28. '' Font collection to hold the fonts we need:
  29. Private _fc As FontCollection = New FontCollection()
  30. '' The text layout used to render input fields when flattening the document:
  31. Private _inputTl As TextLayout = New TextLayout(72)
  32. '' The text format used for input fields:
  33. Private _inputTf As TextFormat = New TextFormat()
  34. Private _inputFont As GCTEXT.Font = FontCollection.SystemFonts.FindFamilyName("Segoe UI", True)
  35. Private _inputFontSize As Single = 12
  36. '' Input fields margin:
  37. Private _inputMargin As Single = 5
  38. '' Space for employee's signature:
  39. Private _empSignRect As RectangleF
  40. ''
  41. Private _logo As GCDRAW.Image
  42.  
  43.  
  44. '' Main entry point of this sample:
  45. Function CreatePDF(ByVal stream As Stream) As Integer
  46. '' Set up a font collection with the fonts we need:
  47. _fc.RegisterDirectory(Path.Combine("Resources", "Fonts"))
  48. '' Set that font collection on input fields' text layout
  49. '' (we will also set it on all text layouts that we'll use):
  50. _inputTl.FontCollection = _fc
  51. '' Set up layout and formatting for input fields:
  52. _inputTl.ParagraphAlignment = ParagraphAlignment.Center
  53. _inputTf.Font = _inputFont
  54. _inputTf.FontSize = _inputFontSize
  55.  
  56. '' Create the time sheet input form
  57. '' (in a real-life scenario, we probably would only create it once,
  58. '' And then re-use the form PDF):
  59. Dim doc = MakeTimeSheetForm()
  60.  
  61. '' At this point, 'doc' is an empty AcroForm.
  62. '' In a real-life app it would be distributed to employees
  63. '' for them to fill and send back.
  64. Using empSignedStream = FillEmployeeData(doc)
  65. '' At this point 'empSignedStream' contains the form filled with employee's data and signed by them.
  66. ''
  67.  
  68. '' Load the employee-signed document
  69. doc.Load(empSignedStream)
  70.  
  71. '' Fill in supervisor data
  72. Dim supName = "Jane Donahue"
  73. Dim supSignDate = Util.TimeNow().ToShortDateString()
  74. SetFieldValue(doc, _Names.EmpSuper, supName)
  75. SetFieldValue(doc, _Names.SupSignDate, supSignDate)
  76.  
  77. '' Digitally sign the document on behalf of the supervisor
  78. Dim pfxPath = Path.Combine("Resources", "Misc", "DsPdfTest.pfx")
  79. Dim cert = New X509Certificate2(File.ReadAllBytes(pfxPath), "qq",
  80. X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.Exportable)
  81. '' Connect the signature field And signature props
  82. Dim sp = New SignatureProperties() With {
  83. .SignatureBuilder = New Pkcs7SignatureBuilder() With {
  84. .CertificateChain = New X509Certificate2() {cert},
  85. .HashAlgorithm = OID.HashAlgorithms.SHA512
  86. },
  87. .Location = "DsPdfWeb - TimeSheet Incremental",
  88. .SignerName = supName,
  89. .SigningDateTime = Util.TimeNow(),
  90. .SignatureField = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.SupSign), SignatureField)
  91. }
  92.  
  93. '' Any changes to the document would invalidate the employee's signature, so we cannot do this:
  94. '' supSign.Widget.ButtonAppearance.Caption = supName;
  95. ''
  96. '' Done, now save the document with supervisor signature
  97. '' NOTE in order to Not invalidate the employee's signature,
  98. '' we MUST use incremental update here (which Is true by default in Sign() method)
  99. doc.Sign(sp, stream)
  100. End Using
  101. _logo.Dispose()
  102. Return doc.Pages.Count
  103. End Function
  104.  
  105. '' Replaces any text fields in the document with regular text,
  106. '' except fields listed in 'excludeFields':
  107. Private Sub FlattenDoc(ByVal doc As GcPdfDocument, ParamArray excludeFields As String())
  108. For Each f In doc.AcroForm.Fields
  109. If TypeOf f Is TextField AndAlso Not excludeFields.Contains(f.Name) Then
  110. Dim fld = DirectCast(f, TextField)
  111. Dim w = fld.Widget
  112. Dim g = w.Page.Graphics
  113. _inputTl.Clear()
  114. _inputTl.Append(fld.Value, _inputTf)
  115. _inputTl.MaxHeight = w.Rect.Height
  116. _inputTl.PerformLayout(True)
  117. g.DrawTextLayout(_inputTl, w.Rect.Location)
  118. End If
  119. Next
  120. For i = doc.AcroForm.Fields.Count - 1 To 0 Step -1
  121. If TypeOf doc.AcroForm.Fields(i) Is TextField AndAlso Not excludeFields.Contains(doc.AcroForm.Fields(i).Name) Then
  122. doc.AcroForm.Fields.RemoveAt(i)
  123. End If
  124. Next
  125. End Sub
  126.  
  127. '' Data field names:
  128. Private Structure _Names
  129. Shared ReadOnly Dows As String() = {
  130. "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
  131. }
  132. Const EmpName = "empName"
  133. Const EmpTitle = "empTitle"
  134. Const EmpNum = "empNum"
  135. Const EmpStatus = "empStatus"
  136. Const EmpDep = "empDep"
  137. Const EmpSuper = "empSuper"
  138. Shared ReadOnly DtNames = New Dictionary(Of String, String()) From {
  139. {"Sun", New String() {"dtSun", "tSunStart", "tSunEnd", "tSunReg", "tSunOvr", "tSunTotal"}},
  140. {"Mon", New String() {"dtMon", "tMonStart", "tMonEnd", "tMonReg", "tMonOvr", "tMonTotal"}},
  141. {"Tue", New String() {"dtTue", "tTueStart", "tTueEnd", "tTueReg", "tTueOvr", "tTueTotal"}},
  142. {"Wed", New String() {"dtWed", "tWedStart", "tWedEnd", "tWedReg", "tWedOvr", "tWedTotal"}},
  143. {"Thu", New String() {"dtThu", "tThuStart", "tThuEnd", "tThuReg", "tThuOvr", "tThuTotal"}},
  144. {"Fri", New String() {"dtFri", "tFriStart", "tFriEnd", "tFriReg", "tFriOvr", "tFriTotal"}},
  145. {"Sat", New String() {"dtSat", "tSatStart", "tSatEnd", "tSatReg", "tSatOvr", "tSatTotal"}}
  146. }
  147. Const TotalReg = "totReg"
  148. Const TotalOvr = "totOvr"
  149. Const TotalHours = "totHours"
  150. Const EmpSign = "empSign"
  151. Const EmpSignDate = "empSignDate"
  152. Const SupSign = "supSign"
  153. Const SupSignDate = "supSignDate"
  154. End Structure
  155.  
  156. '' Creates the Time Sheet form:
  157. Private Function MakeTimeSheetForm() As GcPdfDocument
  158.  
  159. Const marginH = 72.0F, marginV = 48.0F
  160. Dim doc = New GcPdfDocument()
  161. Dim page = doc.NewPage()
  162. Dim g = page.Graphics
  163. Dim ip = New PointF(marginH, marginV)
  164.  
  165. Dim tl = New TextLayout(g.Resolution) With {.FontCollection = _fc}
  166.  
  167. tl.Append("TIME SHEET", New TextFormat() With {.FontName = "Segoe UI", .FontSize = 18})
  168. tl.PerformLayout(True)
  169. g.DrawTextLayout(tl, ip)
  170. ip.Y += tl.ContentHeight + 15
  171.  
  172. _logo = GCDRAW.Image.FromFile(Path.Combine("Resources", "ImagesBis", "AcmeLogo-vertical-250px.png"))
  173. Dim s = New SizeF(250.0F * 0.75F, 64.0F * 0.75F)
  174. g.DrawImage(_logo, New RectangleF(ip, s), Nothing, ImageAlign.Default)
  175. ip.Y += s.Height + 5
  176.  
  177. tl.Clear()
  178. tl.Append("Where Business meets Technology",
  179. New TextFormat() With {.FontName = "Segoe UI", .FontItalic = True, .FontSize = 10})
  180. tl.PerformLayout(True)
  181. g.DrawTextLayout(tl, ip)
  182. ip.Y += tl.ContentHeight + 15
  183.  
  184. tl.Clear()
  185. tl.Append($"1901, Halford Avenue,{vbCrLf}Santa Clara, California – 95051-2553,{vbCrLf}United States",
  186. New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9})
  187. tl.MaxWidth = page.Size.Width - marginH * 2
  188. tl.TextAlignment = TextAlignment.Trailing
  189. tl.PerformLayout(True)
  190. g.DrawTextLayout(tl, ip)
  191. ip.Y += tl.ContentHeight + 25
  192.  
  193. Dim pen = New GCDRAW.Pen(Color.Gray, 0.5F)
  194.  
  195. Dim colw = (page.Size.Width - marginH * 2) / 2
  196. Dim fields1 = DrawTable(ip,
  197. New Single() {colw, colw},
  198. New Single() {30, 30, 30},
  199. g, pen)
  200.  
  201. Dim tf = New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}
  202. With tl
  203. .ParagraphAlignment = ParagraphAlignment.Center
  204. .TextAlignment = TextAlignment.Leading
  205. .MarginLeft = 4
  206. .MarginRight = 4
  207. .MarginTop = 4
  208. .MarginBottom = 4
  209. End With
  210.  
  211. '' t_ - caption
  212. '' b_ - bounds
  213. '' f_ - field name, null means no field
  214. Dim drawField As Action(Of String, RectangleF, String) =
  215. Sub(t_, b_, f_)
  216. Dim tWidth As Single
  217. If Not String.IsNullOrEmpty(t_) Then
  218. tl.Clear()
  219. tl.MaxHeight = b_.Height
  220. tl.MaxWidth = b_.Width
  221. tl.Append(t_, tf)
  222. tl.PerformLayout(True)
  223. g.DrawTextLayout(tl, b_.Location)
  224. tWidth = tl.ContentRectangle.Right
  225. Else
  226. tWidth = 0
  227. End If
  228. If Not String.IsNullOrEmpty(f_) Then
  229. Dim fld = New TextField() With {.Name = f_}
  230. fld.Widget.Page = page
  231. fld.Widget.Rect = New RectangleF(
  232. b_.X + tWidth + _inputMargin, b_.Y + _inputMargin,
  233. b_.Width - tWidth - _inputMargin * 2, b_.Height - _inputMargin * 2)
  234. fld.Widget.DefaultAppearance.Font = _inputFont
  235. fld.Widget.DefaultAppearance.FontSize = _inputFontSize
  236. fld.Widget.Border.Color = Color.LightSlateGray
  237. fld.Widget.Border.Width = 0.5F
  238. doc.AcroForm.Fields.Add(fld)
  239. End If
  240. End Sub
  241.  
  242. drawField("EMPLOYEE NAME: ", fields1(0, 0), _Names.EmpName)
  243. drawField("TITLE: ", fields1(1, 0), _Names.EmpTitle)
  244. drawField("EMPLOYEE NUMBER: ", fields1(0, 1), _Names.EmpNum)
  245. drawField("STATUS: ", fields1(1, 1), _Names.EmpStatus)
  246. drawField("DEPARTMENT: ", fields1(0, 2), _Names.EmpDep)
  247. drawField("SUPERVISOR: ", fields1(1, 2), _Names.EmpSuper)
  248.  
  249. ip.Y = fields1(0, 2).Bottom
  250.  
  251. Dim col0 = 100.0F
  252. colw = (page.Size.Width - marginH * 2 - col0) / 5
  253. Dim rowh = 25.0F
  254. Dim fields2 = DrawTable(ip,
  255. New Single() {col0, colw, colw, colw, colw, colw},
  256. New Single() {50, rowh, rowh, rowh, rowh, rowh, rowh, rowh, rowh},
  257. g, pen)
  258.  
  259. tl.ParagraphAlignment = ParagraphAlignment.Far
  260. drawField("DATE", fields2(0, 0), Nothing)
  261. drawField("START TIME", fields2(1, 0), Nothing)
  262. drawField("END TIME", fields2(2, 0), Nothing)
  263. drawField("REGULAR HOURS", fields2(3, 0), Nothing)
  264. drawField("OVERTIME HOURS", fields2(4, 0), Nothing)
  265. tf.FontBold = True
  266. drawField("TOTAL HOURS", fields2(5, 0), Nothing)
  267. tf.FontBold = False
  268. tl.ParagraphAlignment = ParagraphAlignment.Center
  269. tf.ForeColor = Color.Gray
  270. For i = 0 To 6
  271. drawField(_Names.Dows(i), fields2(0, i + 1), _Names.DtNames(_Names.Dows(i))(0))
  272. Next
  273. '' Vertically align date fields (compensate for different DOW widths):
  274. Dim dowFields = doc.AcroForm.Fields.TakeLast(7)
  275. Dim minW = dowFields.Min(Function(f_) CType(f_, TextField).Widget.Rect.Width)
  276. dowFields.ToList().ForEach(
  277. Sub(f_)
  278. Dim r_ = CType(f_, TextField).Widget.Rect
  279. r_.Offset(r_.Width - minW, 0)
  280. r_.Width = minW
  281. CType(f_, TextField).Widget.Rect = r_
  282. End Sub
  283. )
  284.  
  285. tf.ForeColor = Color.Black
  286. For row = 1 To 7
  287. For col = 1 To 5
  288. drawField(Nothing, fields2(col, row), _Names.DtNames(_Names.Dows(row - 1))(col))
  289. Next
  290. Next
  291.  
  292. tf.FontBold = True
  293. drawField("WEEKLY TOTALS", fields2(0, 8), Nothing)
  294. tf.FontBold = False
  295.  
  296. drawField(Nothing, fields2(3, 8), _Names.TotalReg)
  297. drawField(Nothing, fields2(4, 8), _Names.TotalOvr)
  298. drawField(Nothing, fields2(5, 8), _Names.TotalHours)
  299.  
  300. ip.Y = fields2(0, 8).Bottom
  301.  
  302. col0 = 72 * 4
  303. colw = page.Size.Width - marginH * 2 - col0
  304. Dim fields3 = DrawTable(ip,
  305. New Single() {col0, colw},
  306. New Single() {rowh + 10, rowh, rowh},
  307. g, pen)
  308.  
  309. drawField("EMPLOYEE SIGNATURE: ", fields3(0, 1), Nothing)
  310. Dim r = fields3(0, 1)
  311. _empSignRect = New RectangleF(r.X + r.Width / 2, r.Y, r.Width / 2 - _inputMargin * 2, r.Height)
  312. Dim sfEmp = New SignatureField() With {.Name = _Names.EmpSign}
  313. sfEmp.Widget.Rect = New RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2)
  314. sfEmp.Widget.Page = page
  315. sfEmp.Widget.BackColor = Color.LightSeaGreen
  316. doc.AcroForm.Fields.Add(sfEmp)
  317. drawField("DATE: ", fields3(1, 1), _Names.EmpSignDate)
  318.  
  319. drawField("SUPERVISOR SIGNATURE: ", fields3(0, 2), Nothing)
  320. r = fields3(0, 2)
  321. Dim sfSup = New SignatureField() With {.Name = _Names.SupSign}
  322. sfSup.Widget.Rect = New RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2)
  323. sfSup.Widget.Page = page
  324. sfSup.Widget.BackColor = Color.LightYellow
  325. doc.AcroForm.Fields.Add(sfSup)
  326. drawField("DATE: ", fields3(1, 2), _Names.SupSignDate)
  327.  
  328. '' Done:
  329. Return doc
  330. End Function
  331.  
  332. '' Simple table drawing method. Returns the array of table cell rectangles.
  333. 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(,)
  334. If widths.Length = 0 OrElse heights.Length = 0 Then
  335. Throw New Exception("Table must have some columns and rows.")
  336. End If
  337. Dim cells(widths.Length, heights.Length) As RectangleF
  338. Dim r = New RectangleF(loc, New SizeF(widths.Sum(), heights.Sum()))
  339. '' Draw left borders (except for 1st one):
  340. Dim x = loc.X
  341. For i = 0 To widths.Length - 1
  342. For j = 0 To heights.Length - 1
  343. cells(i, j).X = x
  344. cells(i, j).Width = widths(i)
  345. Next
  346. If (i > 0) Then
  347. g.DrawLine(x, r.Top, x, r.Bottom, p)
  348. End If
  349. x += widths(i)
  350. Next
  351. '' Draw top borders (except for 1st one):
  352. Dim y = loc.Y
  353. For j = 0 To heights.Length - 1
  354. For i = 0 To widths.Length - 1
  355. cells(i, j).Y = y
  356. cells(i, j).Height = heights(j)
  357. Next
  358. If (j > 0) Then
  359. g.DrawLine(r.Left, y, r.Right, y, p)
  360. End If
  361. y += heights(j)
  362. Next
  363. '' Draw outer border:
  364. g.DrawRectangle(r, p)
  365. '' Done:
  366. Return cells
  367. End Function
  368.  
  369. '' Fill in employee info and working hours with sample data:
  370. Private Function FillEmployeeData(ByVal doc As GcPdfDocument) As Stream
  371. '' For the purposes of this sample, we fill the form with random data:
  372. Dim empName = "Jaime Smith"
  373. SetFieldValue(doc, _Names.EmpName, empName)
  374. SetFieldValue(doc, _Names.EmpNum, "12345")
  375. SetFieldValue(doc, _Names.EmpDep, "Research & Development")
  376. SetFieldValue(doc, _Names.EmpTitle, "Senior Developer")
  377. SetFieldValue(doc, _Names.EmpStatus, "Full Time")
  378. Dim rand = Util.NewRandom()
  379. Dim workday = Util.TimeNow().AddDays(-15)
  380. While workday.DayOfWeek <> DayOfWeek.Sunday
  381. workday = workday.AddDays(1)
  382. End While
  383. Dim wkTot = TimeSpan.Zero, wkReg = TimeSpan.Zero, wkOvr = TimeSpan.Zero
  384. For i = 0 To 6
  385. '' Start time:
  386. Dim start = New DateTime(workday.Year, workday.Month, workday.Day, rand.Next(6, 12), rand.Next(0, 59), 0)
  387. SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(0), start.ToShortDateString())
  388. SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(1), start.ToShortTimeString())
  389. '' End time:
  390. Dim endd = start.AddHours(rand.Next(8, 14)).AddMinutes(rand.Next(0, 59))
  391. SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(2), endd.ToShortTimeString())
  392. Dim tot = endd - start
  393. Dim reg = TimeSpan.FromHours(If(start.DayOfWeek <> DayOfWeek.Saturday AndAlso start.DayOfWeek <> DayOfWeek.Sunday, 8, 0))
  394. Dim ovr = tot.Subtract(reg)
  395. SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(3), reg.ToString("hh\:mm"))
  396. SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(4), ovr.ToString("hh\:mm"))
  397. SetFieldValue(doc, _Names.DtNames(_Names.Dows(i))(5), tot.ToString("hh\:mm"))
  398. wkTot += tot
  399. wkOvr += ovr
  400. wkReg += reg
  401. ''
  402. workday = workday.AddDays(1)
  403. Next
  404. SetFieldValue(doc, _Names.TotalReg, wkReg.TotalHours.ToString("F"))
  405. SetFieldValue(doc, _Names.TotalOvr, wkOvr.TotalHours.ToString("F"))
  406. SetFieldValue(doc, _Names.TotalHours, wkTot.TotalHours.ToString("F"))
  407. SetFieldValue(doc, _Names.EmpSignDate, workday.ToShortDateString())
  408.  
  409.  
  410. '' Digitally sign the document on behalf of the 'employee':
  411. Dim pfxPath = Path.Combine("Resources", "Misc", "JohnDoe.pfx")
  412. Dim cert = New X509Certificate2(File.ReadAllBytes(pfxPath), "secret",
  413. X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.Exportable)
  414. Dim sp = New SignatureProperties() With {
  415. .SignatureBuilder = New Pkcs7SignatureBuilder() With {
  416. .CertificateChain = New X509Certificate2() {cert}
  417. },
  418. .DocumentAccessPermissions = AccessPermissions.FormFillingAndAnnotations,
  419. .Reason = "I confirm time sheet is correct.",
  420. .Location = "TimeSheetIncremental sample",
  421. .SignerName = empName,
  422. .SigningDateTime = Util.TimeNow()
  423. }
  424.  
  425. '' Connect the signature field and signature props:
  426. Dim empSign = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.EmpSign), SignatureField)
  427. sp.SignatureField = empSign
  428. empSign.Widget.ButtonAppearance.Caption = empName
  429. '' Some browser PDF viewers do not show form fields, so we render a placeholder:
  430. empSign.Widget.Page.Graphics.DrawString("digitally signed", New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}, empSign.Widget.Rect)
  431.  
  432. '' We now 'flatten' the form: loop over document AcroForm's fields,
  433. '' drawing their current values in place, and then remove the fields.
  434. '' This produces a PDF with text fields' values as part of the regular
  435. '' (non-editable) content (we leave fields filled by the supervisor):
  436. FlattenDoc(doc, _Names.EmpSuper, _Names.SupSignDate)
  437.  
  438. '' Done, now save the document with employee's signature:
  439. Dim ms = New MemoryStream()
  440. '' Note that we do NOT use incremental update here (3rd parameter is false)
  441. '' as this is not needed yet (but will be needed/used when signing by supervisor later)
  442. doc.Sign(sp, ms, False)
  443. Return ms
  444. End Function
  445.  
  446. '' Sets the value of a field with the specified name
  447. Private Sub SetFieldValue(ByVal doc As GcPdfDocument, ByVal name As String, ByVal value As String)
  448. Dim fld = doc.AcroForm.Fields.First(Function(f_) f_.Name = name)
  449. If fld IsNot Nothing Then
  450. fld.Value = value
  451. End If
  452. End Sub
  453. End Class
  454.