This is a Windows Forms application that adds JavaScripts to PDF files. It supports multiple files at the same time. Right now, the only supported script is a "time-stamp on print" script, but any others I add in the future would work exactly the same in principle.
My company uses the "time-stamp on print" script for time-sensitive documents. We originally manually added the script to each individual PDF document, then switched to an Adobe Acrobat Pro batch process script. The licenses for Acrobat Pro are expensive, and it can be buggy/slow, so we are looking for a better solution. While I was working on that, figured I might as well make it a GUI app with room to expand for future needs (and open-source third-party use).
I tried a somewhat new method for limiting the length of lines in this project. In short, I inserted a line break before each parameter to a method. Let me know if the code is readable or not (because of the line breaking or any other reasons).
PdfScriptTool
using iTextSharp.text.exceptions;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.draw;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static PdfScriptTool.PdfScriptToolConstants;
namespace PdfScriptTool
{
internal partial class PdfScriptTool : Form, IProgress<ProgressReport>
{
#region Folders
private static string RootPath = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.MyDocuments),
RootFolderName);
private static string ConfigurationPath = Path.Combine(
RootPath,
ConfigurationFolderName);
private static string OutputRootPath = Path.Combine(
RootPath,
OutputFolderName);
private static string ProcessingPath = Path.Combine(
RootPath,
ProcessingFolderName);
private static string TimeStampScriptPath = Path.Combine(
ConfigurationPath,
TimeStampScriptFileName);
#endregion Folders
internal PdfScriptTool()
{
InitializeComponent();
InitializeOpenFileDialog();
Directory.CreateDirectory(RootPath);
Directory.CreateDirectory(OutputRootPath);
Directory.CreateDirectory(ConfigurationPath);
Directory.CreateDirectory(ProcessingPath);
}
private static string TimeStampScript
{
get
{
var timeStampScript = string.Empty;
if (File.Exists(TimeStampScriptPath))
{
using (var reader = new StreamReader(TimeStampScriptPath))
{
timeStampScript = reader.ReadToEnd();
}
}
else
{
timeStampScript = DefaultTimeStampScript;
}
return timeStampScript;
}
}
public void Report(ProgressReport progressReport)
{
if (InvokeRequired)
{
Invoke((Action)(() => Report(progressReport)));
}
else
{
progressBar.Value = progressReport.Percent;
progressLabel.Text = progressReport.CurrentCount
+ ProgressLabelDivider + progressReport.Total;
}
}
private async Task PerformTask(Task task)
{
if(documentsView.CheckedItems.Count > 0)
{
try
{
await task;
}
catch(Exception e)
{
MessageBox.Show("Exception: " + e.Message);
}
progressBar.Value = 0;
progressLabel.Text = string.Empty;
Enabled = true;
}
else
{
MessageBox.Show("Please select at least one document.");
}
}
private static string GetOutputPath(string inputPath)
{
return Path.Combine(OutputRootPath, Path.GetFileName(inputPath));
}
// TODO
private string ConvertToPdf(string filename)
{
string pdfPath = Path.Combine(
ProcessingPath,
Path.GetFileNameWithoutExtension(filename) + ".pdf");
return pdfPath;
}
private void InitializeOpenFileDialog()
{
openFileDialog.Filter = OpenFileDialogFilter;
openFileDialog.Multiselect = OpenFileDialogAllowMultiple;
openFileDialog.Title = OpenFileDialogTitle;
}
private void selectDocuments_Click(object sender, EventArgs e)
{
var dialogResult = openFileDialog.ShowDialog();
if (dialogResult == DialogResult.OK)
{
foreach (var file in openFileDialog.FileNames)
{
documentsView.Items.Add(file, DocumentsViewFileIsChecked);
}
}
}
private async void timeStampDocuments_Click(object sender, EventArgs e)
{
await PerformTask(TimeStampPdfs());
}
private Task TimeStampPdfs() => Task.Run(() =>
{
for (int i = 0; i < documentsView.CheckedItems.Count; i++)
{
if (!string.Equals(
Path.GetExtension(
documentsView.CheckedItems[i].ToString()),
".pdf",
StringComparison.InvariantCultureIgnoreCase))
{
documentsView.CheckedItems[i] = ConvertToPdf(
documentsView.CheckedItems[i].ToString());
}
TimeStampPdf(documentsView.CheckedItems[i].ToString());
Report(new ProgressReport
{
Total = documentsView.CheckedItems.Count,
CurrentCount = i + 1
});
}
}).ContinueWith(t =>
{
MessageBox.Show("Timestamped all files.");
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
);
private void TimeStampPdf(string filename)
{
try
{
using (var pdfReader = new PdfReader(filename))
{
using (var pdfStamper = new PdfStamper(
pdfReader,
new FileStream(
GetOutputPath(filename),
FileMode.Create)))
{
var parentField = PdfFormField.CreateTextField(
pdfStamper.Writer,
false,
false,
0);
parentField.FieldName = TimeStampFieldName;
var lineSeparator = new LineSeparator();
for (var pageNumber = PdfFirstPageNumber;
pageNumber <= pdfReader.NumberOfPages;
pageNumber++)
{
var pdfContentByte = pdfStamper.GetOverContent(
pageNumber);
var textField = new TextField(
pdfStamper.Writer,
new iTextSharp.text.Rectangle(
TimeStampFieldTopLeftXCoordinate,
TimeStampFieldTopLeftYCoordinate,
TimeStampFieldBottomRightXCoordinate,
TimeStampFieldBottomRightYCoordinate),
null);
var childField = textField.GetTextField();
parentField.AddKid(childField);
childField.PlaceInPage = pageNumber;
}
pdfStamper.AddAnnotation(parentField, 1);
var pdfAction = PdfAction.JavaScript(
TimeStampScript,
pdfStamper.Writer);
pdfStamper.Writer.SetAdditionalAction(
PdfWriter.WILL_PRINT,
pdfAction);
}
}
}
catch (InvalidPdfException e)
{
MessageBox.Show(e.Message + " " + filename);
}
}
}
}
ProgressReport
using static PdfScriptTool.PdfScriptToolConstants;
namespace PdfScriptTool
{
internal class ProgressReport
{
public int CurrentCount { get; set; }
public int Percent
{
get
{
return PercentMultiplier * CurrentCount / Total;
}
}
public int Total { get; set; }
}
}
PdfScriptToolConstants
namespace PdfScriptTool
{
internal static class PdfScriptToolConstants
{
internal const string DefaultTimeStampScript
= "var f = this.getField('Timestamp');"
+ "f.alignment = 'left';"
+ "f.multiline = false;"
+ "f.textSize = 11;"
+ "f.richText = true;"
+ "var style = f.defaultStyle;"
+ "style.fontFamily = ['Calibri', 'sans-serif'];"
+ "f.defaultStyle = style;"
+ "var t = new Array();"
+ "t[0] = new Object();"
+ "t[0].text = 'Uncontrolled 24 hours after ';"
+ "t[1] = new Object();"
+ "t[1].text = util.printd('mm/dd/yy h:MM tt', new Date());"
+ "f.richValue = t;";
internal const string RootFolderName = "PDFScriptTool";
internal const string OutputFolderName = "Output";
internal const string ConfigurationFolderName = "Configuration";
internal const string ProcessingFolderName = "Processing";
internal const string TimeStampScriptFileName = "TimeStampScript.txt";
internal const string OpenFileDialogFilter
= "Documents (*.pdf)|*.pdf";
internal const string OpenFileDialogTitle = "Select documents...";
internal const bool OpenFileDialogAllowMultiple = true;
internal const bool DocumentsViewFileIsChecked = true;
internal const string TimeStampFieldName = "Timestamp";
internal const int PdfFirstPageNumber = 1;
internal const int TimeStampFieldTopLeftXCoordinate = 36;
internal const int TimeStampFieldTopLeftYCoordinate = 792;
internal const int TimeStampFieldBottomRightXCoordinate = 576;
internal const int TimeStampFieldBottomRightYCoordinate = 756;
internal const int TimeStampFieldUnderlineLeftXCoordinate = 36;
internal const int TimeStampFieldUnderlineRightXCoordinate = 576;
internal const int TimeStampFieldUnderlineYCoordinate = 768;
internal const int PercentMultiplier = 100;
internal const string ProgressLabelDivider = " of ";
}
}