Super Updated Version, Part 2

In my early attempts, which I unfortunately did not document, I just had the code insert the word “Exhibit” plus a counter, all at a hard-coded font and size. It took me a while to figure out how to make iText7 do that. Of course, it would be far better to give users the choice of customizing their slipsheets, so I came up with this form.

This form lets you customize the slipsheet or use a specific PDF document as slipsheet. You can center the title vertically or some inches from the top. You can align it left, right, or centered. You can select the font and the font size, and lots of other options.

public Slipsheets()
{
    InitializeComponent();
    txtInchesTop.Enabled = !chkVertical.Checked;
    txtFont.Font = new Font(Properties.Settings.Default.Font.Name, 11, FontStyle.Regular);
    txtFont.Text = txtFont.Font.Name + " " + Properties.Settings.Default.Font.Size + " pts";
}

private void Slipsheets_Load(object sender, EventArgs e)
{
    if (Properties.Settings.Default.SlipsheetSettings == true)
    {
        radPanel1.Checked = false;
        radPanel1.PerformClick();
        if (radNumbers.Checked) radNumbers.PerformClick();
        if (radLetters.Checked) radLetters.PerformClick();
        if (radNone.Checked) radNone.PerformClick();
        if (chkVertical.Checked == false)
        {
            txtInchesTop.Select();
        }
    }
    else
    {
        radPanel2.Checked = false;
        radPanel2.PerformClick();
    }
}

There’s the code for the constructor and the Load event. My rule of thumb is to put as much in the constructor as I can, putting in the Load event only what won’t work in the constructor because the form isn’t ready yet. I forget exactly what the issue was here, but that’s how I wound up splitting the code.

private void radPanel_Click(object sender, EventArgs e)
{
    var rb = sender as RadioButton;
    switch (rb.Name)
    {
        case "radPanel1":
            if (rb.Checked == false)
            {
                rb.Checked = true;
                radPanel2.Checked = false;
                groupBox1.Enabled = true;
                groupBox2.Enabled = false;
            }
            break;
        case "radPanel2":
            if (rb.Checked == false)
            {
                rb.Checked = true;
                radPanel1.Checked = false;
                groupBox1.Enabled = false;
                groupBox2.Enabled = true;
            }
            break;
        default:
            break;
    }
}

Both radio buttons are wired to the same event handler. As you can see in the code above, there’s nothing mysterious going on. If you click, that panel becomes enabled and the other one disabled. I had to use the Click event because I couldn’t get it to work through the CheckedChanged event. It fired multiple times (understandable) but I couldn’t figure it out.

private void chkVertical_CheckedChanged(object sender, EventArgs e)
{
    if (chkVertical.Checked)
    {
        txtInchesTop.Enabled = false;
    }
    else
    {
        txtInchesTop.Enabled = true;
        txtInchesTop.Select();
    }
}

Here we see what happens when you click the checkbox. If you want it aligned vertically, the textbox is disabled; otherwise, the textbox is enabled and selected.

private void btnFont_Click(object sender, EventArgs e)
{
    fontDialog1.ShowDialog();
    if (fontDialog1.Font is null == false)
    {
        Properties.Settings.Default.Font = fontDialog1.Font;
        txtFont.Font = new Font(fontDialog1.Font.Name, 11, FontStyle.Regular);
        txtFont.Text = fontDialog1.Font.Name + " " + fontDialog1.Font.Size + " pts";
    }
}

I like how the font selection worked out! I use the Font dialog, and if you select a font, the textbox will show in that font. I thought that was pretty cool! 🙂

private void radCounterType_Click(object sender, EventArgs e)
{
    var rb = sender as RadioButton;
    switch (rb.Name)
    {
        case "radNumbers":
            radNumbers.Checked = true;
            txtStarting.Text = "1";
            txtStarting.Enabled = true;
            break;
        case "radLetters":
            radLetters.Checked = true;
            txtStarting.Text = "A";
            txtStarting.Enabled = true;
            break;
        case "radNone":
            radNone.Checked = true;
            txtStarting.Text = "";
            txtStarting.Enabled = false;
            break;
        default:
            break;
    }
}

Meh… this part is pretty boring, however. Depending on the radio button you click, the starting text will be set accordingly.

private void btnOk_Click(object sender, EventArgs e)
{
    DialogResult = DialogResult.OK;
    Properties.Settings.Default.SlipsheetText = txtSlipsheetText.Text;
    Properties.Settings.Default.Save();
    Close();
}

Finally, there’s the code that executes upon closing the form. Note that we save properties, which are data-bound to the controls on this form, so we can use those same values next time we open the form. The data is “sticky,” in other words.

The Slipsheet Options form, of course, just collects information. Here is the form’s basic structure.

private void toolInsertSlips_Click(object sender, EventArgs e)
{
    Form form = new Slipsheets();
    form.ShowDialog();
    if (form.DialogResult == DialogResult.OK)
    {
        // process slipsheets
    }
    form.Close();
    form.Dispose();
}

And here’s the meaty part of the method. Check it out.

PdfFontFactory.RegisterSystemDirectories();
var HorAlign = new TextAlignment();
if (Properties.Settings.Default.AlignLeft == true)
    HorAlign = TextAlignment.LEFT;
if (Properties.Settings.Default.AlignCenter == true)
    HorAlign = TextAlignment.CENTER;
if (Properties.Settings.Default.AlignRight == true)
    HorAlign = TextAlignment.RIGHT;
float TopAlign = float.Parse(Properties.Settings.Default.InchesTop) * 72f;
int counter = 0;
foreach (DataRow dr in dataSet1.Files.Rows)
{
    string pdfFile = dr.Field<string>(dataSet1.Files.FullNameColumn);
    string tempFile = Path.GetTempFileName();
    var pdfReader = new PdfReader(pdfFile);
    var pdfWriter = new PdfWriter(tempFile);
    var pdf = new PdfDocument(pdfReader, pdfWriter);
    var pageSize = new iText.Kernel.Geom.PageSize(
        pdf.GetFirstPage().GetPageSize());
    pdf.AddNewPage(1, pageSize);
    var para = new Paragraph();
    para.SetFont(
        PdfFontFactory.CreateRegisteredFont(
            Properties.Settings.Default.Font.Name));
    para.SetFontSize(Properties.Settings.Default.Font.Size);
    para.SetFixedPosition(
        1, 0, pageSize.GetHeight() - TopAlign, pageSize.GetWidth());
    para.SetTextAlignment(HorAlign);
    para.Add(Properties.Settings.Default.SlipsheetText.Trim());
    if (Properties.Settings.Default.CounterNone == false)
    {
        counter++;
        string pn = string.Empty;
        if (Properties.Settings.Default.CounterNumbers == true)
        {
            pn = counter.ToString();
        }
        else
        {
            int number = counter;
            int remainder;
            while (number >= 26)
            {
                remainder = number % 26;
                pn = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"[remainder] + pn;
                number = number / 26;
            }
            pn = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"[number] + pn;
        }
        para.Add(" " + pn);
    }
    var doc = new Document(pdf);
    doc.Add(para);
    doc.Flush();
    doc.Close();
    pdf.Close();
    File.Copy(tempFile, pdfFile, true);
    File.Delete(tempFile);
    dr.SetField(
        dataSet1.Files.PageCountColumn,
            dr.Field<int>(dataSet1.Files.PageCountColumn) + 1);
}

In the first line, we call PdfFontFactory.RegisterSystemDirectories. This makes all the fonts on the computer available to iText7, which otherwise only offers four fonts of its own. Then we restore alignment values from the application properties and start looping through the rows in the table.

For each row, we create PdfReader, Pdf Writer, and PdfDocument. Then we add a new page at position 1. Then we create a Paragraph object, and now the fun begins.

We set the font for the paragraph by using PdfFontCreator to create a font from the name we previously stored in the application properties. You’d think you could just use the Font object you selected, but iText7 uses its own PdfFont class. Then we set the font size. Next we set the position at which the paragraph will appear, which is at page 1 and 0 distance from the left. The vertical position is the page height minus the top margin indicated by the user. The PDF format (or perhaps it’s just iText7, I honestly don’t know) uses an XY system of coordinates, with 0,0 being the lower left corner of the page. This confused me at first, since I’m used to 0,0 being the upper left corner of the page. For the width, we’re going to make it the whole width of the page, since the user expects the horizontal alignment to be across the page.

We add the text we want to the paragraph, and then, depending on whether the user selected Numbers, Letters, or None, add the counter. The complex part is converting numbers to letters, such that 26 is “A” and 27 is “AA.” It’s basically a base conversion problem, except without any numeric digits at all. I worked out the algorithm with lots of Google help. 🙂

After all that, we create a Document object, add our paragraph to it, and then flush and close the document and the PdfDocument object. After that, we copy the temp file to our original file and delete the temp file. Finally, we add one to the page count since the PDF indeed now has an additional page.

There’s one last function that uses a separate form, but it’s very simple. The function is to “trim” PDFs, removing the first page and also the last one if the last one is blank. Here’s the form.

The dialog remembers your choices, and you don’t have to trim the first page. You could just choose to trim last pages. However, if both boxes are unchecked, the Ok button becomes disabled, so you must choose one or both. This is a very useful tool for undoing the effects of the Add Slipsheet and Make Even functions. The trimming of the last page is very safe. It only trims pages that have literally 0 content bytes. If your last page looks blank because it was scanned from a blank page, it really isn’t blank. It will still have a few bytes of content.

private void toolTrim_Click(object sender, EventArgs e)
{
    Form frm = new TrimOptions();
    frm.ShowDialog();
    if (frm.DialogResult == DialogResult.OK)
    {
        foreach (DataRow dr in dataSet1.Files.Rows)
        {
            string tempFile = Path.GetTempFileName();
            string pdfFile = dr.Field<string>(dataSet1.Files.FullNameColumn);
            var pdfReader = new PdfReader(pdfFile);
            var pdfWriter = new PdfWriter(tempFile);
            var pdf = new PdfDocument(pdfReader, pdfWriter);
            int removeCount = 0;
            if (Properties.Settings.Default.TrimFirst)
            {
                pdf.RemovePage(1);
                removeCount++;
            }
            if (Properties.Settings.Default.TrimLast)
            {
                int np = pdf.GetNumberOfPages();
                var last = pdf.GetPage(np);
                if (last.GetContentBytes().Length == 0)
                {
                    pdf.RemovePage(np);
                    removeCount++;
                }
            }
            pdf.Close();
            File.Copy(tempFile, pdfFile, true);
            File.Delete(tempFile);
            dr.SetField(
                dataSet1.Files.PageCountColumn, dr.Field<int> 
                    (dataSet1.Files.PageCountColumn) - removeCount);
        }
    }
    frm.Dispose();
}

This code is so much simpler, proof that it’s easier to destroy than to create! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *