TimeSheet.vb
'' 
'' このコードは、DioDocs for PDF のサンプルの一部として提供されています。
'' © 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

'' このサンプルでは、タイムシートの生成、記入、署名に関するシナリオを実装しています。
'' - 最初のステップは、タイムシートフォーム(AcroForm PDF)を生成することです。
''   このフォームには、従業員情報、1週間の作業時間、従業員とスーパーバイザの署名の
''   フィールドが含まれています。
'' - 実際のアプリにおける次のステップは、従業員が記入してフォームに署名することです。
''   このサンプルでは、従業員にによってフォームを埋める代わりに、ランダムに生成された
''   データを使用します。
'' - 次に、入力されたフォームを平坦化します。従業員が入力したテキストフィールドを通常の
''   テキストに変換します。
'' - 最後に、従業員の上司に代わって平準化された文書にデジタル署名して保存します。
'' 
'' TimeSheetIncremental も参照してください。これは本質的に同じコードですが、増分更新を
'' 使用して、従業員とスーパーバイザの両方で文書にデジタル署名します。
Public Class TimeSheet
    '' 必要なフォントを保持するフォントコレクションです。
    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 _disposables As List(Of IDisposable) = New List(Of IDisposable)

    '' このサンプルのメインエントリポイント。
    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 です。
        '' 実際のアプリでは、従業員に配布して記入して返送します。
        FillEmployeeData(doc)

        '' 
        '' この時点で、フォームは従業員のデータで埋められます。
        '' 
        '' 

        '' スーパバイザデータです(実際のアプリケーションでは、おそらくDBからフェッチされます)。
        Dim supName = "Jane Donahue"
        Dim supSignDate = Util.TimeNow().ToShortDateString()
        SetFieldValue(doc, _Names.EmpSuper, supName)
        SetFieldValue(doc, _Names.SupSignDate, supSignDate)

        '' 次のステップは、フォームを「平坦化」することです。文書の AcroForm の
        '' フィールドをループし、現在の値をその場で描画してからフィールドを削除します。
        '' これにより、テキストフィールドの値を通常の(編集不可能な)コンテンツの
        '' 一部として含む PDF が生成されます。
        FlattenDoc(doc)

        '' これで、「上司」に代わってフラット化されたドキュメントにデジタルで署名します。
        Dim pfxPath = Path.Combine("Resources", "Misc", "GcPdfTest.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()
        }

        '' 署名フィールドと署名プロパティを結びつけます。
        Dim supSign = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.SupSign), SignatureField)
        sp.SignatureField = supSign
        supSign.Widget.ButtonAppearance.Caption = supName
        '' 一部のブラウザの PDF ビューアではフォームフィールドが表示されないため、プレースホルダを描画します。
        supSign.Widget.Page.Graphics.DrawString("digitally signed", New TextFormat() With {.FontName = "Segoe UI", .FontSize = 9}, supSign.Widget.Rect)

        '' 終了し、スーパーバイザの署名でドキュメントを保存します。
        doc.Sign(sp, stream)

        '' ドキュメントを保存した後にのみ画像を廃棄します。
        _disposables.ForEach(Sub(d_) d_.Dispose())
        Return doc.Pages.Count
    End Function

    '' ドキュメント内のテキストフィールドを通常のテキストに置き換えます。
    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

    '' データフィールド名です。
    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

        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_ - キャプション
        '' 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)
#If False Then
        '' デジタル従業員の署名については、このコードのコメントを外してください。
        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)

        '' PDF ドキュメントを保存します。
        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()))
        '' 左の枠線を描画します(1番目を除く)。
        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
        '' 上の枠線を描画します(1番目を除く)。
        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 Sub FillEmployeeData(ByVal doc As GcPdfDocument)
        '' このサンプルでは、フォームにランダムなデータを入力します。
        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
            '' 開始時刻。
            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())

        '' 署名を表わすイメージを描くことによって、従業員に代わってタイムシートに「署名」します。
        '' (従業員とスーパーバイザの両方によるデジタル署名については、TimeSheetIncremental を参照してください)
        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

    '' 指定された名前のフィールドの値を設定します。
    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