问题
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).
来源:https://stackoverflow.com/questions/21344750/itextsharp-renamefield-bug