5
\$\begingroup\$

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 ";
    }
}
\$\endgroup\$
4
\$\begingroup\$

It's time to get through separation of concerns.

Whatever you are trying to do with your PDFs, your form is not the place to do it. At the very least you should move the functionality to a different class with static methods.

If you do that you will realise that TimeStampPdfs() will need two parameters. One is the list of the files that you need to process, and the other is the component that does the report on IProgress interface, in this case that is your form.

Continuing, will also realise that if sometime will want to use this class on web for instance you can not have any MessageBox showing on TimeStampPdf, so we will just remove that try catch

lineSeparator and pdfContentByte are never being used there. Remove them.

Some folks like to write nested using statements without defining a new scope (increasing the indentation), I learned with those folks and I now prefer that way.

I will not clutter my answer with a lot of code since you should be able to do most of the refactoring, but I will still give an implementation of TimeStampPdfs

public static Task<IEnumerable<string>>  TimeStampPdfs(IEnumerable<string> files, 
    IProgress<ProgressReport> progress) => Task.Run(() =>
{
    int i = 0;
    int count = files.Count();
    var list = new List<string>();
    foreach (var file in files)
    {
        string outFile = file;
        if (!".pdf".Equals(Path.GetExtension(file),
             StringComparison.InvariantCultureIgnoreCase))
        {
            outFile = ConvertToPdf(file);
        }
        list.Add(outFile);
        TimeStampPdf(outFile);
        progress.Report(new ProgressReport
        {
            Total = count,
            CurrentCount = ++i
        });
    }
    return list;
});

The usage on the form should be something along the lines of:

private async void timeStampDocuments_Click(object sender, EventArgs e)
{
    var files = documentsView.CheckedItems.Cast<object>().Select(o => o.ToString());
    try{
        await PerformTask(TimeStampPdfs(files));
    }catch(Exception e){
        //when you work with tasks you normally get AggregateExceptions
        //catch your exception according to its type 
        //at the moment you don't have a way to tell which file failed, I will leave that on your side
        MessageBox.Show(e.Message);
    }
    //my answer to your previous question also missed the opportunity to show the message here
    MessageBox.Show("Timestamped all files.");
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.