Encrypting PDFs with AES-256: A Cross-Platform Guide

AES-256 PDF encryption with user and owner passwords

This guide explains how to encrypt PDF with AES-256 using RadaeePDF on Android, iOS, Windows and Server, with the same EncryptAs method on every platform.

Applies to: RadaeePDF Classic and Master SDK for Android, Master SDK for iOS (and the matching Classic iOS API), Master SDK for Windows UWP, and RadaeePDF SDK Server (Java and CLI). Encryption requires a Premium license.

Key takeaways:

  • To encrypt PDF with AES-256 in RadaeePDF, call EncryptAs with method 3 (Acrobat X, V=5, R=6).
  • The user password opens the file; the owner password unlocks every permission.
  • The file ID must be 32 bytes: generate it with a cryptographic random generator.
  • Encryption requires a Premium license on every platform.

Password-protecting a PDF looks like a single operation, but you actually have to decide four things: which passwords to set, which permissions to grant, which cipher to use, and which file ID to write. RadaeePDF gives you one method for all of this on every platform, EncryptAs. This guide compares how it behaves on Android, iOS, Windows and Server, using the API documentation in the official GitHub repositories. If you only target Android, the companion article Encrypt PDF with AES-256 on Android covers that platform in more depth.

How to encrypt PDF with AES-256 using EncryptAs

EncryptAs never changes the open document in place. It writes an encrypted copy to a new path and leaves the source file alone. All editions use the same six parameters:

  1. dst: the path of the encrypted copy.
  2. upswd: the user password.
  3. opswd: the owner password.
  4. perm: the permission flags.
  5. method: selects the cipher (see below).
  6. id: the file ID. It must be 32 bytes long. In the Android, Server and Windows sources it is described as “divided to 2 array in native library, as each 16 bytes”.

The Android, Server and Windows doc comments say the method “require[s] premium license”, and the iOS headers say the same. Without a Premium activation, the call returns false.

User password vs owner password

When you open a protected file, the SDK tries the password you pass as the user password first and then as the owner password. The Open doc comment puts it this way: “first time, SDK try password as user password, and then try password as owner password.”

  • User password. This is the password you share with readers. According to KB article #11, it is “sufficient to open the file with applied permission mask set”.
  • Owner password. It opens the file “without any restriction from the permission mask” (same KB article).

The Android and Server Java docs say both values “can be null”. The command-line tool in RadaeePDF Server refuses to encrypt with an empty owner password, though. Always setting a strong owner password is the safest habit, because without one the permission mask has nothing to fall back on.

Permission bits

The perm parameter uses the permission flags from PDF Reference 1.7. The doc comment for EncryptAs in the Android, Server and Windows sources lists:

  • bits 1-2: reserved
  • bit 3 (0x4): print
  • bit 4 (0x8): modify
  • bit 5 (0x10): extract text or image
  • others: see the PDF reference

A set bit grants the permission. The rdsec command-line tool in RadaeePDF Server passes -1 (every bit set, so everything is allowed). Starting from -1 and clearing the bits you want to block is the simplest way to keep the reserved bits set, as the PDF reference requires. For example, ~(0x8 | 0x10) allows printing but blocks modification and text or image extraction.

To read the flags of a file you opened, call Document.GetPermission() on Android and Server Java (the docs say it needs “a professional or premium license”), or read the Permission property of PDFDoc on Windows.

AES-256 vs AES-128 by platform

According to KB #11, RadaeePDF writes only AES-encrypted output. What changes from platform to platform is how the key length is chosen:

  • Android (Classic and Master), Server Java, Windows UWP. The doc comment reads: “set 3 means using AES 256bits encrypt(Acrobat X), V=5 and R = 6 mode, others AES with V=4 and R=4 mode.” So method = 3 gives AES-256, and any other value gives AES-128. The Server Java Document.java carries exactly the same comment and signature as the Android one, so AES-256 is documented on the Server too.
  • iOS (Classic and Master). In PDFObjc.h and the C header RDPDF.h, the method parameter of encryptAs is documented only as “reserved”. The iOS headers don’t say which value selects AES-256, and the published iOS example (KB #75) passes 4. If AES-256 is a hard requirement on iOS, check the output file’s encryption dictionary (V=5, R=6) in a PDF inspector, or ask RadaeePDF support before you rely on it.
  • Server CLI. rdsec -enc calls the native encryptAs with method 0 and permissions -1, which according to the doc comment above means AES-128. If you need AES-256 on a server, use the Java binding with method = 3.

The original KB #11 sample also uses method = 4 (AES-128). For new code on Android, Windows and Server, pass 3.

Generating the 32-byte file ID

The id must be exactly 32 bytes. Avoid fixed strings such as “123456789abcdefghijklmnopqrstuvw”: every file you encrypt would carry the same ID. Use a cryptographic random generator instead: java.security.SecureRandom on Android and Server, SecRandomCopyBytes on iOS, CryptographicBuffer.GenerateRandom on UWP.

The Server rdsec tool takes a different approach: it reuses the file’s existing ID when there is one and falls back to random bytes when there isn’t. In Java you can do the same with Document.GetID(0) and Document.GetID(1), which return the first and last 16 bytes, or null “if no id for this document”.

Android (Master or Classic), Java

This works unchanged on Master and Classic Android, which share the same EncryptAs signature. Call Global.Init(context) (in com.radaee.comm) with a Premium key before any document call.

import com.radaee.pdf.Document;

import java.security.SecureRandom;

public final class PdfEncryptor {

    /**
     * Writes an AES-256 encrypted copy of srcPath to dstPath.
     * Global.Init(context) must already have activated a Premium license.
     */
    public static boolean encryptAes256(String srcPath, String srcPassword,
                                        String dstPath, String userPassword,
                                        String ownerPassword) {
        Document doc = new Document();
        int err = doc.Open(srcPath, srcPassword);
        if (err != 0) {
            // -1: password needed, -2: unknown encryption,
            // -3: damaged file, -10: access denied or invalid path
            return false;
        }
        try {
            byte[] fileId = new byte[32];
            new SecureRandom().nextBytes(fileId);

            int perm = ~(0x8 | 0x10); // allow print, block modify and extract
            int method = 3;           // AES-256 (V=5, R=6)

            return doc.EncryptAs(dstPath, userPassword, ownerPassword,
                                 perm, method, fileId);
        } finally {
            doc.Close();
        }
    }
}

Server, Java

RadaeePDF SDK Server ships the same com.radaee.pdf.Document class through JNI, so the method above runs unchanged. The only difference is initialization: Server calls Global.Init(serial), which loads the RDDJni native library and activates the key. The demo’s Global.java loads fonts and cmaps from /root/res on Linux, so adjust those paths for your deployment.

import com.radaee.comm.Global;
import com.radaee.pdf.Document;

public class EncryptTool {
    public static void main(String[] args) {
        if (!Global.Init(args[0])) {          // args[0] = Premium serial
            System.err.println("License not activated");
            return;
        }
        boolean ok = PdfEncryptor.encryptAes256(
                args[1], null, args[2], "userPwd", "ownerPwd");
        System.out.println(ok ? "Encrypted" : "Encryption failed");
    }
}

iOS (Master), Objective-C

On iOS the document class is RDPDFDoc, declared in PDFObjc.h. It has no explicit close method: the native document is closed in dealloc, so release your reference when you are done. The license must be active first. The demo projects do this with [RDVGlobal Init].

#import <Security/Security.h>
#import "PDFObjc.h"

BOOL EncryptPDFCopy(NSString *srcPath, NSString *dstPath,
                    NSString *userPwd, NSString *ownerPwd)
{
    RDPDFDoc *doc = [[RDPDFDoc alloc] init];
    int err = [doc open:srcPath :nil];
    if (err != 0) {
        NSLog(@"open failed: %d", err); // -1 needs password
        return NO;
    }

    unsigned char fid[32];
    if (SecRandomCopyBytes(kSecRandomDefault, sizeof(fid), fid) != errSecSuccess) {
        return NO;
    }

    int perm = ~(0x8 | 0x10); // allow print, block modify and extract
    // "method" is documented as reserved in the iOS headers.
    bool ok = [doc encryptAs:dstPath :userPwd :ownerPwd :perm :0 :fid];
    doc = nil; // dealloc closes the native document
    return ok;
}

Classic iOS declares the same encryptAs selector. Its RadaeePDFPlugin wrapper also exposes encryptDocAs:userPwd:ownerPwd:permission:method:idString:, which converts an NSString to a C string as the ID. If you use that wrapper, pass exactly 32 ASCII characters. Swift projects call the Objective-C API through a bridging header.

Windows UWP, C#

The Windows SDK wraps the native engine in the RDDLib.pdf.PDFDoc C++/CX class, declared in RDPDF.h. Its EncryptAs(String dst, String User_pswd, String Owner_pswd, int perm, int method, Array<BYTE> fid) returns false straight away when fid is shorter than 32 bytes. Activate the license first. The demo’s PDFGlobal.init() calls RDGlobal.ActiveLicense(...).

using System.IO;
using System.Threading.Tasks;
using RDDLib.comm;
using RDDLib.pdf;
using Windows.Security.Cryptography;
using Windows.Storage;
using Windows.Storage.Streams;

public static class PdfEncryptor
{
    public static async Task<bool> EncryptAes256Async(StorageFile source,
        string userPassword, string ownerPassword)
    {
        using (IRandomAccessStream stream = await source.OpenAsync(FileAccessMode.Read))
        {
            PDFDoc doc = new PDFDoc();
            RD_ERROR err = doc.Open(stream, "");
            if (err != RD_ERROR.err_ok)
                return false; // err_password, err_encrypt, err_bad_file...

            try
            {
                byte[] fid;
                CryptographicBuffer.CopyToByteArray(
                    CryptographicBuffer.GenerateRandom(32), out fid);

                string dst = Path.Combine(
                    ApplicationData.Current.LocalFolder.Path, "encrypted.pdf");
                int perm = ~(0x8 | 0x10);

                return doc.EncryptAs(dst, userPassword, ownerPassword,
                                     perm, 3, fid);
            }
            finally
            {
                doc.Close();
            }
        }
    }
}

The wrapper reads both password strings directly, so pass non-null values. C++/CX callers use the same PDFDoc methods.

Opening and checking an encrypted file

Every edition reports password problems through the Open return value:

  • 0: opened.
  • -1: a password is needed (or the one you gave is wrong).
  • -2: unknown encryption.
  • -3: damaged or invalid file.
  • -10: access denied or invalid path.

On Windows, Open returns the RD_ERROR enum instead (err_ok, err_password, err_encrypt, err_bad_file, and so on), and the demo prompts for a password when it gets err_password. Once the file is open, IsEncrypted() (Java), isEncrypted (iOS) and the IsEncrypted property (Windows) tell you whether the file carries a security handler.

Removing a password

To decrypt, open the file with a valid password and save a copy without the security handler:

  • Android and Server Java: doc.SaveAs(dstPath, true). The second parameter is documented as “remove security info?” and needs a professional or premium license.
  • iOS: [doc saveAs:dstPath :YES], documented as “save as the document to another file. it remove any security information”.
  • Server CLI: rdsec -dec <file> <pswd> <file1>. Internally it calls the same save-as call with the remove-security flag set.
./rdsec -enc dec.pdf "" enc1.pdf 123456 ""
./rdsec -dec enc1.pdf 123456 plain.pdf

The Windows UWP PDFDoc.SaveAs(String path) has no remove-security parameter, and the Windows header documents no other decryption call.

Common pitfalls

  • No Premium license. EncryptAs returns false. Check your activation result before you look for a bug anywhere else.
  • A file ID that is not 32 bytes. Windows rejects it outright. On the other platforms the length is a documented requirement, so don’t count on a shorter ID working.
  • Assuming method = 3 means AES-256 on iOS. The iOS headers mark method as reserved. Verify the output instead.
  • Writing to an unwritable path. rdsec reports “please check saved path is writeable”, and the API simply returns false.

Frequently asked questions

Which value of the method parameter enables AES-256?

Pass 3. The SDK then uses AES-256 in Acrobat X mode (V=5, R=6). On Android and Server any other value selects AES-128 (V=4, R=4). On iOS the parameter is documented as reserved, so check the output file.

What is the difference between the user password and the owner password?

The user password is required to open the document. The owner password also opens it and grants full permissions, overriding the permission bits set at encryption time.

Do I need a special license to encrypt PDFs with RadaeePDF?

Yes. The SDK documentation marks EncryptAs as a Premium license feature on Android, iOS, Windows and Server.

Related on radaeepdf.com

Where to go next

Encrypt PDF with AES-256 on Android

Encrypting a PDF with AES-256 on an Android phone

Here is how to encrypt PDF on Android with AES-256 using the RadaeePDF Master SDK, and how to check that the encrypted copy really is protected.

Applies to: RadaeePDF Master SDK for Android

Short answer: Call Document.EncryptAs() with the method parameter set to 3 to enable AES-256 encryption (Acrobat X standard). You must also provide a 32-byte ID array and a valid Premium license for the SDK.

How to encrypt PDF on Android with AES-256

Developers often assume that PDF encryption is a single, monolithic operation. In the RadaeePDF SDK, however, the EncryptAs method gives you granular control over the cryptographic standard used. This is critical because the PDF specification (ISO 32000) defines different security handlers.

The method parameter in EncryptAs dictates which security handler is applied:

  • method = 3: Uses AES-256 encryption. This corresponds to the Acrobat X standard (V=5, R=6). This is the strongest encryption available in the PDF spec and is what you want for modern security compliance.
  • method = 4: Uses AES-128 encryption (V=4, R=4). This is the older standard.

If you pass 4 when you intend to use AES-256, your document will be protected, but with a weaker key size. Conversely, if you pass 3, the SDK will generate the appropriate encryption dictionary in the PDF header to signal AES-256 support to other PDF readers.

Additionally, the SDK requires a Premium license to perform any encryption. If you are using the free or standard edition, the EncryptAs call will fail silently or return false. This is a common source of confusion for developers testing in a development environment without a valid license key.

The fix

To encrypt a PDF with AES-256, you need to prepare a 32-byte ID array. This ID acts as a seed for the encryption process. The SDK documentation specifies that this ID is split into two 16-byte arrays internally by the native library.

Here is the complete implementation in Java for the RadaeePDF Master SDK for Android:

import com.radaee.pdf.Document;

public class PdfEncryptionExample {
    public static boolean encryptPdfWithAes256(String inputPath, String outputPath) {
        // 1. Prepare the 32-byte ID (encryption seed)
        // This string is exactly 32 characters long.
        // In a production app, you might generate this randomly or use a fixed identifier.
        String idString = "123456789abcdefghijklmnopqrstuvw";
        byte[] id = idString.getBytes();

        // 2. Initialize the Document object
        Document doc = new Document();

        // 3. Open the source PDF
        // The second parameter is the password if the source is already encrypted.
        // Pass null if the source is not encrypted.
        int openResult = doc.Open(inputPath, null);
        if (openResult != 0) {
            System.err.println("Failed to open PDF: " + openResult);
            return false;
        }

        // 4. Define permissions
        // Bit 3 (0x4): Allow printing
        // Bit 4 (0x8): Allow modifying content
        // Bit 5 (0x10): Allow extracting text/images
        // Here we set perm = 4, which allows ONLY printing.
        // If you want to allow all standard permissions, you would OR these flags.
        int perm = 0x4; // Print only

        // 5. Encrypt and save
        // Parameters:
        // dst: Output file path
        // upswd: User password (can be null for no user password)
        // opswd: Owner password (can be null for no owner password)
        // perm: Permission flags
        // method: 3 for AES-256 (Acrobat X), 4 for AES-128
        // id: 32-byte array
        boolean success = doc.EncryptAs(
            outputPath,
            "userPassword123", // User password
            "ownerPassword456", // Owner password
            perm,
            3, // AES-256
            id
        );

        // 6. Close the document
        doc.Close();

        return success;
    }
}

Verifying the Encryption

After calling EncryptAs, you can verify that the document is encrypted by opening the new file and checking the IsEncrypted method:

Document doc = new Document();
doc.Open("/sdcard/test_encrypted.pdf", "userPassword123");
if (doc.IsEncrypted()) {
    System.out.println("Document is successfully encrypted.");
} else {
    System.out.println("Document is NOT encrypted.");
}
doc.Close();

Common pitfalls

  1. ID Length: The id parameter must be exactly 32 bytes. If you pass a shorter or longer array, the encryption will fail. The example above uses a 32-character string, which converts to 32 bytes in UTF-8.
  2. License Requirement: EncryptAs requires a Premium license. If you are using the free version, the method will return false. Ensure your app has a valid license initialized.
  3. Method Confusion: Do not confuse method=3 (AES-256) with method=4 (AES-128). If you are targeting modern security standards, always use 3.
  4. Permissions: The perm parameter is a bitmask. If you set it to 0, no permissions are granted (the user can only view). If you want to allow printing and editing, you would use 0x4 | 0x8 (which equals 12).

Related questions

How do I enable AES-256 specifically instead of standard AES?

You enable AES-256 by passing the integer 3 to the method parameter of the Document.EncryptAs method. This value corresponds to the Acrobat X security handler (V=5, R=6), which uses AES-256. If you pass 4, the SDK will use the older AES-128 standard (V=4, R=4). There is no separate flag for AES-256; the method parameter is the sole determinant of the encryption strength.

What are the valid permission flags for restricting printing or editing?

The permission flags are defined in the PDF Reference 1.7 specification and are passed as an integer bitmask to the perm parameter. The key bits are:

  • Bit 3 (0x4): Allow printing the document.
  • Bit 4 (0x8): Allow modifying document contents.
  • Bit 5 (0x10): Allow extracting text or images.

To restrict printing, you simply do not include bit 3 in your mask. For example, to allow only editing but not printing, you would use perm = 0x8. To allow all three, use perm = 0x4 | 0x8 | 0x10 (which equals 28).

Is encryption available in the free version of the SDK?

No. The EncryptAs method is only available in the Premium edition of the RadaeePDF SDK. If you are using the free or standard license, the method will not function correctly and will likely return false. You must purchase a Premium license and initialize it in your application before calling encryption methods.

How can I verify that a PDF has been successfully encrypted?

You can verify encryption by opening the encrypted file with Document.Open() and then calling the IsEncrypted() method. If the method returns true, the document is encrypted.

Related on radaeepdf.com

See also

For more details on the EncryptAs method and license requirements, refer to the official documentation in the RadaeePDF-Master-Android repository and the Encrypt a PDF KB article.

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