Convert PDF Pages to Images with the RadaeePDF SDK

Converting PDF pages to PNG and JPEG images on server and mobile

This guide shows how to convert PDF to image files, PNG or JPEG, with RadaeePDF on a server, on Android and on iOS.

Applies to: RadaeePDF Server SDK (CLI, Java, C#), RadaeePDF Master for Android, and RadaeePDF Master for iOS.

Key takeaways:

  • To convert PDF to image with RadaeePDF, render each page into a pixel buffer with a matrix, then encode it as PNG or JPEG.
  • On a server you can do it without code using the rdpage CLI, or from Java and C#.
  • Use a scale of dpi / 72: about 2.0 for previews and 4.17 for 300 dpi OCR images.
  • Transparent backgrounds need PNG output; JPEG has no alpha channel.

Turning PDF pages into images is one of the most requested tasks in the RadaeePDF knowledge base. Typical uses are document previews in a web portal, thumbnails in a file list, page images for an OCR or indexing step, and archives that must be readable without a PDF viewer.

Many developers know RadaeePDF as a mobile viewer, but the same native engine also ships as a Server SDK for Windows and Linux. It runs headless and is meant for conversions, editing and content manipulation on a server: rendering pages to bitmaps, extracting text, encrypting, merging, splitting and stamping documents. This article starts on the server, then shows the equivalent code for Android and iOS.

How to convert PDF to image: the rendering model

All editions follow the same model:

  1. Open a Document and get a Page object for a 0-based page number.
  2. Allocate a pixel buffer: a BufferedImage or int[] array (Server Java), an RDDIB (Server C#), a Bitmap (Android) or an RDPDFDIB (iOS).
  3. Build a Matrix that maps PDF coordinates to pixels and render into the buffer.
  4. Encode the pixels as PNG or JPEG, then close the page and the document.

The matrix needs a little attention. PDF coordinates start at the bottom-left corner, while bitmaps start at the top-left. As the Matrix source explains, the usual values are scale for x, -scale for y, and pageHeight * scale as the y offset. A scale of 1.0 renders at the page’s native size, 72 dpi, so 2.0 gives 144 dpi.

Choosing a resolution

Because page sizes are expressed in points (1/72 inch), the scale for a target resolution is simply dpi / 72. A few practical reference points:

  • Thumbnails for a file list or page strip: a scale between 0.25 and 0.5, or a fixed longest side such as 200 pixels.
  • On-screen previews in a web page: 1.5 to 2.0 (108 to 144 dpi) is usually sharp enough on high-density displays.
  • OCR or print-quality archives: scale up to 300 dpi (about 4.17), and keep the output in PNG to avoid compression artifacts around text.

Remember that a 32-bit buffer costs 4 bytes per pixel. An A4 page (595 x 842 points) rendered at scale 2.0 is 1190 x 1684 pixels, about 8 MB before encoding. A server that converts several documents in parallel needs that amount for every page being rendered at the same time.

Server side: convert without writing code

The rdpage tool in the Server SDK’s CLI folder renders pages directly. Use a dpi of 72 for 100% and 144 for 200%:

./rdpage -out test.pdf "" png 108 0/test001.png 1/test002.png

# Export every page as JPEG at 200% into the "test" folder (0000.jpg, 0001.jpg, ...)
./rdpage -exp test.pdf "" jpg 144 test

The second argument is the document password ("" when there is none). Keep libRDDJni.so (or RDDJni.dll), cmaps.dat and umaps.dat in the same folder as the executable. If the tool reports “license not activated”, run ./rdinfo -r to get a request code and put the key you receive in key.txt next to the tools.

Server side: Java

The Java bindings in java/PDFJniDemo render into a standard java.awt.image.BufferedImage through Page.RenderToBuf(BufferedImage bimg, int back_clr, Matrix mat), so the result can go straight to javax.imageio.ImageIO. The method below is modeled on the JSP demo images.jsp. Its size parameter, the length of the longest side in pixels, comes from the knowledge base converter. The method also keeps the JSP demo’s 4096-pixel cap.

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

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

public class PdfToImages {

    /**
     * @param size        longest side in pixels, 0 = native size (72 dpi)
     * @param png         true for PNG, false for JPEG
     * @param transparent PNG only: keep the page background transparent
     * @return 0 on success, or the Document.Open error code
     *         (-1 password, -2 unknown encryption, -3 damaged, -10 access denied)
     */
    public static int convert(String pdfPath, String password, File outDir,
                              int size, boolean png, boolean transparent) throws IOException {
        Document doc = new Document();
        int result = doc.Open(pdfPath, password);
        if (result != 0) return result;
        outDir.mkdirs();
        try {
            int count = doc.GetPageCount();
            for (int i = 0; i < count; i++) {
                Page page = doc.GetPage(i);
                if (page == null) break; // license not activated
                float pw = doc.GetPageWidth(i);
                float ph = doc.GetPageHeight(i);
                float scale = (size > 0) ? size / Math.max(pw, ph) : 1.0f;
                if (pw * scale > 4096.0f) scale = 4096.0f / pw;
                if (ph * scale > 4096.0f) scale = 4096.0f / ph;
                int iw = Math.max(1, (int) (pw * scale + 0.5f));
                int ih = Math.max(1, (int) (ph * scale + 0.5f));

                BufferedImage bimg = new BufferedImage(iw, ih,
                        png ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
                Matrix mat = new Matrix(scale, -scale, 0, ph * scale);
                int background = (png && transparent) ? 0x00000000 : 0xFFFFFFFF;
                boolean ok = page.RenderToBuf(bimg, background, mat);
                mat.Destroy();
                page.Close();
                if (ok) {
                    String name = String.format("page_%04d.%s", i, png ? "png" : "jpg");
                    ImageIO.write(bimg, png ? "png" : "jpg", new File(outDir, name));
                }
            }
        } finally {
            doc.Close();
        }
        return 0;
    }

    public static void main(String[] args) throws IOException {
        if (!Global.Init("YOUR-LICENSE-KEY")) {
            System.err.println("License not activated");
        }
        System.out.println(convert(args[0], "", new File(args[1]), 1200, true, false));
    }
}

A few notes on this code:

  • Global.Init(String serial) loads the native library and activates the license. On Linux it reads the CMaps and fonts from /root/res/ by default, so adjust those paths in Global.java for your deployment.
  • back_clr is the page background color in ARGB format. The SDK demos pass -1 (opaque white). The Server source documents it only as “the background color”, so a zero-alpha value such as 0x00000000 should leave the background transparent: test it with your SDK version, and remember that transparency only survives if you save as PNG.
  • JPEG has no alpha channel, so the JPEG path renders into a TYPE_INT_RGB image.
  • If you prefer raw pixels, RenderToBuf(int[] data, int w, int h, int back_clr, Matrix mat) fills an array of 0xAARRGGBB values.

Server side: C#

The C# wrappers (RDDLib.comm, RDDLib.pdf) render into an RDDIB buffer, which can encode itself with SavePNG(path) or SaveJPG(path, quality):

using System;
using RDDLib.comm;
using RDDLib.pdf;

public static class PdfToImages
{
    public static int Convert(string pdfPath, string outDir, float scale)
    {
        PDFDoc doc = new PDFDoc();
        int err = doc.Open(pdfPath, "");
        if (err != 0) return err;

        int count = doc.GetPageCount();
        for (int i = 0; i < count; i++)
        {
            PDFPage page = doc.GetPage(i);
            if (page == null) break;
            float pw = doc.GetPageWidth(i);
            float ph = doc.GetPageHeight(i);
            RDDIB dib = new RDDIB((int)(pw * scale), (int)(ph * scale));
            RDMatrix mat = new RDMatrix(scale, -scale, 0, ph * scale);
            page.RenderPrepare(dib);
            if (page.Render(dib, mat, 2))
                dib.SaveJPG(System.IO.Path.Combine(outDir, $"page_{i:D4}.jpg"), 90);
            page.Close();
        }
        doc.Close();
        return 0;
    }
}

Call RDGlobal.Init() and RDGlobal.Active(key) (which returns 3 on success) once before converting, as the Windows demo does.

Android (Master)

On Android, the Master library renders into an android.graphics.Bitmap with Page.RenderToBmp(Bitmap, Matrix). The knowledge base article on converting PDFs to images notes that the logic is the same as on the server. Here it is with the current Master API, where Matrix lives in com.radaee.comm:

import android.content.Context;
import android.graphics.Bitmap;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

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

public final class PdfToImages {

    public static int convert(Context ctx, String pdfPath, String password,
                              File outDir, int size, boolean transparent) {
        Global.Init(ctx); // license key is read from Global.mKey
        Document doc = new Document();
        int result = doc.Open(pdfPath, password);
        if (result != 0) return result;
        outDir.mkdirs();
        int count = doc.GetPageCount();
        for (int i = 0; i < count; i++) {
            Page page = doc.GetPage(i);
            if (page == null) continue;
            float pw = doc.GetPageWidth(i);
            float ph = doc.GetPageHeight(i);
            float ratio = (size > 0) ? size / Math.max(pw, ph) : 1.0f;
            Bitmap bmp = Bitmap.createBitmap((int) (pw * ratio), (int) (ph * ratio),
                    Bitmap.Config.ARGB_8888);
            if (transparent) {
                bmp.eraseColor(0x00000000);       // fully transparent
                page.RenderPrepare((Bitmap) null); // reset page status, no erase
            } else {
                page.RenderPrepare(bmp);           // erases the bitmap to white
            }
            Matrix mat = new Matrix(ratio, -ratio, 0, ph * ratio);
            boolean ok = page.RenderToBmp(bmp, mat);
            mat.Destroy();
            page.Close();
            if (ok) {
                File out = new File(outDir, "page_" + i + (transparent ? ".png" : ".jpg"));
                try (FileOutputStream fos = new FileOutputStream(out)) {
                    bmp.compress(transparent ? Bitmap.CompressFormat.PNG
                            : Bitmap.CompressFormat.JPEG, 90, fos);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            bmp.recycle();
        }
        doc.Close();
        return 0;
    }
}

Transparent backgrounds on Android

Transparent backgrounds are among the most-read rendering topics in the knowledge base. The approach above follows the Android article: erase the bitmap to 0x00000000 and render without RenderPrepare(bmp), because that overload erases the bitmap to white. Older articles also call BMP.MulAlpha() afterwards. In the current Master source that method is marked deprecated since 3.12beta2, because the improved color blending no longer needs it.

Form fields are drawn with a translucent highlight (default 0x200040FF). The knowledge base converter turns it off before exporting. In Master for Android, call Global.setAnnotTransparency(0) after Global.Init.

iOS (Master)

The iOS Master library exposes Objective-C classes in PDFObjc.h: RDPDFDoc, RDPDFPage, RDPDFDIB and RDPDFMatrix. Initialize once with [RDVGlobal Init], which activates the serial set in RDVGlobal.m. Then render into a DIB and wrap its pixels in a CGImage, using the same bitmap flags as the sample app’s file browser:

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

// Renders every page to PNG. Returns 0 or the open error code.
static int PDFPagesToPNG(NSString *path, NSString *password, NSString *outDir, float scale)
{
    RDPDFDoc *doc = [[RDPDFDoc alloc] init];
    int err = [doc open:path :password];
    if (err != 0) return err;
    [[NSFileManager defaultManager] createDirectoryAtPath:outDir
                              withIntermediateDirectories:YES attributes:nil error:nil];
    int count = [doc pageCount];
    for (int i = 0; i < count; i++) {
        @autoreleasepool {
            float pw = [doc pageWidth:i];
            float ph = [doc pageHeight:i];
            int iw = (int)(pw * scale);
            int ih = (int)(ph * scale);
            RDPDFPage *page = [doc page:i];
            if (page == nil) continue;
            RDPDFDIB *dib = [[RDPDFDIB alloc] init:iw :ih];
            RDPDFMatrix *mat = [[RDPDFMatrix alloc] init:scale :-scale :0 :ph * scale];
            [page renderPrepare:dib]; // erases the DIB to white
            if (![page render:dib :mat :2]) continue;

            CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, [dib data], iw * ih * 4, NULL);
            CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
            CGImageRef img = CGImageCreate(iw, ih, 8, 32, iw << 2, cs,
                kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst,
                provider, NULL, false, kCGRenderingIntentDefault);
            CGColorSpaceRelease(cs);
            CGDataProviderRelease(provider);
            NSData *png = UIImagePNGRepresentation([UIImage imageWithCGImage:img]);
            CGImageRelease(img);
            NSString *file = [outDir stringByAppendingPathComponent:
                              [NSString stringWithFormat:@"page_%04d.png", i]];
            [png writeToFile:file atomically:YES];
        }
    }
    return 0; // ARC releases the page, DIB and document objects
}

The @autoreleasepool block frees each page’s image before the next page is rendered, which matters for long documents.

Keeping memory under control

  • Cap the image size. Pixel memory grows with the square of the scale. A 4096-pixel limit, as in the JSP demo, or a fixed longest side keeps memory use predictable.
  • Release native objects promptly. Call Matrix.Destroy() and Page.Close() after every page and Document.Close() at the end. On Android, recycle() each bitmap after it has been saved.
  • Use embedded thumbnails when they exist. Page.RenderThumb(Bitmap) on Android returns true only if the page carries a thumbnail image, so fall back to a normal render when it returns false.
  • Only need the first page? The Android sample’s grid view calls Document.SetOpenFlag(3) before opening and GetPage0() to fetch the first page fast. The flag is global and makes the SDK assume every page has the size of the first one, so the sample restores SetOpenFlag(1) right after opening.

Common pitfalls

  • Upside-down output: the y scale must be negative, with pageHeight * scale as the y offset.
  • Black or missing background in JPEG files: JPEG cannot store alpha, so render on white or keep transparent output in PNG.
  • Only a few pages rendered: without an activated license, GetPage returns null after the first pages. Check the result of the init call.
  • Wrong fonts on Linux servers: make sure the resource paths in Global.Init (Java) or RDGlobal.Init (C#) point to the folder that holds cmaps.dat, umaps.dat and the font files.

Frequently asked questions

How do I convert a PDF page to PNG on a server?

Run the rdpage tool with the -exp option and a dpi value, or render the page in Java with Page.RenderToBuf into a BufferedImage and save it with ImageIO.

Which resolution should I use when converting PDF to image?

Page sizes are in points, so the scale is the target dpi divided by 72. A scale between 1.5 and 2.0 suits on-screen previews, while OCR or print-quality archives need about 300 dpi.

Can I render a page with a transparent background?

Yes on Android: erase the bitmap to fully transparent, render without RenderPrepare and save as PNG. On the server, pass a background color with zero alpha and test the result with your SDK version.

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