'''' This code is part of Document Solutions for PDF .NET demos.'' Copyright (c) MESCIUS inc. All rights reserved.''ImportsSystem.IOImportsSystem.DrawingImportsGrapeCity.Documents.PdfImportsGrapeCity.Documents.Pdf.AcroFormsImportsGrapeCity.Documents.TextImportsGrapeCity.Documents.DrawingImportsGrapeCity.Documents.Pdf.SecurityImportsSystem.Security.Cryptography.X509CertificatesImports GCTEXT = GrapeCity.Documents.TextImports 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.PublicClassTimeSheetIncremental'' Font collection to hold the fonts we need:Private_fcAsFontCollection = NewFontCollection()'' The text layout used to render input fields when flattening the document:Private_inputTlAsTextLayout = NewTextLayout(72)'' The text format used for input fields:Private_inputTfAsTextFormat = NewTextFormat()Private_inputFontAsGCTEXT.Font = StandardFonts.HelveticaBoldPrivate_inputFontSizeAsSingle = 12'' Input fields margin:Private_inputMarginAsSingle = 5'' Space for employee's signature:Private_empSignRectAsRectangleF''Private_logoAsGCDRAW.Image '' Main entry point of this sample:FunctionCreatePDF(ByVal stream AsStream) AsInteger'' 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 dataDim 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 supervisorDim pfxPath = Path.Combine("Resources", "Misc", "DsPdfTest.pfx")Dim cert = NewX509Certificate2(File.ReadAllBytes(pfxPath), "qq",X509KeyStorageFlags.MachineKeySetOrX509KeyStorageFlags.PersistKeySetOrX509KeyStorageFlags.Exportable)'' Connect the signature field And signature propsDim sp = NewSignatureProperties() With { .SignatureBuilder = NewPkcs7SignatureBuilder() With { .CertificateChain = NewX509Certificate2() {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)EndUsing_logo.Dispose()Return doc.Pages.CountEndFunction '' Replaces any text fields in the document with regular text,'' except fields listed in 'excludeFields':PrivateSubFlattenDoc(ByVal doc AsGcPdfDocument, ParamArray excludeFields AsString())ForEach f In doc.AcroForm.FieldsIfTypeOf f IsTextFieldAndAlsoNot excludeFields.Contains(f.Name) ThenDim fld = DirectCast(f, TextField)Dim w = fld.WidgetDim 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)EndIfNextFor i = doc.AcroForm.Fields.Count - 1To0Step -1IfTypeOf doc.AcroForm.Fields(i) IsTextFieldAndAlsoNot excludeFields.Contains(doc.AcroForm.Fields(i).Name) Then doc.AcroForm.Fields.RemoveAt(i)EndIfNextEndSub '' Data field names:PrivateStructure_NamesSharedReadOnlyDowsAsString() = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }ConstEmpName = "empName"ConstEmpTitle = "empTitle"ConstEmpNum = "empNum"ConstEmpStatus = "empStatus"ConstEmpDep = "empDep"ConstEmpSuper = "empSuper"SharedReadOnlyDtNames = New Dictionary(OfString, String()) From { {"Sun", NewString() {"dtSun", "tSunStart", "tSunEnd", "tSunReg", "tSunOvr", "tSunTotal"}}, {"Mon", NewString() {"dtMon", "tMonStart", "tMonEnd", "tMonReg", "tMonOvr", "tMonTotal"}}, {"Tue", NewString() {"dtTue", "tTueStart", "tTueEnd", "tTueReg", "tTueOvr", "tTueTotal"}}, {"Wed", NewString() {"dtWed", "tWedStart", "tWedEnd", "tWedReg", "tWedOvr", "tWedTotal"}}, {"Thu", NewString() {"dtThu", "tThuStart", "tThuEnd", "tThuReg", "tThuOvr", "tThuTotal"}}, {"Fri", NewString() {"dtFri", "tFriStart", "tFriEnd", "tFriReg", "tFriOvr", "tFriTotal"}}, {"Sat", NewString() {"dtSat", "tSatStart", "tSatEnd", "tSatReg", "tSatOvr", "tSatTotal"}} }ConstTotalReg = "totReg"ConstTotalOvr = "totOvr"ConstTotalHours = "totHours"ConstEmpSign = "empSign"ConstEmpSignDate = "empSignDate"ConstSupSign = "supSign"ConstSupSignDate = "supSignDate"EndStructure '' Creates the Time Sheet form:PrivateFunctionMakeTimeSheetForm() AsGcPdfDocument ConstmarginH = 72.0F, marginV = 48.0FDim doc = NewGcPdfDocument()Dim page = doc.NewPage()Dim g = page.GraphicsDim ip = NewPointF(marginH, marginV) Dim tl = NewTextLayout(g.Resolution) With {.FontCollection = _fc} tl.Append("TIME SHEET", NewTextFormat() With {.FontName = "Noto Sans", .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 = NewSizeF(250.0F * 0.75F, 64.0F * 0.75F) g.DrawImage(_logo, NewRectangleF(ip, s), Nothing, ImageAlign.Default) ip.Y += s.Height + 5 tl.Clear() tl.Append("Where Business meets Technology",NewTextFormat() With {.FontName = "Noto Sans", .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",NewTextFormat() With {.FontName = "Noto Sans", .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 = NewGCDRAW.Pen(Color.Gray, 0.5F) Dim colw = (page.Size.Width - marginH * 2) / 2Dim fields1 = DrawTable(ip,NewSingle() {colw, colw},NewSingle() {30, 30, 30}, g, pen) Dim tf = NewTextFormat() With {.FontName = "Noto Sans", .FontSize = 9}With tl .ParagraphAlignment = ParagraphAlignment.Center .TextAlignment = TextAlignment.Leading .MarginLeft = 4 .MarginRight = 4 .MarginTop = 4 .MarginBottom = 4EndWith '' t_ - caption'' b_ - bounds'' f_ - field name, null means no fieldDim drawField As Action(OfString, RectangleF, String) =Sub(t_, b_, f_)Dim tWidth AsSingleIfNotString.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.RightElse tWidth = 0EndIfIfNotString.IsNullOrEmpty(f_) ThenDim fld = NewTextField() With {.Name = f_} fld.Widget.Page = page fld.Widget.Rect = NewRectangleF( 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)EndIfEndSub 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) / 5Dim rowh = 25.0FDim fields2 = DrawTable(ip,NewSingle() {col0, colw, colw, colw, colw, colw},NewSingle() {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.GrayFor i = 0To6 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 = minWCType(f_, TextField).Widget.Rect = r_EndSub ) tf.ForeColor = Color.BlackFor row = 1To7For col = 1To5 drawField(Nothing, fields2(col, row), _Names.DtNames(_Names.Dows(row - 1))(col))NextNext 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 - col0Dim fields3 = DrawTable(ip,NewSingle() {col0, colw},NewSingle() {rowh + 10, rowh, rowh}, g, pen) drawField("EMPLOYEE SIGNATURE: ", fields3(0, 1), Nothing)Dim r = fields3(0, 1)_empSignRect = NewRectangleF(r.X + r.Width / 2, r.Y, r.Width / 2 - _inputMargin * 2, r.Height)Dim sfEmp = NewSignatureField() With {.Name = _Names.EmpSign} sfEmp.Widget.Rect = NewRectangleF(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 = NewSignatureField() With {.Name = _Names.SupSign} sfSup.Widget.Rect = NewRectangleF(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 docEndFunction '' Simple table drawing method. Returns the array of table cell rectangles.PrivateFunctionDrawTable(ByVal loc AsPointF, ByVal widths AsSingle(), ByVal heights AsSingle(), ByVal g AsGcGraphics, ByVal p AsGCDRAW.Pen) AsRectangleF(,)If widths.Length = 0OrElse heights.Length = 0ThenThrowNew Exception("Table must have some columns and rows.")EndIfDim cells(widths.Length, heights.Length) AsRectangleFDim r = NewRectangleF(loc, NewSizeF(widths.Sum(), heights.Sum()))'' Draw left borders (except for 1st one):Dim x = loc.XFor i = 0To widths.Length - 1For j = 0To heights.Length - 1 cells(i, j).X = x cells(i, j).Width = widths(i)NextIf (i > 0) Then g.DrawLine(x, r.Top, x, r.Bottom, p)EndIf x += widths(i)Next'' Draw top borders (except for 1st one):Dim y = loc.YFor j = 0To heights.Length - 1For i = 0To widths.Length - 1 cells(i, j).Y = y cells(i, j).Height = heights(j)NextIf (j > 0) Then g.DrawLine(r.Left, y, r.Right, y, p)EndIf y += heights(j)Next'' Draw outer border: g.DrawRectangle(r, p)'' Done:Return cellsEndFunction '' Fill in employee info and working hours with sample data:PrivateFunctionFillEmployeeData(ByVal doc AsGcPdfDocument) AsStream'' 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)EndWhileDim wkTot = TimeSpan.Zero, wkReg = TimeSpan.Zero, wkOvr = TimeSpan.ZeroFor i = 0To6'' 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 - startDim 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)NextSetFieldValue(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 = NewX509Certificate2(File.ReadAllBytes(pfxPath), "secret",X509KeyStorageFlags.MachineKeySetOrX509KeyStorageFlags.PersistKeySetOrX509KeyStorageFlags.Exportable)Dim sp = NewSignatureProperties() With { .SignatureBuilder = NewPkcs7SignatureBuilder() With { .CertificateChain = NewX509Certificate2() {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", NewTextFormat() With {.FontName = "Noto Sans", .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 = NewMemoryStream()'' 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 msEndFunction '' Sets the value of a field with the specified namePrivateSubSetFieldValue(ByVal doc AsGcPdfDocument, ByVal name AsString, ByVal value AsString)Dim fld = doc.AcroForm.Fields.First(Function(f_) f_.Name = name)If fld IsNotNothingThen fld.Value = valueEndIfEndSubEndClass