This is an update from here. This tool is a Windows Form application that can add fields and scripts to PDF files, as well as convert Microsoft Office Word documents to PDF. The usage is as follows:
- User opens program and presses "Select Files" button.
- A dialog opens allowing the user to select one or more Word documents or PDF files.
- The user presses a button to either add a field/script (for now, each field/script this tool supports has its own button) or convert to PDF only.
- The back end handles the processing. Any Word documents are first converted to PDF and placed in a "Processing" folder.
- If a field/script was selected, the appropriate field and script are added to the PDF files.
- All PDF files, including those in processing, are saved to an "Output" folder.
An image of the GUI is at the bottom of this post for reference.
An overview of the classes:
Program.cs
is the auto-generated Windows Form main file. Opens the Windows Form. Code omitted from this post.PdfScript.cs
is the Windows Form itself. It contains all the UI elements including button listeners, and passes commands to the back end.PdfProcessor.cs
is the back end. It does all the work, and reports progress to the front end.Field.cs
is a class to configure PDF fields.ProgressReport.cs
is a class to handle progress reporting in the form of a current, total, and percent.Script.cs
is a class to configure PDF scripts.
PdfScriptTool
namespace PdfScriptTool
{
using System.Linq;
using Action = System.Action;
using DialogResult = System.Windows.Forms.DialogResult;
using EventArgs = System.EventArgs;
using Exception = System.Exception;
using Form = System.Windows.Forms.Form;
using Func = System.Func<System.Threading.Tasks.Task>;
using IProgress = System.IProgress<ProgressReport>;
using MessageBox = System.Windows.Forms.MessageBox;
using Resources = Properties.Resources;
using Task = System.Threading.Tasks.Task;
/// <summary>
/// The main application window.
/// </summary>
internal partial class PdfScriptTool : Form, IProgress
{
/// <summary>
/// Whether or not files in the file view should be automatically
/// checked (selected).
/// </summary>
private const bool FileViewFileIsChecked = true;
/// <summary>
/// Whether or not the open file dialog should allow selection of
/// multiple files.
/// </summary>
private const bool OpenFileDialogAllowMultiple = true;
/// <summary>
/// The PDF Processor that does the back end work.
/// </summary>
private PdfProcessor pdfProcessor;
/// <summary>
/// Initializes a new instance of the <see cref="PdfScriptTool"/>
/// class.
/// </summary>
internal PdfScriptTool()
{
InitializeComponent();
InitializeOpenFileDialog();
pdfProcessor = new PdfProcessor();
}
/// <summary>
/// Reports the progress of the current task.
/// </summary>
/// <param name="progressReport">The progress report containing a
/// current count, total count, and percent.</param>
public void Report(ProgressReport progressReport)
{
if (InvokeRequired)
{
Invoke((Action)(() => Report(progressReport)));
}
else
{
progressBar.Value = progressReport.Percent;
}
}
/// <summary>
/// Performs a specified task in the backend.
/// </summary>
/// <param name="function">The task to perform.</param>
/// <returns>The completed task.</returns>
internal async Task PerformTask(Func function)
{
if (fileView.CheckedItems.Count > 0)
{
Enabled = false;
try
{
pdfProcessor.Files =
fileView.CheckedItems.OfType<string>().ToList();
await function();
}
catch (Exception e)
{
ShowException(e);
}
ShowMessage(Resources.FilesSavedInMessage +
PdfProcessor.OutputRootPath);
progressBar.Value = 0;
Enabled = true;
}
else
{
ShowMessage(Resources.NoFilesSelectedErrorMessage);
}
}
/// <summary>
/// Shows an exception in a message box.
/// </summary>
/// <param name="e">The exception to show.</param>
private static void ShowException(Exception e)
{
ShowMessage(e.Message);
}
/// <summary>
/// Shows a message in a message box.
/// </summary>
/// <param name="message">The message to show.</param>
private static void ShowMessage(string message)
{
MessageBox.Show(message);
}
/// <summary>
/// Listener for the "Convert to PDF Only" button.
/// </summary>
/// <param name="sender">The object that triggered the event.</param>
/// <param name="e">The event arguments.</param>
private async void ConvertOnly_Click(object sender, EventArgs e)
{
await PerformTask(() => pdfProcessor.ProcessPdfs(this));
}
/// <summary>
/// Sets attributes for the open file dialog.
/// </summary>
private void InitializeOpenFileDialog()
{
openFileDialog.Filter = Resources.OpenFileDialogFilter;
openFileDialog.Multiselect = OpenFileDialogAllowMultiple;
openFileDialog.Title = Resources.OpenFileDialogTitle;
}
/// <summary>
/// Listener for the "Select Files" button. Shows the select files
/// dialog and adds all selected files to the files view, locking each
/// file to prevent editing until released.
/// </summary>
/// <param name="sender">The object that triggered the event.</param>
/// <param name="e">The event arguments.</param>
private void SelectFiles_Click(object sender, EventArgs e)
{
var dialogResult = openFileDialog.ShowDialog();
if (dialogResult == DialogResult.OK)
{
foreach (var filename in openFileDialog.FileNames)
{
fileView.Items.Add(filename, FileViewFileIsChecked);
}
}
}
/// <summary>
/// Listener for the "Timestamp 24 Hours" button.
/// </summary>
/// <param name="sender">The object that triggered the event.</param>
/// <param name="e">The event arguments.</param>
private async void TimeStampDefaultDay_Click(
object sender, EventArgs e)
{
await PerformTask(() => pdfProcessor.ProcessPdfs(
this,
Field.DefaultTimeStampField,
Script.TimeStampOnPrintDefaultDayScript));
}
/// <summary>
/// Listener for the "Timestamp 30 Days" button.
/// </summary>
/// <param name="sender">The object that triggered the event.</param>
/// <param name="e">The event arguments.</param>
private async void TimeStampDefaultMonth_Click(
object sender, EventArgs e)
{
await PerformTask(() => pdfProcessor.ProcessPdfs(
this,
Field.DefaultTimeStampField,
Script.TimeStampOnPrintDefaultMonthScript));
}
}
}
PdfProcessor
namespace PdfScriptTool
{
using Application = Microsoft.Office.Interop.Word.Application;
using Directory = System.IO.Directory;
using Environment = System.Environment;
using File = System.IO.File;
using FileMode = System.IO.FileMode;
using FileStream = System.IO.FileStream;
using IProgress = System.IProgress<ProgressReport>;
using List = System.Collections.Generic.List<string>;
using Path = System.IO.Path;
using PdfAction = iTextSharp.text.pdf.PdfAction;
using PdfFormField = iTextSharp.text.pdf.PdfFormField;
using PdfName = iTextSharp.text.pdf.PdfName;
using PdfReader = iTextSharp.text.pdf.PdfReader;
using PdfStamper = iTextSharp.text.pdf.PdfStamper;
using PdfWriter = iTextSharp.text.pdf.PdfWriter;
using Rectangle = iTextSharp.text.Rectangle;
using Resources = Properties.Resources;
using SpecialFolder = System.Environment.SpecialFolder;
using StringComparison = System.StringComparison;
using Task = System.Threading.Tasks.Task;
using TextField = iTextSharp.text.pdf.TextField;
using WdExportFormat = Microsoft.Office.Interop.Word.WdExportFormat;
/// <summary>
/// The back end of the PDF Script Tool.
/// </summary>
internal class PdfProcessor
{
/// <summary>
/// Increment of two to skip a page.
/// </summary>
private const int EveryOtherPage = 2;
/// <summary>
/// Increment of one to get every page.
/// </summary>
private const int EveryPage = 1;
/// <summary>
/// Page one should be the first page.
/// </summary>
private const int FirstPageNumber = 1;
/// <summary>
/// Page two should be the first page.
/// </summary>
private const int SecondPageNumber = 2;
/// <summary>
/// Initializes a new instance of the <see cref="PdfProcessor"/> class.
/// </summary>
internal PdfProcessor()
{
Directory.CreateDirectory(RootPath);
Directory.CreateDirectory(OutputRootPath);
Directory.CreateDirectory(ProcessingPath);
}
/// <summary>
/// Gets the folder output is stored in.
/// "Output" in the root folder.
/// </summary>
internal static string OutputRootPath
{
get
{
return Path.Combine(RootPath, Resources.OutputFolderName);
}
}
/// <summary>
/// Gets the folder processing files are stored in.
/// "Processing" in the root folder.
/// </summary>
internal static string ProcessingPath
{
get
{
return Path.Combine(RootPath, Resources.ProcessingFolderName);
}
}
/// <summary>
/// Gets the root folder the program works in.
/// "PDF Script Tool" in the user's "My Documents" folder.
/// </summary>
internal static string RootPath
{
get
{
return Path.Combine(
Environment.GetFolderPath(SpecialFolder.MyDocuments),
Resources.RootFolderName);
}
}
/// <summary>
/// Gets or sets the list of files (paths) to be processed.
/// </summary>
internal List Files { get; set; }
/// <summary>
/// Adds a field and a script to the currently selected files.
/// </summary>
/// <param name="progress">
/// The object to which progress is reported.
/// </param>
/// <param name="field"> The field to be added to the files.</param>
/// <param name="script">The script to be added to the files.</param>
/// <returns>The completed task.</returns>
internal async Task ProcessPdfs(
IProgress progress, Field field = null, Script script = null)
{
await Task.Run(() =>
{
for (int i = 0; i < Files.Count; i++)
{
var currentFile = Files[i];
if (!IsPdf(currentFile))
{
currentFile = ConvertToPdf(currentFile);
}
if (field != null || script != null)
{
ProcessPdf(currentFile, field, script);
}
else
{
MovePdfToOutput(currentFile);
}
progress.Report(new ProgressReport
{
Total = Files.Count,
CurrentCount = i + 1
});
}
});
}
/// <summary>
/// Adds a field to a page of a PDF document.
/// </summary>
/// <param name="field">The field to add.</param>
/// <param name="pageNumber">
/// The page number on which the field will be added.
/// </param>
/// <param name="pdfStamper">The PDF stamper for the document.</param>
/// <param name="parentField">The parent field.</param>
private static void AddFieldToPage(
Field field,
int pageNumber,
PdfStamper pdfStamper,
PdfFormField parentField)
{
var textField = new TextField(
pdfStamper.Writer,
new Rectangle(
field.TopLeftX,
field.TopLeftY,
field.BottomRightX,
field.BottomRightY),
null);
var childField = textField.GetTextField();
parentField.AddKid(childField);
childField.PlaceInPage = pageNumber;
}
/// <summary>
/// Adds a field to a PDF document.
/// </summary>
/// <param name="field">The field to add.</param>
/// <param name="pdfStamper">The PDF stamper for the document.</param>
/// <param name="numberOfPages">
/// The number of pages in the document.
/// </param>
private static void AddFieldToPdf(
Field field, PdfStamper pdfStamper, int numberOfPages)
{
var parentField = PdfFormField.CreateTextField(
pdfStamper.Writer, false, false, 0);
parentField.FieldName = field.Title;
int pageNumber = field.Pages == Pages.Last ?
numberOfPages : FirstPageNumber;
if (field.Pages == Pages.First || field.Pages == Pages.Last)
{
AddFieldToPage(
field,
pageNumber,
pdfStamper,
parentField);
}
else
{
int increment = field.Pages == Pages.All ?
EveryPage : EveryOtherPage;
if (field.Pages == Pages.Even)
{
pageNumber += 1;
}
for (; pageNumber <= numberOfPages; pageNumber += increment)
{
AddFieldToPage(
field,
pageNumber,
pdfStamper,
parentField);
}
}
pdfStamper.AddAnnotation(parentField, FirstPageNumber);
}
/// <summary>
/// Adds a script to a PDF document.
/// </summary>
/// <param name="script">The script to add.</param>
/// <param name="pdfStamper">The PDF stamper for the document.</param>
private static void AddScriptToPdf(
Script script, PdfStamper pdfStamper)
{
var pdfAction = PdfAction.JavaScript(
script.ScriptText, pdfStamper.Writer);
PdfName actionType = null;
switch (script.ScriptEvent)
{
case ScriptEvent.DidPrint:
actionType = PdfWriter.DID_PRINT;
break;
case ScriptEvent.DidSave:
actionType = PdfWriter.DID_SAVE;
break;
case ScriptEvent.WillPrint:
actionType = PdfWriter.WILL_PRINT;
break;
case ScriptEvent.WillSave:
actionType = PdfWriter.WILL_SAVE;
break;
}
pdfStamper.Writer.SetAdditionalAction(
actionType, pdfAction);
}
/// <summary>
/// Converts a file to a PDF document.
/// </summary>
/// <param name="filename">The path of the file to convert.</param>
/// <returns>The path of the converted PDF document.</returns>
private static string ConvertToPdf(string filename)
{
var outputFilename = Path.GetFileNameWithoutExtension(filename)
+ Resources.PdfFileExtension;
var outputPath = Path.Combine(ProcessingPath, outputFilename);
var wordApplication = new Application();
var wordDocument = wordApplication.Documents.Open(filename);
var exportFormat = WdExportFormat.wdExportFormatPDF;
wordDocument.ExportAsFixedFormat(outputPath, exportFormat);
wordDocument.Close(false);
wordApplication.Quit();
return outputPath;
}
/// <summary>
/// Gets the output path for a specified input file.
/// </summary>
/// <param name="inputPath">The input file path.</param>
/// <returns>The output path for the file.</returns>
private static string GetOutputPath(string inputPath)
{
return Path.Combine(OutputRootPath, Path.GetFileName(inputPath));
}
/// <summary>
/// Checks if a file is a PDF document.
/// </summary>
/// <param name="filename">The path of the file to check.</param>
/// <returns>Whether or not the file is a PDF document.</returns>
private static bool IsPdf(string filename)
{
return string.Equals(
Path.GetExtension(filename),
Resources.PdfFileExtension,
StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// Moves a PDF file to the output folder.
/// </summary>
/// <param name="filename">The PDF file to move.</param>
private static void MovePdfToOutput(string filename)
{
File.Move(filename, GetOutputPath(filename));
}
/// <summary>
/// Adds features to a PDF document.
/// </summary>
/// <param name="filename">The path of the PDF document.</param>
/// <param name="field">The field to add.</param>
/// <param name="script">The script to add.</param>
private static void ProcessPdf(
string filename, Field field, Script script)
{
using (var pdfReader = new PdfReader(filename))
{
using (var pdfStamper = new PdfStamper(
pdfReader,
new FileStream(GetOutputPath(filename), FileMode.Create)))
{
if (field != null)
{
AddFieldToPdf(
field,
pdfStamper,
pdfReader.NumberOfPages);
}
if (script != null)
{
AddScriptToPdf(script, pdfStamper);
}
}
}
}
}
}
Field
namespace PdfScriptTool
{
using static Properties.Resources;
/// <summary>
/// The allowed specifications for which pages a field can be placed on.
/// </summary>
internal enum Pages
{
/// <summary>
/// All pages in the document.
/// </summary>
All,
/// <summary>
/// Odd pages in the document.
/// </summary>
Odd,
/// <summary>
/// Even pages in the document.
/// </summary>
Even,
/// <summary>
/// The first page of the document.
/// </summary>
First,
/// <summary>
/// The last page of the document.
/// </summary>
Last
}
/// <summary>
/// Fields are used to design text fields for PDF files.
/// </summary>
internal class Field
{
/// <summary>
/// The default field used for "time stamp on print" methods.
/// </summary>
internal static readonly Field DefaultTimeStampField
= new Field(
DefaultTimestampFieldTitle,
DefaultTopLeftX,
DefaultTopLeftY,
DefaultBottomRightX,
DefaultBottomRightY,
Pages.All);
/// <summary>
/// The x coordinate of the default bottom right field corner.
/// </summary>
private const int DefaultBottomRightX = 576;
/// <summary>
/// The y coordinate of the default bottom right field corner.
/// </summary>
private const int DefaultBottomRightY = 756;
/// <summary>
/// The x coordinate of the default top left field corner.
/// </summary>
private const int DefaultTopLeftX = 36;
/// <summary>
/// The y coordinate of the default top left field corner.
/// </summary>
private const int DefaultTopLeftY = 792;
/// <summary>
/// The maximum y coordinate in points on a portrait-oriented PDF.
/// Top edge of document.
/// </summary>
private const int TYMax = 792;
/// <summary>
/// The maximum x coordinate in points on a portrait-oriented PDF.
/// Right edge of document.
/// </summary>
private const int XMax = 612;
/// <summary>
/// The minimum x coordinate in points on a PDF.
/// Left edge of document.
/// </summary>
private const int XMin = 0;
/// <summary>
/// The minimum y coordinate in points on a PDF.
/// Bottom edge of document.
/// </summary>
private const int YMin = 0;
/// <summary>
/// The four coordinates that comprise the top left and bottom right
/// corners of the field.
/// </summary>
private int[] coordinates;
/// <summary>
/// Initializes a new instance of the <see cref="Field" /> class.
/// </summary>
/// <param name="title">
/// The title of the field.
/// </param>
/// <param name="topLeftX">
/// The top left corner x coordinate of the field.
/// </param>
/// <param name="topLeftY">
/// The top left corner y coordinate of the field.
/// </param>
/// <param name="bottomRightX">
/// The bottom right x coordinate of the field.
/// </param>
/// <param name="bottomRightY">
/// The bottom right y coordinate of the field.
/// </param>
/// <param name="pages">The pages the field should be placed on.
/// </param>
internal Field(
string title,
int topLeftX,
int topLeftY,
int bottomRightX,
int bottomRightY,
Pages pages)
{
Title = title;
coordinates = new int[4];
coordinates[0] = topLeftX;
coordinates[1] = topLeftY;
coordinates[2] = bottomRightX;
coordinates[3] = bottomRightY;
Pages = pages;
}
/// <summary>
/// Gets the bottom right corner x coordinate of the field.
/// </summary>
internal int BottomRightX
{
get
{
return coordinates[2];
}
}
/// <summary>
/// Gets the bottom right corner y coordinate of the field.
/// </summary>
internal int BottomRightY
{
get
{
return coordinates[3];
}
}
/// <summary>
/// Gets or sets the pages the field should be placed on.
/// </summary>
internal Pages Pages { get; set; }
/// <summary>
/// Gets or sets the title of the field.
/// </summary>
internal string Title { get; set; }
/// <summary>
/// Gets the top left corner x coordinate of the field.
/// </summary>
internal int TopLeftX
{
get
{
return coordinates[0];
}
}
/// <summary>
/// Gets the top left corner y coordinate of the field.
/// </summary>
internal int TopLeftY
{
get
{
return coordinates[1];
}
}
}
}
ProgressReport
namespace PdfScriptTool
{
/// <summary>
/// Allows for reporting progress as both a percent and a count.
/// </summary>
internal class ProgressReport
{
/// <summary>
/// Converts a decimal number to a percent value.
/// </summary>
private const int PercentMultiplier = 100;
/// <summary>
/// Gets or sets the number of tasks completed.
/// </summary>
internal int CurrentCount { get; set; }
/// <summary>
/// Gets the percent of tasks completed.
/// </summary>
internal int Percent
{
get
{
return PercentMultiplier * CurrentCount / Total;
}
}
/// <summary>
/// Gets or sets the total number of tasks.
/// </summary>
internal int Total { get; set; }
}
}
Script.cs
namespace PdfScriptTool
{
using static Properties.Resources;
/// <summary>
/// The event that triggers the execution of the script.
/// </summary>
internal enum ScriptEvent
{
/// <summary>
/// The document is preparing to print.
/// </summary>
WillPrint,
/// <summary>
/// The document is preparing to save.
/// </summary>
WillSave,
/// <summary>
/// The document was printed.
/// </summary>
DidPrint,
/// <summary>
/// The document was saved.
/// </summary>
DidSave
}
/// <summary>
/// Scripts define JavaScript text that can be added to a PDF file.
/// </summary>
internal class Script
{
/// <summary>
/// The default "time stamp on print" script, valid for a day.
/// </summary>
internal static readonly Script TimeStampOnPrintDefaultDayScript =
new Script(TimeStampOnPrintDefaultDay, ScriptEvent.WillPrint);
/// <summary>
/// The default "time stamp on print" script, valid for a month.
/// </summary>
internal static readonly Script TimeStampOnPrintDefaultMonthScript =
new Script(TimeStampOnPrintDefaultMonth, ScriptEvent.WillPrint);
/// <summary>
/// Initializes a new instance of the <see cref="Script"/> class.
/// </summary>
/// <param name="scriptText">The JavaScript text of the script.</param>
/// <param name="scriptEvent">The event that triggers the execution of
/// the script.</param>
internal Script(string scriptText, ScriptEvent scriptEvent)
{
ScriptText = scriptText;
ScriptEvent = scriptEvent;
}
/// <summary>
/// Gets or sets the JavaScript text of the script.
/// </summary>
internal string ScriptText { get; set; }
/// <summary>
/// Gets or sets the event that triggers the execution of the script.
/// </summary>
internal ScriptEvent ScriptEvent { get; set; }
}
}