How to Fill and Flatten PDF Form Fields with the RadaeePDF SDK

Filling and flattening PDF form fields with an SDK

This guide shows how to fill PDF form fields from code with RadaeePDF on Android and iOS, and how to flatten them so the values become part of the page.

Applies to: RadaeePDF Classic and Master SDK for Android (Java) and iOS (Objective-C). The form-filling and flattening APIs shown here exist with the same names in both editions.

Short answer: Open the document, call ObjsStart() on each page, loop over GetAnnot(i) and use GetFieldType() to tell text, button and choice fields apart. Set values with SetEditText(), SetCheckValue(), SetRadio(), SetComboItem() or SetListSels(), then save. To flatten, call Page.FlatAnnots() on every page and save the result to a new file with Document.SaveAs(). According to the SDK source documentation, all field methods and FlatAnnots() need a premium license.

How to fill PDF form fields: the annotation model

RadaeePDF does not keep a separate form object model. Each AcroForm widget is an annotation on its page, so you reach fields through the page’s annotation list. On Android, Page.Annotation.GetFieldType() returns:

  • 0: unknown (not a form field)
  • 1: button field (push button, checkbox or radio button)
  • 2: text field
  • 3: choice field (combo box or list box)
  • 4: signature field

You can then narrow the type down with a few more calls. GetCheckStatus() returns 0/1 for an unchecked/checked checkbox, 2/3 for an unchecked/checked radio button, and -1 if the annotation is not a check control. GetComboItemCount() and GetListItemCount() return -1 when the field is not a combo or list. GetFieldFullName() gives the qualified name (for example Form1.EditBox1), which is the most reliable key for matching your data. The Javadoc says GetAnnotCount() and GetAnnot() work after ObjsStart(), Render() or RenderToBmp(), so in a headless job you call ObjsStart() first.

Filling and flattening on Android (Java)

The sample below targets the Classic Android SDK (com.radaee.pdf.Global). In the Master Android SDK, Global lives in com.radaee.comm, so change only that import. Document, Page and Page.Annotation have the same signatures in both editions.

import android.content.ContextWrapper;
import android.util.Log;

import com.radaee.pdf.Document;
import com.radaee.pdf.Global; // Master SDK: import com.radaee.comm.Global;
import com.radaee.pdf.Page;

import java.util.Map;

public final class FormFiller {
    private static final String TAG = "FormFiller";

    /**
     * values maps a field full name to a value:
     * text fields: the text; checkboxes: "true"/"false";
     * radio buttons: the export value to select; combo/list: the display text.
     */
    public static boolean fillAndFlatten(ContextWrapper ctx, String srcPath,
                                         String filledPath, String flatPath,
                                         Map<String, String> values) {
        if (!Global.Init(ctx)) {
            Log.e(TAG, "License activation failed");
            return false;
        }
        Document doc = new Document();
        int err = doc.Open(srcPath, null);
        if (err != 0) {
            Log.e(TAG, "Open failed, error " + err); // -1 password, -3 damaged, -10 access denied
            return false;
        }
        try {
            if (!doc.CanSave()) {
                Log.e(TAG, "Document cannot be modified with the active license");
                return false;
            }
            int pageCount = doc.GetPageCount();
            for (int p = 0; p < pageCount; p++) {
                Page page = doc.GetPage(p);
                if (page == null) continue;
                try {
                    page.ObjsStart();
                    int annotCount = page.GetAnnotCount();
                    for (int i = 0; i < annotCount; i++) {
                        Page.Annotation annot = page.GetAnnot(i);
                        if (annot == null) continue;
                        String name = annot.GetFieldFullName();
                        if (name == null || !values.containsKey(name)) continue;
                        applyValue(annot, values.get(name));
                    }
                } finally {
                    page.Close();
                }
            }
            if (!doc.SaveAs(filledPath, false)) {
                Log.e(TAG, "Saving filled copy failed");
                return false;
            }

            // Flatten: every annotation on every page becomes static content
            for (int p = 0; p < pageCount; p++) {
                Page page = doc.GetPage(p);
                if (page == null) continue;
                try {
                    page.ObjsStart();
                    if (!page.FlatAnnots()) Log.w(TAG, "FlatAnnots failed on page " + p);
                } finally {
                    page.Close();
                }
            }
            return doc.SaveAs(flatPath, false);
        } finally {
            doc.Close();
        }
    }

    private static void applyValue(Page.Annotation annot, String value) {
        switch (annot.GetFieldType()) {
            case 2: // text field
                annot.SetEditText(value);
                break;
            case 1: { // button field
                int status = annot.GetCheckStatus();
                if (status == 0 || status == 1) {          // checkbox
                    annot.SetCheckValue(Boolean.parseBoolean(value));
                } else if (status == 2 || status == 3) {   // radio widget
                    if (value.equals(annot.GetCheckValue())) annot.SetRadio();
                }
                break;
            }
            case 3: { // choice field
                int comboCount = annot.GetComboItemCount();
                if (comboCount >= 0) {
                    for (int k = 0; k < comboCount; k++) {
                        if (value.equals(annot.GetComboItem(k))) {
                            annot.SetComboItem(k);
                            break;
                        }
                    }
                    break;
                }
                int listCount = annot.GetListItemCount();
                for (int k = 0; k < listCount; k++) {
                    if (value.equals(annot.GetListItem(k))) {
                        annot.SetListSels(new int[]{k});
                        break;
                    }
                }
                break;
            }
            default: // 0 unknown, 4 signature: not filled here
                break;
        }
    }
}

Some notes on the API, taken from the Javadoc in Page.java and Document.java:

  • SetEditText(String) does not validate the field’s format. Check the value yourself, or use the overload SetEditText(String value, String formatted) to set a value and a separate display string.
  • SetRadio() checks one radio widget and clears the others in its group. Every widget in a group has the same field name, so the sample compares each widget’s export value (GetCheckValue()) to choose the right one.
  • SetListSels(int[]) takes zero-based indexes. If IsListMultiSel() returns true, you can pass more than one.
  • Each Annotation is valid only until its Page is closed. Always pair GetPage() with Page.Close(), and close the Document last. The SDK logs a memory-leak warning if a document is closed while its pages are still open.
  • Document.Save() writes back to the opened file. SaveAs(path, rem_sec) writes a copy, and rem_sec controls whether security information is removed.

If you only want to flatten some widgets, the Annotation class has a per-annotation method. It is flateFromPage() in Classic and FlateFromPage() in Master. It invalidates that annotation’s handle, so read the annotation count again afterwards.

Filling and flattening on iOS (Objective-C)

The iOS headers (PDFObjc.h, in both the Classic and Master iOS repositories) declare the same operations on RDPDFDoc, RDPDFPage and RDPDFAnnot. The selectors differ slightly from Java: combo selection is setComboSel:, the list setter takes a C array plus a count, and per-page flattening is flatAnnots.

#import "PDFObjc.h"

static BOOL FillAndFlatten(NSString *src, NSString *filled, NSString *flat,
                           NSDictionary<NSString *, NSString *> *values)
{
    RDPDFDoc *doc = [[RDPDFDoc alloc] init];
    int err = [doc open:src :nil];
    if (err != 0) { NSLog(@"open failed: %d", err); return NO; }
    if (![doc canSave]) { NSLog(@"document cannot be saved"); return NO; }

    int pageCount = [doc pageCount];
    for (int p = 0; p < pageCount; p++) {
        RDPDFPage *page = [doc page:p];
        if (!page) continue;
        [page objsStart:false];
        int count = [page annotCount];
        for (int i = 0; i < count; i++) {
            RDPDFAnnot *annot = [page annotAtIndex:i];
            NSString *name = [annot fieldFullName];
            NSString *value = name ? values[name] : nil;
            if (!value) continue;
            switch ([annot fieldType]) {
                case 2: [annot setEditText:value]; break;
                case 1: {
                    int st = [annot getCheckStatus];
                    if (st == 0 || st == 1) [annot setCheckValue:[value boolValue]];
                    break;
                }
                case 3: {
                    int n = [annot getComboItemCount];
                    for (int k = 0; k < n; k++) {
                        if ([value isEqualToString:[annot getComboItem:k]]) {
                            [annot setComboSel:k];
                            break;
                        }
                    }
                    n = [annot getListItemCount];
                    for (int k = 0; k < n; k++) {
                        if ([value isEqualToString:[annot getListItem:k]]) {
                            int sel[1] = { k };
                            [annot setListSels:sel :1];
                            break;
                        }
                    }
                    break;
                }
                default: break;
            }
        }
    }
    if (![doc saveAs:filled :false]) return NO;

    for (int p = 0; p < pageCount; p++) {
        RDPDFPage *page = [doc page:p];
        [page objsStart:false];
        [page flatAnnots];
    }
    return [doc saveAs:flat :false];
}

There are no explicit close calls on iOS. In the Classic iOS PDFObjc.m, RDPDFPage and RDPDFDoc release their native handles in dealloc, so ARC closes them once your references go out of scope. To select a radio button, call setRadio on the widget you want. It clears the other widgets in the group.

Common pitfalls

  • License tier. The source documentation marks every field getter and setter, FlatAnnots() and ExportForm() as requiring a premium license. Reading the annotation list (GetAnnotCount(), GetAnnot()) requires professional or premium. On Android, the native active() call that Global.Init() uses returns 1 (standard), 2 (professional) or 3 (premium). If you activate as professional, you can list annotations but cannot fill fields. CanSave() always returns false when no license is active.
  • Flattening is not field-only. FlatAnnots() turns every annotation on the page into page content, including markup such as highlights and notes. Use the per-annotation flatten method if you need to keep some of them.
  • Flatten a copy. Flattening can’t be undone. Save the filled, still-editable version first (as in both samples), then flatten and use SaveAs() to write to a different path.
  • Refresh the view. The Javadoc says to re-render the page after setting a value. If a viewer is showing the document, refresh it after you make changes in code.

Related questions

Can I create new form fields, not just fill existing ones?

Yes, in the Master Android SDK. Its Page class has AddFieldEditbox, AddFieldCheck, AddFieldRadio, AddFieldCombo, AddFieldList, AddFieldButton and AddFieldSign. The Classic Android Page.java has no equivalent methods. The Master Javadoc marks these methods as requiring a premium license.

Is there a JSON shortcut for filling forms on Android?

Yes. com.radaee.util.RadaeePDFManager has getJsonFormFields(), getJsonFormFieldsAtPage(int pageno) and setFormFieldsWithJSON(String json). According to the KB article, they are meant to be called after the viewer has rendered the document. For headless batch jobs, use the annotation loop shown above.

How do I export the filled values without flattening?

Document.ExportForm() (Android) and exportForm (iOS) return the form data as an XML string. Both require a premium license.

Why does GetAnnotCount() return 0 on a page that clearly has fields?

The page’s objects have to be loaded first. Call ObjsStart() (Android) or objsStart: (iOS), or render the page, before you enumerate annotations. Also check the active license: the annotation list needs at least a professional license.

Related on radaeepdf.com

Where to go next