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

'' このサンプルは、TimeSheet とほぼ同じですが、大きな違いが1つあります。
'' 他のサンプルとは異なり、このフォームでは、埋め込まれたフォームは従業員に
'' よってデジタル署名され、署名付き PDF は増分更新を使用してスーパバイザに
'' よって再度署名されます
'' (最初の署名の有効性を保持しながら署名済みの PDF に署名する唯一の方法です)。
'' 
'' 注意:このサンプルをダウンロードし、自分のシステムでローカルに実行する場合は、
'' 有効なライセンスが必要です。ライセンスなしの GcPdf では、ライセンスなしで
'' あることを示すバナーが挿入されることにより、従業員の署名が無効になります。
Public Class TimeSheetIncremental
    '' 必要なフォントを保持するフォントコレクションです。
    Private _fc As FontCollection = New FontCollection()
    '' ドキュメントを平坦化するときに入力フィールドを描画するために使用されるテキストレイアウトです。
    Private _inputTl As TextLayout = New TextLayout(72)
    '' 入力フィールドに使用されるテキスト書式です。
    Private _inputTf As TextFormat = New TextFormat()
    '' 入力フィールドのマージンです。
    Private _inputMargin As Single = 5
    '' 従業員の署名のためのスペースです。
    Private _empSignRect As RectangleF
    ''
    Private _logo As Image


    '' このサンプルのメインエントリポイント。
    Function CreatePDF(ByVal stream As Stream) As Integer
        '' 必要なフォントでフォントコレクションを設定します。
        _fc.RegisterDirectory(Path.Combine("Resources", "Fonts"))
        '' 入力フィールドのテキストレイアウトにそのフォントコレクションを設定します
        '' (使用するすべてのテキストレイアウトでも設定します)。
        _inputTl.FontCollection = _fc
        '' 入力フィールドのレイアウトと書式を設定します。
        _inputTl.ParagraphAlignment = ParagraphAlignment.Center
        _inputTf.FontName = "Segoe UI"
        _inputTf.FontSize = 12
        _inputTf.FontBold = True

        '' タイムシート入力フォームを作成します
        '' (現実のシナリオでは、PDF フォームを一度しか作成せずに、
        '' それを再利用することになります)。
        Dim doc = MakeTimeSheetForm()

        '' この時点で、 'doc' は空の AcroForm です。
        '' 実際のアプリでは、従業員に配布して記入して返送します。
        Using empSignedStream = FillEmployeeData(doc)
            '' この時点で、 'empSignedStream' は従業員のデータで満たされ、署名されたフォームが含まれています。

            '' 従業員が署名したドキュメントを読み込みます。
            doc.Load(empSignedStream)

            '' スーパーバイザーデータを入力します。
            Dim supName = "Jane Donahue"
            Dim supSignDate = DateTime.Now.ToShortDateString()
            SetFieldValue(doc, _Names.EmpSuper, supName)
            SetFieldValue(doc, _Names.SupSignDate, supSignDate)

            '' スーパーバイザーに代わってドキュメントに電子署名します。
            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 {
                .Certificate = cert,
                .Location = "GcPdfWeb - TimeSheet Incremental",
                .SignerName = supName,
                .SignatureDigestAlgorithm = SignatureDigestAlgorithm.SHA512,
                .SignatureField = DirectCast(doc.AcroForm.Fields.First(Function(f_) f_.Name = _Names.SupSign), SignatureField)
            }

            '' ドキュメントを変更すると従業員の署名が無効になるため、以下を行うことはできません。
            '' supSign.Widget.ButtonAppearance.Caption = supName;
            '' 
            '' 終了し、スーパーバイザー署名で文書を保存します。
            '' 注意:従業員の署名を無効にしないために、ここで増分更新を使用しなければなりません
            '' (Sign() メソッドではデフォルトで true です)。
            doc.Sign(sp, stream)
        End Using
        _logo.Dispose()
        Return doc.Pages.Count
    End Function

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

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

        _logo = 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 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.TextFormat = _inputTf
                    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

        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)

        '' 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 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)
        '' PDF ドキュメントを保存します。
        Return cells
    End Function

    '' 従業員情報と勤務時間にサンプルデータを入力します。
    Private Function FillEmployeeData(ByVal doc As GcPdfDocument) As Stream
        '' このサンプルでは、フォームにランダムなデータを入力します。
        Dim empName = "作成者1"
        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 = DateTime.Now.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())


        '' '従業員'に代わってドキュメントに電子署名します。
        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 {
            .Certificate = cert,
            .DocumentAccessPermissions = AccessPermissions.FormFillingAndAnnotations,
            .Reason = "I confirm time sheet is correct.",
            .Location = "TimeSheetIncremental sample",
            .SignerName = empName
        }

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

        '' フォームを「平坦化」します。ドキュメントの AcroForm のフィールドをループし、
        '' 現在の値を所定の位置に描画してからフィールドを削除します。
        '' これにより、テキストフィールドの値を通常の(編集不可能な)コンテンツの一部と
        '' して PDF が生成されます(スーパーバイザが入力したフィールドを残します)。
        FlattenDoc(doc, _Names.EmpSuper, _Names.SupSignDate)

        '' 終了し、従業員の署名でドキュメントを保存します。
        Dim ms = New MemoryStream()
        '' ここでは増分更新を使用しないことに注意してください(3番目のパラメータは false です)。
        '' これはまだ必要ではありません(後でスーパーバイザが署名する際に必要となります)。
        doc.Sign(sp, ms, False)
        Return ms
    End Function

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