1

I have a PDF with AcroFields. Some fields are multi-line fields. I have a requirement to populate AcroFields with given text and pad any remaining space in the field with an "*" (or a predefined character/string). I can add text using iText, but don't know how to calculate the right amount of filler to add.

Please could you suggest how I do this. Thank you.

The code I've written is:

 public string CreatePdf(IDictionary<FieldKey, string> dictionary, string template, string saveAs)
 {
      // Create new PDF from template
      using (PdfReader reader = new PdfReader(template))
      using (FileStream stream = new FileStream(saveAs, FileMode.Create, FileAccess.ReadWrite))
      using (PdfStamper stamper = new PdfStamper(reader, stream))
      {
          // Populate PDF fields from dictionary
          AcroFields formFields = stamper.AcroFields;
          foreach (KeyValuePair<FieldKey, string> dataField in dictionary)
          {
              switch (dataField.Key.FieldType)
              {
                  case FieldType.Text:              
                  string fieldValue = dataField.Value;
                  // Add filler if a filler is set
                  if (!String.IsNullOrWhiteSpace(dataField.Key.FillRight))
                  {
                      fieldValue = GetTextWithFiller(formFields, dataField.Key.FieldName, fieldValue, dataField.Key.FillRight);
                  }
                  // Text field
                  if (!formFields.SetField(dataField.Key.FieldName, fieldValue))
                      throw new InvalidDataException(String.Format("Invalid Template Field: {0} in Template: {1}", dataField.Key.FieldName, template));
                  break;
                  case FieldType.Image:
                      // Image field
                      PlaceImage(formFields, dataField);
                      break;
                  case FieldType.Barcode2Of5:
                      // 2 of 5 Barcode
                      PlaceBarcode2Of5(dataField.Value, stamper);
                      break;
                  case FieldType.Barcode128:
                      // 2 of 5 Barcode
                      PlaceBarcode128(dataField.Value, stamper);
                      break;
                  default:
                      throw new InvalidDataException(String.Format("Invalid data filed type : {0}", dataField.Key.FieldType));
              }
          }
          // Save PDF
          reader.RemoveUnusedObjects();
          stamper.FormFlattening = true;
          stamper.Close();
      }
      return saveAs;
  }

And the Method to get the Filler, which is not working as I expected:

private static string GetTextWithFiller(AcroFields fields, string fieldName, string text, string filler)
{        
    // Get the size of the rectangle that defines the field
    AcroFields.FieldPosition fieldPosition = fields.GetFieldPositions(fieldName)[0];
    Rectangle rect = fieldPosition.position;
    // Get field font
    PdfDictionary merged = fields.GetFieldItem(fieldName).GetMerged(0);
    TextField textField = new TextField(null, null, null);
    fields.DecodeGenericDictionary(merged, textField);
    Font fieldFont = new Font(textField.Font);
    Chunk whatWeHave = new Chunk(text, fieldFont);
    float textWidth = whatWeHave.GetWidthPoint();
    // See how far the text field is filled with give text
    float textEndPoint = rect.Left + textWidth;
    float rectBottom = rect.Bottom;
    float rectRight = rect.Right;
    float rectTop = rect.Top;
    // How many rows to fill
    int textRows = Convert.ToInt32(rect.Height / fieldFont.CalculatedSize);
    float totalCharactersWeCanFit = rect.Width * textRows;
    if (textWidth < totalCharactersWeCanFit)
        {
        // Get the width of filler character
        Chunk fillCharWidth = new Chunk(filler, fieldFont);
        // Available gap
        float gap = totalCharactersWeCanFit - textWidth;
        // How much filler required
        int fillAmount = Convert.ToInt32(gap / fillCharWidth.GetWidthPoint());
        // Fill with filler
        StringBuilder tempString = new StringBuilder();
        tempString.Append(text);
        for (int n = 0; n < fillAmount; ++n)
        {
            tempString.Append(filler);
        }
        text = tempString.ToString();
    }
    return text;
}
Bruno Lowagie
  • 75,994
  • 9
  • 109
  • 165
Chamila
  • 13
  • 4
  • There is no such thing as row count in a multi-line AcroField. The amount of data you can add depends on the font size defined for the field. For instance: if the font size is defined as 0, then the text will scale in such a way that all the text is rendered in the field. How are you currently filling out your form? Are you flattening it? The answer to that counter-question is very important if you want an answer. – Bruno Lowagie Nov 19 '15 at 15:41
  • Thank you Bruno, I'm iterating through all AcroFields and populating data based using a dictionary. Yes I'm flattening the PDF. The function I've written for this is: – Chamila Nov 19 '15 at 15:44
  • Sorry, removed the code and added in the question. – Chamila Nov 19 '15 at 15:58

1 Answers1

3

Consider a form with three multi-line text fields:

enter image description here

For the first field, we defined a font size 0 (which is also what you do); for the second field, we define a font 12; for the third field, we define a font 6.

Now let's fill out and flatten the form:

public void manipulatePdf(String src, String dest) throws DocumentException, IOException {
    PdfReader reader = new PdfReader(src);
    PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest));
    AcroFields form = stamper.getAcroFields();
    StringBuilder sb = new StringBuilder();
    for (String name : form.getFields().keySet()) {
        int n = getInformation(form, name);
        for (int i = 0; i < n; i++) {
            sb.append(" *");
        }
        String filler = sb.toString();
        form.setField(name, name + filler);
    }
    stamper.setFormFlattening(true);
    stamper.close();
    reader.close();
}

The result looks like this:

enter image description here

As you can see, the number of * we add as filler depends on the font size that was defined at the field level. If the font size is 0, the font will be adapted in such a way that the text always fits. In case the font has an actual value, we can more or less calculate the number of lines and "columns" we'll need:

public int getInformation(AcroFields form, String name) {
    form.getFieldItem(name);
    AcroFields.Item item = form.getFieldItem(name);
    PdfDictionary dict = item.getMerged(0);
    PdfString da = dict.getAsString(PdfName.DA);
    Object[] da_values = AcroFields.splitDAelements(da.toUnicodeString());
    if (da_values == null) {
        System.out.println("No default appearance");
    }
    BaseFont bf = null;
    String font = (String)da_values[AcroFields.DA_FONT];
    if (font != null) {
        PdfDictionary dr = dict.getAsDict(PdfName.DR);
        if (dr != null) {
            PdfDictionary fontDict = dr.getAsDict(PdfName.FONT);
            bf = BaseFont.createFont((PRIndirectReference)fontDict.get(new PdfName(font)));
        }
    }
    if (bf == null) {
        System.out.println("No BaseFont");
    }
    else {
        System.out.println("Basefont: " + bf.getPostscriptFontName());
        System.out.println("Size: " + da_values[AcroFields.DA_SIZE]);
        Float size = (Float)da_values[AcroFields.DA_SIZE];
        if (size == 0)
            return 1000;
        Rectangle rect = form.getFieldPositions(name).get(0).position;
        float factor = bf.getFontDescriptor(BaseFont.BBOXURY, 1) - bf.getFontDescriptor(BaseFont.BBOXLLY, 1);
        int rows = Math.round(rect.getHeight() / (size * factor) + 0.5f);
        int columns = Math.round(rect.getWidth() / bf.getWidthPoint(" *", size) + 0.5f);
        System.out.println("height: " + rect.getHeight() + "; width: " + rect.getWidth());
        System.out.println("rows: " + rows + "; columns: " + columns);
        return rows * columns;
    }
    return 1000;
}

First we get the font so that we can create a BaseFont object. Then we get the font size and using information stored in the BaseFont, we'll define the factor that will be used to calculate the leading (that's the space between two lines).

We also ask the field for its dimensions. We then calculate how many lines we can fit into the height (rows) and how many times we can fit the String " *" into the width (columns) of the field's rectangle. If we multiply columns and rows, we get an approximate value of how many times we have to add " *" to get the appropriate filler. It doesn't matter if we have too much filler: if a font is defined, all the text that doesn't fit will be dropped.

You can find the full example here: MultiLineFieldCount

We take the form multiline.pdf and the getInformation() method returns this information:

Basefont: Helvetica
Size: 0.0
Basefont: Helvetica
Size: 6.0
height: 86.0; width: 108.0
rows: 13; columns: 27
Basefont: Helvetica
Size: 12.0
height: 86.0; width: 107.999985
rows: 7; columns: 14

We can't tell much about the first field, because the font size is 0. That is also the case in your example. If the font is 0, your question is unanswerable. There is just no way you can calculate how many * fit into the field, because you don't know the font size that will be used to render *.

If the font size is 12, we can fit 7 rows and 14 colums (we see 7 rows and 13 columns in the screen shot). If the font size is 6, we can fit 14 rows and 27 columns (we see 14 rows and 26 columns in the screen shot).

The extra column is caused by the fact that we used the ceil() method. It's better to overestimate the number of columns than to underestimate it...

Bruno Lowagie
  • 75,994
  • 9
  • 109
  • 165