iTextSharp RenameField bug?

2019-02-23 11:30发布

问题:

I am attempting to rename a checkbox that is subclassed. Let's say the checkbox's name is MyForm.Check1.page0. When I run:

reader.AcroField.RenameField("MyForm.Check1.page0", "MyForm.Check1.newName");

The checkbox is renamed just "newName". The subclass information is dropped. I get from the documentation that the subclass cannot be changed, but this was unexpected.

According to the documentation:

"Renames a field. Only the last part of the name can be renamed. For example, if the original field is "ab.cd.ef" only the "ef" part can be renamed."

It has been a long day but I would read that to mean you can rename a field with a subclass of "ab.cd.ef" to "ab.cd.yz", not that if you rename "ab.cd.ef" to "ab.cd.yz" you instead end up with a field named just "yz".

I found the source class on GitHub and it looks like its a bug to me. From GitHub:

 public bool RenameField(String oldName, String newName) {
            int idx1 = oldName.LastIndexOf('.') + 1;
            int idx2 = newName.LastIndexOf('.') + 1;
            <snip>
            Item item = fields[oldName];
            newName = newName.Substring(idx2);

I think the issue is that last line. If this is by design it seems very strange to me. I think I can work around it, but again it just seems very strange.

Edit:

I have copied and cleaned the code and made a sample command line tool that shows this issue. You should just have to download a copy and change the paths for the PDF's. This is all coming from a much larger application so its a bit bloated as a test application but it was faster than trying to rewrite everything. Some of the code is a bit sloppy as its a work in progress. I also left out some code that isn't relevant to this issue (namely the JavaScript code I am inserting).

If you would prefer a delivery mechanism other than DropBox let me know.

I am also pasting the details of the .cs file below. You should just be able to paste this into a C# project and it should work. You will need a PDF with a text field with the name set to "TableStartPosition" or else adjust the fieldPositions object in the FillCoverPage method and update the hard coded paths.

When you run the application there will be some check boxes added that are being assigned a name of "InkSaver.chk2.pageX" (where X is a number). However at the end of the run the checkboxes are just named "pageX". You can watch the code in the RenameFields() method.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace RenameFieldTest
{
    class Program
    {
        Stream _pdfTemplateStream;
        MemoryStream _pdfResultStream;

        PdfReader _pdfTemplateReader;
        PdfStamper _pdfResultStamper;

        static void Main(string[] args)
        {
            Program p = new Program();
            try
            {
                p.RunTest();
            }
            catch (Exception f)
            {
                Console.WriteLine(f.Message);
                Console.ReadLine();
            }
        }
        internal void RunTest()
        {
            FileStream fs = File.OpenRead(@"C:\temp\a\RenameFieldTest\RenameFieldTest\Library\CoverPage.pdf");
            _pdfTemplateStream = fs;
            _pdfResultStream = new MemoryStream();
            //PDFTemplateStream = new FileStream(_templatePath, FileMode.Open);
            _pdfTemplateReader = new PdfReader(_pdfTemplateStream);
            _pdfResultStamper = new PdfStamper(_pdfTemplateReader, _pdfResultStream);

            #region setup objects
            List<CustomCategory> Categories = new List<CustomCategory>();
            CustomCategory c1 = new CustomCategory();
            c1.CategorySizesInUse.Add(CustomCategory.AvailableSizes[1]);
            c1.CategorySizesInUse.Add(CustomCategory.AvailableSizes[2]);
            Categories.Add(c1);

            CustomCategory c2 = new CustomCategory();
            c2.CategorySizesInUse.Add(CustomCategory.AvailableSizes[0]);
            c2.CategorySizesInUse.Add(CustomCategory.AvailableSizes[1]);
            Categories.Add(c2);

            List<CustomObject> Items = new List<CustomObject>();
            CustomObject co1 = new CustomObject();
            co1.Category = c1;
            co1.Title = "Object 1";
            Items.Add(co1);

            CustomObject co2 = new CustomObject();
            co2.Category = c2;
            co2.Title = "Object 2";
            Items.Add(co2);

            #endregion

            FillCoverPage(Items);
            _pdfResultStamper.Close();
            _pdfTemplateReader.Close();

            List<MemoryStream> pdfStreams = new List<MemoryStream>();
            pdfStreams.Add(new MemoryStream(_pdfResultStream.ToArray()));

            MergePdfs(@"C:\temp\a\RenameFieldTest\RenameFieldTest\Library\Outfile.pdf", pdfStreams);

            _pdfResultStream.Dispose();
            _pdfTemplateStream.Dispose();
        }
        internal void FillCoverPage(List<CustomObject> Items)
        {

            //Before we start we need to figure out where to start adding the table
            var fieldPositions = _pdfResultStamper.AcroFields.GetFieldPositions("TableStartPosition");
            if (fieldPositions == null)
            { throw new Exception("Could not find the TableStartPosition field. Unable to determine point of origin for the table!"); }

            _pdfResultStamper.AcroFields.RemoveField("TableStartPosition");

            var fieldPosition = fieldPositions[0];
            // Get the position of the field
            var targetPosition = fieldPosition.position;

            //First, get all the available card sizes
            List<string> availableSizes = CustomCategory.AvailableSizes;


            //Generate a table with the number of available card sizes + 1 for the device name
            PdfPTable table = new PdfPTable(availableSizes.Count + 1);
            float[] columnWidth = new float[availableSizes.Count + 1];
            for (int y = 0; y < columnWidth.Length; y++)
            {
                if (y == 0)
                { columnWidth[y] = 320; }
                else
                { columnWidth[y] = 120; }
            }

            table.SetTotalWidth(columnWidth);
            table.WidthPercentage = 100;

            PdfContentByte canvas;

            List<PdfFormField> checkboxes = new List<PdfFormField>();

            //Build the header row
            table.Rows.Add(new PdfPRow(this.GetTableHeaderRow(availableSizes)));

            //Insert the global check boxes
            PdfPCell[] globalRow = new PdfPCell[availableSizes.Count + 1];
            Phrase tPhrase = new Phrase("Select/Unselect All");
            PdfPCell tCell = new PdfPCell();
            tCell.BackgroundColor = BaseColor.LIGHT_GRAY;
            tCell.AddElement(tPhrase);
            globalRow[0] = tCell;

            for (int x = 0; x < availableSizes.Count; x++)
            {
                tCell = new PdfPCell();
                tCell.BackgroundColor = BaseColor.LIGHT_GRAY;
                PdfFormField f = PdfFormField.CreateCheckBox(_pdfResultStamper.Writer);
                string fieldName = string.Format("InkSaver.Global.chk{0}", availableSizes[x].Replace(".", ""));
                //f.FieldName = fieldName;
                string js = string.Format("hideAll(event.target, '{0}');", availableSizes[x].Replace(".", ""));
                f.Action = PdfAction.JavaScript(js, _pdfResultStamper.Writer);
                tCell.CellEvent = new ChildFieldEvent(_pdfResultStamper.Writer, f, fieldName);
                globalRow[x + 1] = tCell;
                checkboxes.Add(f);
            }
            table.Rows.Add(new PdfPRow(globalRow));

            int status = 0;
            int pageNum = 1;

            for (int itemIndex = 0; itemIndex < Items.Count; itemIndex++)
            {
                tCell = new PdfPCell();
                Phrase p = new Phrase(Items[itemIndex].Title);
                tCell.AddElement(p);
                tCell.HorizontalAlignment = Element.ALIGN_LEFT;

                PdfPCell[] cells = new PdfPCell[availableSizes.Count + 1];
                cells[0] = tCell;

                for (int availCardSizeIndex = 0; availCardSizeIndex < availableSizes.Count; availCardSizeIndex++)
                {
                    if (Items[itemIndex].Category.CategorySizesInUse.Contains(availableSizes[availCardSizeIndex]))
                    {
                        string str = availableSizes[availCardSizeIndex];
                        tCell = new PdfPCell();
                        tCell.PaddingLeft = 10f;
                        tCell.PaddingRight = 10f;
                        cells[availCardSizeIndex + 1] = tCell;
                        cells[availCardSizeIndex].HorizontalAlignment = Element.ALIGN_CENTER;

                        PdfFormField f = PdfFormField.CreateCheckBox(_pdfResultStamper.Writer);
                        string fieldName = string.Format("InkSaver.chk{0}.{1}", availableSizes[availCardSizeIndex].Replace(".", ""), itemIndex + 1);
                        //f.FieldName = fieldName; <-- This causes the checkbox to be double-named (i.e. InkSaver.Global.chk0.InkSaver.Global.chk0
                        string js = string.Format("hideCardSize(event.target, {0}, '{1}');", itemIndex + 1, availableSizes[availCardSizeIndex]);
                        f.Action = PdfAction.JavaScript(js, _pdfResultStamper.Writer);
                        tCell.CellEvent = new ChildFieldEvent(_pdfResultStamper.Writer, f, fieldName);

                        checkboxes.Add(f);
                    }
                    else
                    {
                        //Add a blank cell
                        tCell = new PdfPCell();
                        cells[availCardSizeIndex + 1] = tCell;
                    }
                }
                //Test if the column text will fit

                table.Rows.Add(new PdfPRow(cells));

                canvas = _pdfResultStamper.GetUnderContent(pageNum);
                ColumnText ct2 = new ColumnText(canvas);
                ct2.AddElement(new PdfPTable(table));
                ct2.Alignment = Element.ALIGN_LEFT;
                ct2.SetSimpleColumn(targetPosition.Left, 0, targetPosition.Right, targetPosition.Top, 0, 0);
                status = ct2.Go(true);

                if ((status != ColumnText.NO_MORE_TEXT) || (itemIndex == (Items.Count - 1)))
                {
                    ColumnText ct3 = new ColumnText(canvas);
                    ct3.AddElement(table);
                    ct3.Alignment = Element.ALIGN_LEFT;
                    ct3.SetSimpleColumn(targetPosition.Left, 0, targetPosition.Right, targetPosition.Top, 0, 0);
                    ct3.Go();

                    foreach (PdfFormField f in checkboxes)
                    {
                        _pdfResultStamper.AddAnnotation(f, pageNum);
                    }
                    checkboxes.Clear();

                    if (itemIndex < (Items.Count - 1))
                    {
                        pageNum++;
                        _pdfResultStamper.InsertPage(pageNum, _pdfTemplateReader.GetPageSize(1));

                        table = new PdfPTable(availableSizes.Count + 1);
                        table.SetTotalWidth(columnWidth);
                        table.WidthPercentage = 100;
                        table.Rows.Add(new PdfPRow(this.GetTableHeaderRow(availableSizes)));
                    }
                }
            }
        }
        private PdfPCell[] GetTableHeaderRow(List<string> AvailableSizes)
        {
            PdfPCell[] sizeHeaders = new PdfPCell[AvailableSizes.Count + 1];
            Phrase devName = new Phrase("Device Name");
            PdfPCell deviceHeader = new PdfPCell(devName);
            deviceHeader.HorizontalAlignment = Element.ALIGN_CENTER;
            deviceHeader.BackgroundColor = BaseColor.GRAY;
            sizeHeaders[0] = deviceHeader;
            for (int x = 0; x < AvailableSizes.Count; x++)
            {
                PdfPCell hCell = new PdfPCell(new Phrase(AvailableSizes[x]));
                hCell.HorizontalAlignment = Element.ALIGN_CENTER;
                hCell.BackgroundColor = BaseColor.GRAY;
                sizeHeaders[x + 1] = hCell;
            }
            return sizeHeaders;
        }
        public void MergePdfs(string filePath, List<MemoryStream> pdfStreams)
        {
            //Create output stream            
            FileStream outStream = new FileStream(filePath, FileMode.Create);

            if (pdfStreams.Count > 0)
            {
                try
                {
                    int PriceCardCounter = 0;
                    //Create Main reader
                    PdfReader reader = new PdfReader(pdfStreams[0]);

                    //rename fields in the PDF.  This is required because PDF's cannot have more than one field with the same name
                    RenameFields(reader, PriceCardCounter++);

                    //Create main writer
                    PdfCopyFields Writer = new PdfCopyFields(outStream);

                    //Open document for writing
                    ////Add pages
                    Writer.AddDocument(reader);

                    //For each additional pdf after first combine them into main document
                    foreach (var PdfStream in pdfStreams.Skip(1))
                    {
                        PdfReader reader2 = new PdfReader(PdfStream);
                        //rename PDF fields
                        RenameFields(reader2, PriceCardCounter++);
                        // Add content
                        Writer.AddDocument(reader2);
                    }

                    Writer.Close();
                }
                finally
                {
                    foreach (var Strm in pdfStreams)
                    {
                        try { if (null != Strm) Strm.Dispose(); }
                        catch { }
                    }
                    outStream.Close();
                }
            }
        }
        private void RenameFields(PdfReader reader, int PriceCardID)
        {
            int tempPageNum = 1;
            //rename all fields
            foreach (string field in reader.AcroFields.Fields.Keys)
            {
                if (((reader.AcroFields.GetFieldType(field) == 1) || (reader.AcroFields.GetFieldType(field) == 2)) && (field.StartsWith("InkSaver")))
                {
                    //This is a InkSaver button, set the name so its subclassed
                    string classPath;
                    if (reader.AcroFields.GetFieldType(field) == 2)
                    {
                        classPath = field.Substring(0, field.LastIndexOf("."));
                        if (field.StartsWith("InkSaver.chk"))
                        {
                            int a = field.LastIndexOf(".");
                            string sub = field.Substring(a + 1, (field.Length - a - 1));
                            int pageNum = int.Parse(sub);
                            int realPageNum = pageNum + tempPageNum;//PostProcessing.Instance.CoverPageLength;
                            PriceCardID = realPageNum;
                        }
                    }
                    else
                    {
                        classPath = field.Substring(0, field.LastIndexOf("."));
                    }
                    string newID = classPath + ".page" + PriceCardID.ToString();
                    bool ret = reader.AcroFields.RenameField(field, newID);
                }
                else
                {
                    reader.AcroFields.RenameField(field, field + "_" + PriceCardID.ToString());// field + Guid.NewGuid().ToString("N"));
                }
            }
        }
    }
    public class ChildFieldEvent : IPdfPCellEvent
    {
        protected PdfWriter writer;
        protected PdfFormField parent;
        protected string checkBoxName;

        internal ChildFieldEvent(PdfWriter writer, PdfFormField parent, string CheckBoxName)
        {
            this.writer = writer;
            this.parent = parent;
            this.checkBoxName = CheckBoxName;
        }

        public void CellLayout(PdfPCell cell, Rectangle rect, PdfContentByte[] cb)
        {
            createCheckboxField(rect);
        }
        private void createCheckboxField(Rectangle rect)
        {
            RadioCheckField bt = new RadioCheckField(this.writer, rect, this.checkBoxName, "Yes");
            bt.CheckType = RadioCheckField.TYPE_SQUARE;
            bt.Checked = true;
            this.parent.AddKid(bt.CheckField);
        }
    }
}

回答1:

Maybe the author didn't explain it completely, but I think there really is a bug. Let's assume we have a PDF document with an AcroForm which contains two fields: name and owner.name. Now we want to rename owner.name to owner.name1 and name to name1. The first one will succeed, but the name in the field registry (AcroFields.fields) is now name1, not owner.name1 as expected (before it was owner.name). The seconds rename then fails because name1 already exists in the field registry. It affects only the registry, the field name in the resulting PDF document is correct.

Here the critical code snippet from Java iText:

// snippet from com.itextpdf.text.pdf.AcroFields.renameField
int idx2 = newName.lastIndexOf('.') + 1;
// cut the last part from the original name
newName = newName.substring(idx2);
PdfString ss = new PdfString(newName, PdfObject.TEXT_UNICODE);
// problem: only the last part will be registered, this must 
// be IMO the (original) whole name including the dots
fields.put(newName, item);


回答2:

There is no such thing as a field with a subclass. The dots in the field names refer to a hierarchy. For instance: if you have a field named person. This field can have children such as name and address. The fully qualified names of these child fields would then be person.name and person.address. Address can in turn have child fields, such as street, city and country, resulting in fully qualified names such as person.address.street, person.address.city and person.address.country.

You can rename fields, such as street, but you can't change the hierarchy, because the fully qualified name isn't present anywhere inside the PDF. So if you have a field with a fully qualified name person.address.street, you can only rename the street part. For instance: you can rename person.address.street into person.address.line1. You can not rename person.address.street into person.street as that would change the structure of the form.

What the docs say is that you can rename field names, but you can't change the field structure.

You say that something feels as a bug, but I don't see what is wrong. The docs indicate that ef is the only part of the fully qualified name that can be changed. Nowhere does it say that you can change ab.cd.ef into xy as that would mean that you have to rewrite a structure tree consisting of different PDF dictionaries instead of renaming the value of a key in a single dictionary.

This is, by the way, also explained in my book.

Addendum:

I've created a simple example named RenameField. It takes a form (subscribe.pdf) and creates a new form (subscribe_renamed.pdf). The difference? We have renamed "personal.loginname" into "personal.login". It would have been impossible to rename "personal.loginname" into "login" as that would require changing the hierarchy (just test it, you'll see it won't work).