TimeSheetIncremental.cs
  1. //
  2. // This code is part of Document Solutions for PDF demos.
  3. // Copyright (c) MESCIUS inc. All rights reserved.
  4. //
  5. using System;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Drawing;
  9. using System.Collections.Generic;
  10. using GrapeCity.Documents.Pdf;
  11. using GrapeCity.Documents.Pdf.AcroForms;
  12. using GrapeCity.Documents.Text;
  13. using GrapeCity.Documents.Common;
  14. using GrapeCity.Documents.Drawing;
  15. using GrapeCity.Documents.Pdf.Security;
  16. using System.Security.Cryptography.X509Certificates;
  17. using GCTEXT = GrapeCity.Documents.Text;
  18. using GCDRAW = GrapeCity.Documents.Drawing;
  19.  
  20. namespace DsPdfWeb.Demos
  21. {
  22. // This sample is almost the same as TimeSheet, with one significant difference:
  23. // unlike the other sample, in this one the filled form is digitally signed by
  24. // the employee, and the signed PDF is signed again by the supervisor using
  25. // incremental update (the only way to sign an already signed PDF while
  26. // preserving the validity of the first signature).
  27. //
  28. // NOTE: if you download this sample and run it locally on your own system,
  29. // you will need to have a valid license for it to work as expected, because
  30. // in an unlicensed version the automatically added nag page caption will
  31. // invalidate the employee's signature.
  32. public class TimeSheetIncremental
  33. {
  34. // Font collection to hold the fonts we need:
  35. private FontCollection _fc = new FontCollection();
  36. // The text layout used to render input fields when flattening the document:
  37. private TextLayout _inputTl = new TextLayout(72);
  38. // The text format used for input fields:
  39. private TextFormat _inputTf = new TextFormat();
  40. private GCTEXT.Font _inputFont = FontCollection.SystemFonts.FindFamilyName("Segoe UI", true);
  41. private float _inputFontSize = 12;
  42. // Input fields margin:
  43. private float _inputMargin = 5;
  44. // Space for employee's signature:
  45. private RectangleF _empSignRect;
  46. //
  47. private GCDRAW.Image _logo;
  48.  
  49. // Main entry point of this sample:
  50. public int CreatePDF(Stream stream)
  51. {
  52. // Set up a font collection with the fonts we need:
  53. _fc.RegisterDirectory(Path.Combine("Resources", "Fonts"));
  54. // Set that font collection on input fields' text layout
  55. // (we will also set it on all text layouts that we'll use):
  56. _inputTl.FontCollection = _fc;
  57. // Set up layout and formatting for input fields:
  58. _inputTl.ParagraphAlignment = ParagraphAlignment.Center;
  59. _inputTf.Font = _inputFont;
  60. _inputTf.FontSize = _inputFontSize;
  61.  
  62. // Create the time sheet input form
  63. // (in a real-life scenario, we probably would only create it once,
  64. // and then re-use the form PDF):
  65. var doc = MakeTimeSheetForm();
  66.  
  67. // At this point, 'doc' is an empty AcroForm.
  68. // In a real-life app it would be distributed to employees
  69. // for them to fill and send back.
  70. using (var empSignedStream = FillEmployeeData(doc))
  71. {
  72. //
  73. // At this point 'empSignedStream' contains the form filled with employee's data and signed by them.
  74. //
  75.  
  76. // Load the employee-signed document:
  77. doc.Load(empSignedStream);
  78.  
  79. // Fill in supervisor data:
  80. var supName = "Jane Donahue";
  81. var supSignDate = Common.Util.TimeNow().ToShortDateString();
  82. SetFieldValue(doc, _Names.EmpSuper, supName);
  83. SetFieldValue(doc, _Names.SupSignDate, supSignDate);
  84.  
  85. // Digitally sign the document on behalf of the supervisor:
  86. var pfxPath = Path.Combine("Resources", "Misc", "DsPdfTest.pfx");
  87. var cert = new X509Certificate2(File.ReadAllBytes(pfxPath), "qq",
  88. X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
  89. var sp = new SignatureProperties()
  90. {
  91. SignatureBuilder = new Pkcs7SignatureBuilder()
  92. {
  93. CertificateChain = new X509Certificate2[] { cert },
  94. HashAlgorithm = OID.HashAlgorithms.SHA512
  95. },
  96. Location = "DsPdfWeb - TimeSheet Incremental",
  97. SignerName = supName,
  98. SigningDateTime = Common.Util.TimeNow(),
  99. // Connect the signature field and signature props:
  100. SignatureField = doc.AcroForm.Fields.First(f_ => f_.Name == _Names.SupSign) as SignatureField,
  101. };
  102.  
  103. // Any changes to the document would invalidate the employee's signature, so we cannot do this:
  104. // supSign.Widget.ButtonAppearance.Caption = supName;
  105. //
  106. // Done, now save the document with supervisor signature:
  107. // NOTE: in order to not invalidate the employee's signature,
  108. // we MUST use incremental update here (which is true by default in Sign() method):
  109. doc.Sign(sp, stream);
  110. _logo.Dispose();
  111. return doc.Pages.Count;
  112. }
  113. }
  114.  
  115. // Replaces any text fields in the document with regular text,
  116. // except the fields listed in 'excludeFields':
  117. private void FlattenDoc(GcPdfDocument doc, params string[] excludeFields)
  118. {
  119. foreach (var f in doc.AcroForm.Fields)
  120. {
  121. if (f is TextField fld && !excludeFields.Contains(fld.Name))
  122. {
  123. var w = fld.Widget;
  124. var g = w.Page.Graphics;
  125. _inputTl.Clear();
  126. _inputTl.Append(fld.Value, _inputTf);
  127. _inputTl.MaxHeight = w.Rect.Height;
  128. _inputTl.PerformLayout(true);
  129. g.DrawTextLayout(_inputTl, w.Rect.Location);
  130. }
  131. }
  132. for (int i = doc.AcroForm.Fields.Count - 1; i >= 0; --i)
  133. if (doc.AcroForm.Fields[i] is TextField fld && !excludeFields.Contains(fld.Name))
  134. doc.AcroForm.Fields.RemoveAt(i);
  135. }
  136.  
  137. // Data field names:
  138. static class _Names
  139. {
  140. public static readonly string[] Dows = new string[]
  141. {
  142. "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
  143. };
  144. public const string EmpName = "empName";
  145. public const string EmpTitle = "empTitle";
  146. public const string EmpNum = "empNum";
  147. public const string EmpStatus = "empStatus";
  148. public const string EmpDep = "empDep";
  149. public const string EmpSuper = "empSuper";
  150. public static Dictionary<string, string[]> DtNames = new Dictionary<string, string[]>()
  151. {
  152. {"Sun", new string[] { "dtSun", "tSunStart", "tSunEnd", "tSunReg", "tSunOvr", "tSunTotal" } },
  153. {"Mon", new string[] { "dtMon", "tMonStart", "tMonEnd", "tMonReg", "tMonOvr", "tMonTotal" } },
  154. {"Tue", new string[] { "dtTue", "tTueStart", "tTueEnd", "tTueReg", "tTueOvr", "tTueTotal" } },
  155. {"Wed", new string[] { "dtWed", "tWedStart", "tWedEnd", "tWedReg", "tWedOvr", "tWedTotal" } },
  156. {"Thu", new string[] { "dtThu", "tThuStart", "tThuEnd", "tThuReg", "tThuOvr", "tThuTotal" } },
  157. {"Fri", new string[] { "dtFri", "tFriStart", "tFriEnd", "tFriReg", "tFriOvr", "tFriTotal" } },
  158. {"Sat", new string[] { "dtSat", "tSatStart", "tSatEnd", "tSatReg", "tSatOvr", "tSatTotal" } },
  159. };
  160. public const string TotalReg = "totReg";
  161. public const string TotalOvr = "totOvr";
  162. public const string TotalHours = "totHours";
  163. public const string EmpSign = "empSign";
  164. public const string EmpSignDate = "empSignDate";
  165. public const string SupSign = "supSign";
  166. public const string SupSignDate = "supSignDate";
  167. }
  168.  
  169. // Creates the Time Sheet form:
  170. private GcPdfDocument MakeTimeSheetForm()
  171. {
  172. const float marginH = 72, marginV = 48;
  173. var doc = new GcPdfDocument();
  174. var page = doc.NewPage();
  175. var g = page.Graphics;
  176. var ip = new PointF(marginH, marginV);
  177.  
  178. var tl = new TextLayout(g.Resolution) { FontCollection = _fc };
  179.  
  180. tl.Append("TIME SHEET", new TextFormat() { FontName = "Segoe UI", FontSize = 18 });
  181. tl.PerformLayout(true);
  182. g.DrawTextLayout(tl, ip);
  183. ip.Y += tl.ContentHeight + 15;
  184.  
  185. _logo = GCDRAW.Image.FromFile(Path.Combine("Resources", "ImagesBis", "AcmeLogo-vertical-250px.png"));
  186. var s = new SizeF(250f * 0.75f, 64f * 0.75f);
  187. g.DrawImage(_logo, new RectangleF(ip, s), null, ImageAlign.Default);
  188. ip.Y += s.Height + 5;
  189.  
  190. tl.Clear();
  191. tl.Append("Where Business meets Technology",
  192. new TextFormat() { FontName = "Segoe UI", FontItalic = true, FontSize = 10 });
  193. tl.PerformLayout(true);
  194. g.DrawTextLayout(tl, ip);
  195. ip.Y += tl.ContentHeight + 15;
  196.  
  197. tl.Clear();
  198. tl.Append("1901, Halford Avenue,\r\nSanta Clara, California – 95051-2553,\r\nUnited States",
  199. new TextFormat() { FontName = "Segoe UI", FontSize = 9 });
  200. tl.MaxWidth = page.Size.Width - marginH * 2;
  201. tl.TextAlignment = TextAlignment.Trailing;
  202. tl.PerformLayout(true);
  203. g.DrawTextLayout(tl, ip);
  204. ip.Y += tl.ContentHeight + 25;
  205.  
  206. var pen = new GCDRAW.Pen(Color.Gray, 0.5f);
  207.  
  208. var colw = (page.Size.Width - marginH * 2) / 2;
  209. var fields1 = DrawTable(ip,
  210. new float[] { colw, colw },
  211. new float[] { 30, 30, 30 },
  212. g, pen);
  213.  
  214. var tf = new TextFormat() { FontName = "Segoe UI", FontSize = 9 };
  215. tl.ParagraphAlignment = ParagraphAlignment.Center;
  216. tl.TextAlignment = TextAlignment.Leading;
  217. tl.MarginLeft = tl.MarginRight = tl.MarginTop = tl.MarginBottom = 4;
  218.  
  219. // t_ - caption
  220. // b_ - bounds
  221. // f_ - field name, null means no field
  222. Action<string, RectangleF, string> drawField = (t_, b_, f_) =>
  223. {
  224. float tWidth;
  225. if (!string.IsNullOrEmpty(t_))
  226. {
  227. tl.Clear();
  228. tl.MaxHeight = b_.Height;
  229. tl.MaxWidth = b_.Width;
  230. tl.Append(t_, tf);
  231. tl.PerformLayout(true);
  232. g.DrawTextLayout(tl, b_.Location);
  233. tWidth = tl.ContentRectangle.Right;
  234. }
  235. else
  236. tWidth = 0;
  237. if (!string.IsNullOrEmpty(f_))
  238. {
  239. var fld = new TextField() { Name = f_ };
  240. fld.Widget.Page = page;
  241. fld.Widget.Rect = new RectangleF(
  242. b_.X + tWidth + _inputMargin, b_.Y + _inputMargin,
  243. b_.Width - tWidth - _inputMargin * 2, b_.Height - _inputMargin * 2);
  244. fld.Widget.DefaultAppearance.Font = _inputFont;
  245. fld.Widget.DefaultAppearance.FontSize = _inputFontSize;
  246. fld.Widget.Border.Color = Color.LightSlateGray;
  247. fld.Widget.Border.Width = 0.5f;
  248. doc.AcroForm.Fields.Add(fld);
  249. }
  250. };
  251.  
  252. drawField("EMPLOYEE NAME: ", fields1[0, 0], _Names.EmpName);
  253. drawField("TITLE: ", fields1[1, 0], _Names.EmpTitle);
  254. drawField("EMPLOYEE NUMBER: ", fields1[0, 1], _Names.EmpNum);
  255. drawField("STATUS: ", fields1[1, 1], _Names.EmpStatus);
  256. drawField("DEPARTMENT: ", fields1[0, 2], _Names.EmpDep);
  257. drawField("SUPERVISOR: ", fields1[1, 2], _Names.EmpSuper);
  258.  
  259. ip.Y = fields1[0, 2].Bottom;
  260.  
  261. float col0 = 100;
  262. colw = (page.Size.Width - marginH * 2 - col0) / 5;
  263. float rowh = 25;
  264. var fields2 = DrawTable(ip,
  265. new float[] { col0, colw, colw, colw, colw, colw },
  266. new float[] { 50, rowh, rowh, rowh, rowh, rowh, rowh, rowh, rowh },
  267. g, pen);
  268.  
  269. tl.ParagraphAlignment = ParagraphAlignment.Far;
  270. drawField("DATE", fields2[0, 0], null);
  271. drawField("START TIME", fields2[1, 0], null);
  272. drawField("END TIME", fields2[2, 0], null);
  273. drawField("REGULAR HOURS", fields2[3, 0], null);
  274. drawField("OVERTIME HOURS", fields2[4, 0], null);
  275. tf.FontBold = true;
  276. drawField("TOTAL HOURS", fields2[5, 0], null);
  277. tf.FontBold = false;
  278. tl.ParagraphAlignment = ParagraphAlignment.Center;
  279. tf.ForeColor = Color.Gray;
  280. for (int i = 0; i < 7; ++i)
  281. drawField(_Names.Dows[i], fields2[0, i + 1], _Names.DtNames[_Names.Dows[i]][0]);
  282. // Vertically align date fields (compensate for different DOW widths):
  283. var dowFields = doc.AcroForm.Fields.TakeLast(7);
  284. var minW = dowFields.Min(f_ => ((TextField)f_).Widget.Rect.Width);
  285. dowFields.ToList().ForEach(f_ =>
  286. {
  287. var r_ = ((TextField)f_).Widget.Rect;
  288. r_.Offset(r_.Width - minW, 0);
  289. r_.Width = minW;
  290. ((TextField)f_).Widget.Rect = r_;
  291. });
  292.  
  293. tf.ForeColor = Color.Black;
  294. for (int row = 1; row <= 7; ++row)
  295. for (int col = 1; col <= 5; ++col)
  296. drawField(null, fields2[col, row], _Names.DtNames[_Names.Dows[row - 1]][col]);
  297.  
  298. tf.FontBold = true;
  299. drawField("WEEKLY TOTALS", fields2[0, 8], null);
  300. tf.FontBold = false;
  301.  
  302. drawField(null, fields2[3, 8], _Names.TotalReg);
  303. drawField(null, fields2[4, 8], _Names.TotalOvr);
  304. drawField(null, fields2[5, 8], _Names.TotalHours);
  305.  
  306. ip.Y = fields2[0, 8].Bottom;
  307.  
  308. col0 = 72 * 4;
  309. colw = page.Size.Width - marginH * 2 - col0;
  310. var fields3 = DrawTable(ip,
  311. new float[] { col0, colw },
  312. new float[] { rowh + 10, rowh, rowh },
  313. g, pen);
  314.  
  315. drawField("EMPLOYEE SIGNATURE: ", fields3[0, 1], null);
  316. // Employee signature:
  317. var r = fields3[0, 1];
  318. _empSignRect = new RectangleF(r.X + r.Width / 2, r.Y, r.Width / 2 - _inputMargin * 2, r.Height);
  319. var sf = new SignatureField() { Name = _Names.EmpSign };
  320. sf.Widget.Rect = new RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2);
  321. sf.Widget.Page = page;
  322. sf.Widget.BackColor = Color.LightSeaGreen;
  323. doc.AcroForm.Fields.Add(sf);
  324. drawField("DATE: ", fields3[1, 1], _Names.EmpSignDate);
  325.  
  326. drawField("SUPERVISOR SIGNATURE: ", fields3[0, 2], null);
  327. // Supervisor signature:
  328. r = fields3[0, 2];
  329. sf = new SignatureField() { Name = _Names.SupSign };
  330. sf.Widget.Rect = new RectangleF(r.X + r.Width / 2, r.Y + _inputMargin, r.Width / 2 - _inputMargin * 2, r.Height - _inputMargin * 2);
  331. sf.Widget.Page = page;
  332. sf.Widget.BackColor = Color.LightYellow;
  333. doc.AcroForm.Fields.Add(sf);
  334. drawField("DATE: ", fields3[1, 2], _Names.SupSignDate);
  335.  
  336. // Done:
  337. return doc;
  338. }
  339.  
  340. // Simple table drawing method. Returns the array of table cell rectangles.
  341. private RectangleF[,] DrawTable(PointF loc, float[] widths, float[] heights, GcGraphics g, GCDRAW.Pen p)
  342. {
  343. if (widths.Length == 0 || heights.Length == 0)
  344. throw new Exception("Table must have some columns and rows.");
  345.  
  346. RectangleF[,] cells = new RectangleF[widths.Length, heights.Length];
  347.  
  348. var r = new RectangleF(loc, new SizeF(widths.Sum(), heights.Sum()));
  349.  
  350. // Draw left borders (except for 1st one):
  351. float x = loc.X;
  352. for (int i = 0; i < widths.Length; ++i)
  353. {
  354. for (int j = 0; j < heights.Length; ++j)
  355. {
  356. cells[i, j].X = x;
  357. cells[i, j].Width = widths[i];
  358. }
  359. if (i > 0)
  360. g.DrawLine(x, r.Top, x, r.Bottom, p);
  361. x += widths[i];
  362. }
  363. // Draw top borders (except for 1st one):
  364. float y = loc.Y;
  365. for (int j = 0; j < heights.Length; ++j)
  366. {
  367. for (int i = 0; i < widths.Length; ++i)
  368. {
  369. cells[i, j].Y = y;
  370. cells[i, j].Height = heights[j];
  371. }
  372. if (j > 0)
  373. g.DrawLine(r.Left, y, r.Right, y, p);
  374. y += heights[j];
  375. }
  376. // Draw outer border:
  377. g.DrawRectangle(r, p);
  378. // Done:
  379. return cells;
  380. }
  381.  
  382. // Fill in employee info and working hours with sample data:
  383. private Stream FillEmployeeData(GcPdfDocument doc)
  384. {
  385. // For the purposes of this sample, we fill the form with random data:
  386. var empName = "Jaime Smith";
  387. SetFieldValue(doc, _Names.EmpName, empName);
  388. SetFieldValue(doc, _Names.EmpNum, "12345");
  389. SetFieldValue(doc, _Names.EmpDep, "Research & Development");
  390. SetFieldValue(doc, _Names.EmpTitle, "Senior Developer");
  391. SetFieldValue(doc, _Names.EmpStatus, "Full Time");
  392. var rand = new Random((int)Common.Util.TimeNow().Ticks);
  393. DateTime workday = Common.Util.TimeNow().AddDays(-15);
  394. while (workday.DayOfWeek != DayOfWeek.Sunday)
  395. workday = workday.AddDays(1);
  396. TimeSpan wkTot = TimeSpan.Zero, wkReg = TimeSpan.Zero, wkOvr = TimeSpan.Zero;
  397. for (int i = 0; i < 7; ++i)
  398. {
  399. // Start time:
  400. var start = new DateTime(workday.Year, workday.Month, workday.Day, rand.Next(6, 12), rand.Next(0, 59), 0);
  401. SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][0], start.ToShortDateString());
  402. SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][1], start.ToShortTimeString());
  403. // End time:
  404. var end = start.AddHours(rand.Next(8, 14)).AddMinutes(rand.Next(0, 59));
  405. SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][2], end.ToShortTimeString());
  406. var tot = end - start;
  407. var reg = TimeSpan.FromHours((start.DayOfWeek != DayOfWeek.Saturday && start.DayOfWeek != DayOfWeek.Sunday) ? 8 : 0);
  408. var ovr = tot.Subtract(reg);
  409. SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][3], reg.ToString(@"hh\:mm"));
  410. SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][4], ovr.ToString(@"hh\:mm"));
  411. SetFieldValue(doc, _Names.DtNames[_Names.Dows[i]][5], tot.ToString(@"hh\:mm"));
  412. wkTot += tot;
  413. wkOvr += ovr;
  414. wkReg += reg;
  415. //
  416. workday = workday.AddDays(1);
  417. }
  418. SetFieldValue(doc, _Names.TotalReg, wkReg.TotalHours.ToString("F"));
  419. SetFieldValue(doc, _Names.TotalOvr, wkOvr.TotalHours.ToString("F"));
  420. SetFieldValue(doc, _Names.TotalHours, wkTot.TotalHours.ToString("F"));
  421. SetFieldValue(doc, _Names.EmpSignDate, workday.ToShortDateString());
  422.  
  423. // Digitally sign the document on behalf of the 'employee':
  424. var pfxPath = Path.Combine("Resources", "Misc", "JohnDoe.pfx");
  425. var cert = new X509Certificate2(File.ReadAllBytes(pfxPath), "secret",
  426. X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
  427. var sp = new SignatureProperties()
  428. {
  429. SignatureBuilder = new Pkcs7SignatureBuilder()
  430. {
  431. CertificateChain = new X509Certificate2[] { cert }
  432. },
  433. DocumentAccessPermissions = AccessPermissions.FormFillingAndAnnotations,
  434. Reason = "I confirm time sheet is correct.",
  435. Location = "TimeSheetIncremental sample",
  436. SignerName = empName,
  437. SigningDateTime = Common.Util.TimeNow(),
  438. };
  439.  
  440. // Connect the signature field and signature props:
  441. SignatureField empSign = doc.AcroForm.Fields.First(f_ => f_.Name == _Names.EmpSign) as SignatureField;
  442. sp.SignatureField = empSign;
  443. empSign.Widget.ButtonAppearance.Caption = empName;
  444. // Some browser PDF viewers do not show form fields, so we render a placeholder:
  445. empSign.Widget.Page.Graphics.DrawString("digitally signed", new TextFormat() { FontName = "Segoe UI", FontSize = 9 }, empSign.Widget.Rect);
  446.  
  447. // We now 'flatten' the form: loop over document AcroForm's fields,
  448. // drawing their current values in place, and then remove the fields.
  449. // This produces a PDF with text fields' values as part of the regular
  450. // (non-editable) content (we leave fields filled by the supervisor):
  451. FlattenDoc(doc, _Names.EmpSuper, _Names.SupSignDate);
  452.  
  453. // Done, now save the document with employee's signature:
  454. var ms = new MemoryStream();
  455. // Note that we do NOT use incremental update here (3rd parameter is false)
  456. // as this is not needed yet (but will be needed/used when signing by supervisor later):
  457. doc.Sign(sp, ms, false);
  458. return ms;
  459. }
  460.  
  461. // Sets the value of a field with the specified name:
  462. private void SetFieldValue(GcPdfDocument doc, string name, string value)
  463. {
  464. var fld = doc.AcroForm.Fields.First(f_ => f_.Name == name);
  465. if (fld != null)
  466. fld.Value = value;
  467. }
  468. }
  469. }
  470.