TimeSheet.vb
''
'' 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 System.Security.Cryptography.X509Certificates
Imports GCTEXT = GrapeCity.Documents.Text
Imports GCDRAW = GrapeCity.Documents.Drawing

'' This sample implements a scenario involving generating, filling and signing a time sheet:
'' - The first step is to generate a time sheet form (AcroForm PDF).
''   The form contains fields for employee info, working times for a week,
''   and employee's and supervisor's signatures.
'' - The next step in a real app would involve employees filling and signing the form.
''   In this sample, we use some randomly generated data to fill the form on behalf
''   of an employee.
'' - We then flatten the filled form - convert the text fields filled by the employee
''   to regular text.
'' - Finally, we digitally sign the flattened document on behalf of the employee's
''   supervisor, and save it.
''
'' See also TimeSheetIncremental - it is essentially the same code, but uses
'' incremental update to digitally sign the document by both employee and supervisor.
Public Class TimeSheet
    '' 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
    '' This will hold the list of images so we can dispose them after saving the document:
    Private _disposables As List(Of IDisposable) = New List(Of IDisposable)

    '' 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.
        FillEmployeeData(doc)

        ''
        '' At this point the form is filled with employee's data.
        ''

        '' Supervisor data (in a real app, these would probably be fetched from a db):
        Dim supName = "Jane Donahue"
        Dim supSignDate = Util.TimeNow().ToShortDateString()
        SetFieldValue(doc, _Names.EmpSuper, supName)
        SetFieldValue(doc, _Names.SupSignDate, supSignDate)

        '' The next step is to 'flatten' the form: we 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:
        FlattenDoc(doc)

        '' Now we digitally sign the flattened document on behalf of the 'manager':
        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}
            },
            .Location = "DsPdfWeb - TimeSheet sample",
            .SignerName = supName,
            .SigningDateTime = Util.TimeNow()
        }

        '' Connect the signature field and signature props:
        Dim supSign = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.SupSign), SignatureField)
        sp.SignatureField = supSign
        supSign.Widget.ButtonAppearance.Caption = supName
        '' Some browser PDF viewers do not show form fields, so we render a placeholder:
        supSign.Widget.Page.Graphics.DrawString("digitally signed", New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}, supSign.Widget.Rect)

        '' Done, now save the document with supervisor signature:
        doc.Sign(sp, stream)

        '' Dispose images only after the document is saved:
        _disposables.ForEach(Sub(d_) d_.Dispose())
        Return doc.Pages.Count
    End Function

    '' Replaces any text fields in the document with regular text:
    Private Sub FlattenDoc(ByVal doc As GcPdfDocument)
        For Each f In doc.AcroForm.Fields
            If (TypeOf f Is TextField) 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 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

        Dim 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)
#If False Then
        '' For a digital employee signature, uncomment this code:
        Dim sfEmp = New SignatureField()
        sfEmp.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)
#End If
        drawField("DATE: ", fields3(1, 1), _Names.EmpSignDate)

        drawField("SUPERVISOR SIGNATURE: ", fields3(0, 2), Nothing)
        r = fields3(0, 2)
        Dim sfSup = New SignatureField()
        sfSup.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)
        ''
        Return cells
    End Function

    '' Fill in employee info and working hours with sample data:
    Private Sub FillEmployeeData(ByVal doc As GcPdfDocument)
        '' For the purposes of this sample, we fill the form with random data:
        SetFieldValue(doc, _Names.EmpName, "Jaime Smith")
        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())

        '' 'Sign' the time sheet on behalf of the employee by drawing an image representing the signature
        '' (see TimeSheetIncremental for digitally signing by both employee And supervisor)
        Dim empSignImage = GCDRAW.Image.FromFile(Path.Combine("Resources", "ImagesBis", "signature.png"))
        Dim ia = New ImageAlign(ImageAlignHorz.Center, ImageAlignVert.Center, True, True, True, False, False) With {.KeepAspectRatio = True}
        doc.Pages(0).Graphics.DrawImage(empSignImage, _empSignRect, Nothing, ia)
    End Sub

    '' 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