TimeSheetIncremental.cs
// 
// このコードは、DioDocs for PDF のサンプルの一部として提供されています。
// © MESCIUS inc. All rights reserved.
// 
using System;
using System.IO;
using System.Linq;
using System.Drawing;
using System.Collections.Generic;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Pdf.AcroForms;
using GrapeCity.Documents.Text;
using GrapeCity.Documents.Common;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Pdf.Security;
using System.Security.Cryptography.X509Certificates;
using GCTEXT = GrapeCity.Documents.Text;
using GCDRAW = GrapeCity.Documents.Drawing;

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

        // このサンプルのメインエントリポイント。
        public int CreatePDF(Stream stream)
        {
            // 必要なフォントでフォントコレクションを設定します。
            _fc.RegisterDirectory(Path.Combine("Resources", "Fonts"));
            // 入力フィールドのテキストレイアウトにそのフォントコレクションを設定します
            // (使用するすべてのテキストレイアウトでも設定します)。
            _inputTl.FontCollection = _fc;
            // 入力フィールドのレイアウトと書式を設定します。
            _inputTl.ParagraphAlignment = ParagraphAlignment.Center;
            _inputTf.Font = _inputFont;
            _inputTf.FontSize = _inputFontSize;

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

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

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

                // スーパーバイザーデータを入力します。
                var supName = "Jane Donahue";
                var supSignDate = Common.Util.TimeNow().ToShortDateString();
                SetFieldValue(doc, _Names.EmpSuper, supName);
                SetFieldValue(doc, _Names.SupSignDate, supSignDate);

                // スーパーバイザーに代わってドキュメントに署名します。
                var pfxPath = Path.Combine("Resources", "Misc", "GcPdfTest.pfx");
                var cert = new X509Certificate2(File.ReadAllBytes(pfxPath), "qq",
                    X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
                var sp = new SignatureProperties()
                {
                    SignatureBuilder = new Pkcs7SignatureBuilder()
                    {
                        CertificateChain = new X509Certificate2[] { cert },
                        HashAlgorithm = OID.HashAlgorithms.SHA512
                    },
                    Location = "DsPdfWeb - TimeSheet Incremental",
                    SignerName = supName,
                    SigningDateTime = Common.Util.TimeNow(),
                    // 署名フィールドと署名プロパティを結びつけます。
                    SignatureField = doc.AcroForm.Fields.First(f_ => f_.Name == _Names.SupSign) as SignatureField,
                };

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

        // 'excludeFields' にリストされているフィールドを除いて、
        // ドキュメント内のテキストフィールドを通常のテキストに置き換えます。
        private void FlattenDoc(GcPdfDocument doc, params string[] excludeFields)
        {
            foreach (var f in doc.AcroForm.Fields)
            {
                if (f is TextField fld && !excludeFields.Contains(fld.Name))
                {
                    var w = fld.Widget;
                    var 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);
                }
            }
            for (int i = doc.AcroForm.Fields.Count - 1; i >= 0; --i)
                if (doc.AcroForm.Fields[i] is TextField fld && !excludeFields.Contains(fld.Name))
                    doc.AcroForm.Fields.RemoveAt(i);
        }

        // データフィールド名です。
        static class _Names
        {
            public static readonly string[] Dows = new string[]
            {
                "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
            };
            public const string EmpName = "empName";
            public const string EmpTitle = "empTitle";
            public const string EmpNum = "empNum";
            public const string EmpStatus = "empStatus";
            public const string EmpDep = "empDep";
            public const string EmpSuper = "empSuper";
            public static Dictionary<string, string[]> DtNames = new Dictionary<string, string[]>()
            {
                {"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" } },
            };
            public const string TotalReg = "totReg";
            public const string TotalOvr = "totOvr";
            public const string TotalHours = "totHours";
            public const string EmpSign = "empSign";
            public const string EmpSignDate = "empSignDate";
            public const string SupSign = "supSign";
            public const string SupSignDate = "supSignDate";
        }

        // タイムシートフォームを作成します。
        private GcPdfDocument MakeTimeSheetForm()
        {
            const float marginH = 72, marginV = 48;
            var doc = new GcPdfDocument();
            var page = doc.NewPage();
            var g = page.Graphics;
            var ip = new PointF(marginH, marginV);

            var tl = new TextLayout(g.Resolution) { FontCollection = _fc };

            tl.Append("TIME SHEET", new TextFormat() { FontName = "Segoe UI", 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"));
            var s = new SizeF(250f * 0.75f, 64f * 0.75f);
            g.DrawImage(_logo, new RectangleF(ip, s), null, ImageAlign.Default);
            ip.Y += s.Height + 5;

            tl.Clear();
            tl.Append("Where Business meets Technology",
                new TextFormat() { 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,\r\nSanta Clara, California – 95051-2553,\r\nUnited States",
                new TextFormat() { 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;

            var pen = new GCDRAW.Pen(Color.Gray, 0.5f);

            var colw = (page.Size.Width - marginH * 2) / 2;
            var fields1 = DrawTable(ip,
                new float[] { colw, colw },
                new float[] { 30, 30, 30 },
                g, pen);

            var tf = new TextFormat() { FontName = "Segoe UI", FontSize = 9 };
            tl.ParagraphAlignment = ParagraphAlignment.Center;
            tl.TextAlignment = TextAlignment.Leading;
            tl.MarginLeft = tl.MarginRight = tl.MarginTop = tl.MarginBottom = 4;

            // t_ - キャプション
            // b_ - 範囲
            // f_ - フィールド名、null はフィールドがないことを意味します
            Action<string, RectangleF, string> drawField = (t_, b_, f_) =>
            {
                float tWidth;
                if (!string.IsNullOrEmpty(t_))
                {
                    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;
                if (!string.IsNullOrEmpty(f_))
                {
                    var fld = new TextField() { 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);
                }
            };

            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;

            float col0 = 100;
            colw = (page.Size.Width - marginH * 2 - col0) / 5;
            float rowh = 25;
            var fields2 = DrawTable(ip,
                new float[] { col0, colw, colw, colw, colw, colw },
                new float[] { 50, rowh, rowh, rowh, rowh, rowh, rowh, rowh, rowh },
                g, pen);

            tl.ParagraphAlignment = ParagraphAlignment.Far;
            drawField("DATE", fields2[0, 0], null);
            drawField("START TIME", fields2[1, 0], null);
            drawField("END TIME", fields2[2, 0], null);
            drawField("REGULAR HOURS", fields2[3, 0], null);
            drawField("OVERTIME HOURS", fields2[4, 0], null);
            tf.FontBold = true;
            drawField("TOTAL HOURS", fields2[5, 0], null);
            tf.FontBold = false;
            tl.ParagraphAlignment = ParagraphAlignment.Center;
            tf.ForeColor = Color.Gray;
            for (int i = 0; i < 7; ++i)
                drawField(_Names.Dows[i], fields2[0, i + 1], _Names.DtNames[_Names.Dows[i]][0]);
            // 日付フィールドを垂直に配置します(異なるDOW幅を補正します)。
            var dowFields = doc.AcroForm.Fields.TakeLast(7);
            var minW = dowFields.Min(f_ => ((TextField)f_).Widget.Rect.Width);
            dowFields.ToList().ForEach(f_ =>
            {
                var r_ = ((TextField)f_).Widget.Rect;
                r_.Offset(r_.Width - minW, 0);
                r_.Width = minW;
                ((TextField)f_).Widget.Rect = r_;
            });

            tf.ForeColor = Color.Black;
            for (int row = 1; row <= 7; ++row)
                for (int col = 1; col <= 5; ++col)
                    drawField(null, fields2[col, row], _Names.DtNames[_Names.Dows[row - 1]][col]);

            tf.FontBold = true;
            drawField("WEEKLY TOTALS", fields2[0, 8], null);
            tf.FontBold = false;

            drawField(null, fields2[3, 8], _Names.TotalReg);
            drawField(null, fields2[4, 8], _Names.TotalOvr);
            drawField(null, fields2[5, 8], _Names.TotalHours);

            ip.Y = fields2[0, 8].Bottom;

            col0 = 72 * 4;
            colw = page.Size.Width - marginH * 2 - col0;
            var fields3 = DrawTable(ip,
                new float[] { col0, colw },
                new float[] { rowh + 10, rowh, rowh },
                g, pen);

            drawField("EMPLOYEE SIGNATURE: ", fields3[0, 1], null);
            // 従業員の署名
            var r = fields3[0, 1];
            _empSignRect = new RectangleF(r.X + r.Width / 2, r.Y, r.Width / 2 - _inputMargin * 2, r.Height);
            var sf = new SignatureField() { Name = _Names.EmpSign };
            sf.Widget.Rect = new RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2);
            sf.Widget.Page = page;
            sf.Widget.BackColor = Color.LightSeaGreen;
            doc.AcroForm.Fields.Add(sf);
            drawField("DATE: ", fields3[1, 1], _Names.EmpSignDate);

            drawField("SUPERVISOR SIGNATURE: ", fields3[0, 2], null);
            // スーパーバイザーの署名
            r = fields3[0, 2];
            sf = new SignatureField() { Name = _Names.SupSign };
            sf.Widget.Rect = new RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2);
            sf.Widget.Page = page;
            sf.Widget.BackColor = Color.LightYellow;
            doc.AcroForm.Fields.Add(sf);
            drawField("DATE: ", fields3[1, 2], _Names.SupSignDate);

            // PDF ドキュメントを保存します。
            return doc;
        }

        // シンプルな表の描画メソッドです。表のセルの矩形の配列を返します。
        private RectangleF[,] DrawTable(PointF loc, float[] widths, float[] heights, GcGraphics g, GCDRAW.Pen p)
        {
            if (widths.Length == 0 || heights.Length == 0)
                throw new Exception("Table must have some columns and rows.");

            RectangleF[,] cells = new RectangleF[widths.Length, heights.Length];

            var r = new RectangleF(loc, new SizeF(widths.Sum(), heights.Sum()));

            // 左の枠線を描画します(1番目を除く)。
            float x = loc.X;
            for (int i = 0; i < widths.Length; ++i)
            {
                for (int j = 0; j < heights.Length; ++j)
                {
                    cells[i, j].X = x;
                    cells[i, j].Width = widths[i];
                }
                if (i > 0)
                    g.DrawLine(x, r.Top, x, r.Bottom, p);
                x += widths[i];
            }
            // 上の枠線を描画します(1番目を除く)。
            float y = loc.Y;
            for (int j = 0; j < heights.Length; ++j)
            {
                for (int i = 0; i < widths.Length; ++i)
                {
                    cells[i, j].Y = y;
                    cells[i, j].Height = heights[j];
                }
                if (j > 0)
                    g.DrawLine(r.Left, y, r.Right, y, p);
                y += heights[j];
            }
            // 外枠を描画します。
            g.DrawRectangle(r, p);
            // PDF ドキュメントを保存します。
            return cells;
        }

        // 従業員情報と勤務時間にサンプルデータを入力します。
        private Stream FillEmployeeData(GcPdfDocument doc)
        {
            // このサンプルでは、フォームにランダムなデータを入力します。
            var 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");
            var rand = new Random((int)Common.Util.TimeNow().Ticks);
            DateTime workday = Common.Util.TimeNow().AddDays(-15);
            while (workday.DayOfWeek != DayOfWeek.Sunday)
                workday = workday.AddDays(1);
            TimeSpan wkTot = TimeSpan.Zero, wkReg = TimeSpan.Zero, wkOvr = TimeSpan.Zero;
            for (int i = 0; i < 7; ++i)
            {
                // 開始時刻。
                var 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());
                // 終了時刻。
                var end = start.AddHours(rand.Next(8, 14)).AddMinutes(rand.Next(0, 59));
                SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][2], end.ToShortTimeString());
                var tot = end - start;
                var reg = TimeSpan.FromHours((start.DayOfWeek != DayOfWeek.Saturday && start.DayOfWeek != DayOfWeek.Sunday) ? 8 : 0);
                var 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);
            }
            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());

            // '従業員'に代わってドキュメントに署名します。
            var pfxPath = Path.Combine("Resources", "Misc", "JohnDoe.pfx");
            var cert = new X509Certificate2(File.ReadAllBytes(pfxPath), "secret",
                X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
            var sp = new SignatureProperties()
            {
                SignatureBuilder = new Pkcs7SignatureBuilder()
                {
                    CertificateChain = new X509Certificate2[] { cert }
                },
                DocumentAccessPermissions = AccessPermissions.FormFillingAndAnnotations,
                Reason = "I confirm time sheet is correct.",
                Location = "TimeSheetIncremental sample",
                SignerName = empName,
                SigningDateTime = Common.Util.TimeNow(),
            };

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

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

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

        // 指定された名前のフィールドの値を設定します。
        private void SetFieldValue(GcPdfDocument doc, string name, string value)
        {
            var fld = doc.AcroForm.Fields.First(f_ => f_.Name == name);
            if (fld != null)
                fld.Value = value;
        }
    }
}