How to search and extract text across a whole PDF document on iOS

Searching text across all pages of a PDF on iOS

This guide shows how to search text in PDF on iOS across every page of a document with RadaeePDF, and how to reuse the same text objects to extract page text and highlight matches.

Applies to: RadaeePDF Classic and Master SDK for iOS. All snippets use selectors declared in PDFViewer/PDFLib/PDFObjc.h, which are the same in both editions.

Short answer: For each page, get an RDPDFPage with [doc page:i] and load its text with [page objsStart:false]. Then call [page find:key :matchCase :wholeWord]. The returned RDPDFFinder gives you the match count (count) and the first character index of each match (objsIndex:). Run the loop on a background queue, the way the demo’s RDExtendedSearch class does. The same character indexes work with objsString:: to extract text and with objsCharRect:: to get rectangles for highlighting.

How the text API is organized

Text search and text extraction in RadaeePDF both work on a page’s text objects. You call objsStart: once per page. After that, every character on the page has a 0-based index from 0 to objsCount - 1, and the other calls use those indexes:

  • -(void)objsStart:(bool)rtol loads the page’s text objects. Pass true only for right-to-left text. The demo passes GLOBAL.g_sel_rtol. The header notes that this needs a standard license.
  • -(int)objsCount returns the number of characters on the page.
  • -(NSString *)objsString:(int)from :(int)to returns the text for a range of character indexes.
  • -(void)objsCharRect:(int)index :(PDF_RECT *)rect returns a character’s box in PDF page coordinates.
  • -(RDPDFFinder *)find:(NSString *)key :(bool)match_case :(bool)whole_word starts a find session on the page. It returns nil when there are no matches.
  • -(RDPDFFinder *)find2:(NSString *)key :(bool)match_case :(bool)whole_word :(bool)skip_blanks does the same but treats line breaks as blanks. Use it when a phrase can wrap across lines.

RDPDFFinder has three methods: count, objsIndex: (the first character of match n) and objsEnd: (the end character of match n).

The KB sample Search in whole pdf file (iOS) uses the older names PDFDoc, PDFPage, PDFFinder and an argument-less objsStart. In the current Classic and Master repositories, these classes are RDPDFDoc, RDPDFPage and RDPDFFinder, and objsStart: takes a bool.

Search text in PDF on iOS on a background queue

On a long document, searching every page can take noticeable time, so keep it off the main thread. This is how the demo’s RDExtendedSearch works: it loops over the pages inside dispatch_async(dispatch_get_global_queue(...)) and checks a stop flag on every page so the search can be cancelled. The class below does the same thing in a self-contained way.

Objective-C (Classic and Master)

#import "PDFObjc.h"

@interface DocSearcher : NSObject
@property (atomic) BOOL cancelled;
- (void)search:(RDPDFDoc *)doc
           key:(NSString *)key
     matchCase:(bool)matchCase
     wholeWord:(bool)wholeWord
    completion:(void (^)(NSArray<NSDictionary *> *hits))completion;
@end

@implementation DocSearcher

- (void)search:(RDPDFDoc *)doc
           key:(NSString *)key
     matchCase:(bool)matchCase
     wholeWord:(bool)wholeWord
    completion:(void (^)(NSArray<NSDictionary *> *hits))completion
{
    self.cancelled = NO;
    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        NSMutableArray<NSDictionary *> *hits = [NSMutableArray array];
        int pageCount = [doc pageCount];
        for (int pageno = 0; pageno < pageCount && !self.cancelled; pageno++) {
            RDPDFPage *page = [doc page:pageno];
            if (!page) break;
            [page objsStart:false];
            RDPDFFinder *finder = [page find:key :matchCase :wholeWord];
            int count = [finder count];          // 0 when finder is nil
            int chars = [page objsCount];
            for (int i = 0; i < count; i++) {
                int first = [finder objsIndex:i];
                // Context snippet around the hit, like RDExtendedSearch builds it
                int from = MAX(first - 25, 0);
                int to = MIN(first + (int)key.length + 25, chars - 1);
                from = [page objsAlignWord:from :-1];
                to = [page objsAlignWord:to :1] + 1;
                NSString *context = [page objsString:from :to];
                [hits addObject:@{ @"page": @(pageno),
                                   @"char": @(first),
                                   @"context": context ?: @"" }];
            }
            page = nil;
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            completion(hits);
        });
    });
}

@end

Set cancelled = YES when the user changes the query or closes the document, and keep the RDPDFDoc open until the completion block has run.

Reusing the demo’s RDExtendedSearch

If you embed the demo’s PDFReader sources, you can call the singleton directly. It reads the case, whole-word and right-to-left options from GLOBAL.g_case_sensitive, GLOBAL.g_match_whole_word and GLOBAL.g_sel_rtol. Each result is an RDSearchResult with stringResult and a 1-based page. Both blocks are called on the background queue, which is why SearchResultViewController hops to the main queue:

#import "RDExtendedSearch.h"

[[RDExtendedSearch sharedInstance] searchText:@"invoice" inDoc:doc progress:^(NSMutableArray *occurrences, NSMutableArray *total) {
    dispatch_async(dispatch_get_main_queue(), ^{
        for (RDSearchResult *r in occurrences) {
            NSLog(@"page %d: %@", r.page, r.stringResult);
        }
    });
} finish:^{
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"search finished");
    });
}];

// To stop a running search:
[[RDExtendedSearch sharedInstance] clearSearch];

Extract the full text of a page

To get a page’s full text, call objsStart: and then read the range from 0 to objsCount. This is what the Cordova plugin’s extractTextFromPage: does on iOS. To extract a whole document, run the same calls in a loop on a background queue:

- (NSString *)textOfDocument:(RDPDFDoc *)doc
{
    NSMutableString *all = [NSMutableString string];
    int pageCount = [doc pageCount];
    for (int pageno = 0; pageno < pageCount; pageno++) {
        RDPDFPage *page = [doc page:pageno];
        [page objsStart:false];
        int count = [page objsCount];
        if (count > 0) {
            NSString *text = [page objsString:0 :count];
            if (text) [all appendFormat:@"%@\n", text];
        }
        page = nil;
    }
    return all;
}

Get match rectangles for highlighting

A match can span several characters and even wrap onto a second line. The viewer’s RDVFinder gets the box of each character with objsCharRect:: and merges characters that share a line into a single rectangle. You can do the same to draw your own overlays:

// One rectangle per line covered by the match, in PDF page coordinates.
static NSArray<NSValue *> *MatchRects(RDPDFPage *page, int first, int length)
{
    NSMutableArray<NSValue *> *rects = [NSMutableArray array];
    PDF_RECT word, r;
    [page objsCharRect:first :&word];
    for (int i = first + 1; i < first + length; i++) {
        [page objsCharRect:i :&r];
        float gap = (r.bottom - r.top) / 2;
        if (word.top == r.top && word.bottom == r.bottom &&
            word.right + gap > r.left && word.left - gap < r.right) {
            if (word.left > r.left) word.left = r.left;
            if (word.right < r.right) word.right = r.right;
        } else {
            [rects addObject:[NSValue valueWithBytes:&word objCType:@encode(PDF_RECT)]];
            word = r;
        }
    }
    [rects addObject:[NSValue valueWithBytes:&word objCType:@encode(PDF_RECT)]];
    return rects;
}

Call this right after find: while the page’s text objects are still loaded, with first = [finder objsIndex:i] and length = key.length. That is the same end the viewer uses. PDF coordinates have the y axis pointing up, so the viewer maps a rectangle’s bottom to the top of the on-screen box. If you place overlays yourself at a zoom factor scale, use x = rect.left * scale and y = ([doc pageHeight:pageno] - rect.bottom) * scale.

To make the highlight a permanent annotation in the file instead of a temporary overlay, call [page addAnnotMarkup:first :first + length - 1 :0 :0xFFFFFF00] after objsStart:. Type 0 is Highlight, and the color is the ARGB value the demo uses by default for highlights. Then save the document. The header says this needs a professional or premium license.

If you build your own viewer on the demo’s PDFLayoutView, you get on-screen highlighting without any extra code. vFindStart::: starts a search, vFind: moves to the next (1) or previous (-1) match and paints it, and vFindEnd clears the highlight. PDFReaderCtrl uses these calls internally.

Swift equivalent

The repositories ship Objective-C sources only. Once you add PDFObjc.h to your bridging header, Swift imports the unlabeled selectors with _ parameters, int as Int32 and unannotated object returns as optionals:

func search(_ doc: RDPDFDoc, key: String,
            completion: @escaping ([(page: Int32, char: Int32)]) -> Void) {
    DispatchQueue.global(qos: .userInitiated).async {
        var hits: [(page: Int32, char: Int32)] = []
        for pageno in 0..<doc.pageCount() {
            guard let page = doc.page(pageno) else { break }
            page.objsStart(false)
            guard let finder = page.find(key, false, false) else { continue }
            for i in 0..<finder.count() {
                hits.append((page: pageno, char: finder.objsIndex(i)))
            }
        }
        DispatchQueue.main.async { completion(hits) }
    }
}

In Swift, check the result of find for nil with guard let. In Objective-C, sending count to nil simply returns 0.

Related questions

How do I make the search case-sensitive or whole-word only?

Pass true as the second or third argument of find:::, which are match_case and whole_word. In the demo viewer, these options come from GLOBAL.g_case_sensitive and GLOBAL.g_match_whole_word, and both RDExtendedSearch and PDFLayoutView read them.

Why does a phrase that wraps onto the next line not match?

find::: matches the characters exactly as they appear on the page, including the line break. Use find2:::: with skip_blanks set to true, which treats line breaks as blank characters.

Do I need to call objsStart before every find?

Call it once per RDPDFPage object, before find:, objsCount, objsString:: or objsCharRect::. The RDPDFFinder header notes that objsIndex: expects objsStart to have been called on the page.

Can I search a document that is open in PDFReaderCtrl?

Yes. Get the document with [reader getDoc] and run the background loop on it. To jump to a result, call [reader PDFGoto:pageno] with the 0-based page number.

Related on radaeepdf.com

Where to go next

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

How to open a PDF from a URL on iOS without downloading the whole file first

Streaming a PDF from a URL to an iOS app without a full download

To open PDF from URL on iOS without waiting for the whole file, RadaeePDF reads the remote document through a stream that downloads only the blocks the parser needs.

Applies to: RadaeePDF Classic and Master SDK for iOS. Both demo projects include PDFHttpStream in PDFViewer/PDFLib/ and use the same RDPDFDoc and PDFReaderCtrl selectors.

Short answer: Create a PDFHttpStream and call [stream open:url :cacheFile]. Then open an RDPDFDoc with [doc openStream:stream :password] and pass it to the demo viewer with [reader setDoc:doc :YES]. The stream downloads 4 KB (Classic) or 8 KB (Master) blocks with HTTP Range requests only when the parser reads them, so the first page can appear before the rest of the file arrives. To make this efficient, call [RDPDFDoc setOpenFlag:3] before opening a linearized PDF.

A note on the older KB sample

The KB article How to open pdf from url on iOS? passes the stream to RDPDFViewController with PDFOpenStream::. Neither that class nor that selector is in the current Classic or Master iOS demo projects. In the current sources, the document object opens the stream (-[RDPDFDoc openStream::]) and the viewer is PDFReaderCtrl, which you load from PDFReaderCtrl.storyboard and give the document with setDoc:. The RadaeePDF Cordova plugin follows exactly this path for http:// and https:// URLs.

How PDFHttpStream works

PDFHttpStream is an Objective-C class that implements the SDK’s PDFStream protocol (writeable, read::, write::, position, length, seek:). Its source ships with the demo, so you can read or adapt it. Here is what it does, based on PDFHttpStream.m:

  1. open:: starts a GET with NSURLSession and blocks the calling thread until the response headers arrive.
  2. If the server answers with a status of 400 or higher, or doesn’t send a usable Content-Length, open:: returns NO.
  3. Otherwise it creates the cache file at the path you passed and fills it with zeros up to the full document size.
  4. Each time the PDF parser calls read::, the stream checks which blocks are missing and downloads them with a Range: bytes=start-end request. It writes them into the cache file and marks them as present, so a block is never fetched twice while the stream is open.
  5. close, which dealloc also calls, cancels the network task, closes the cache file and deletes it with unlink.

This tells you what the server and your app need:

  • The server must support byte-range requests and return Content-Length. Most static file servers and CDNs do both.
  • HTTPS works without extra code. The stream uses the standard NSURLSession default configuration, and the demo header’s test URL (testUrlPath) is an https:// link. App Transport Security applies as it does to any other request.
  • The cache isn’t persistent. It lives only as long as the stream, and it takes the full file size on disk as soon as open:: succeeds. To keep a file for offline use, download it separately and open it with open::.
  • The stream is read-only. writeable returns false and write:: returns 0, so open the viewer in read-only mode.

Open PDF from URL on iOS (Classic and Master)

Because open:: blocks and the first openStream:: call reads over the network, do both on a background queue. Switch back to the main queue to show the viewer:

#import "PDFHttpStream.h"
#import "PDFReaderCtrl.h"

@interface RemoteViewer : UIViewController
@property (strong, nonatomic) PDFHttpStream *httpStream; // keep it alive while the document is open
@end

@implementation RemoteViewer

- (void)openRemotePDF:(NSString *)url password:(NSString *)password
{
    NSString *cacheFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"remote-cache.pdf"];

    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
        PDFHttpStream *stream = [[PDFHttpStream alloc] init];
        if (![stream open:url :cacheFile]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self showError:@"Server unreachable, HTTP error, or no Content-Length."];
            });
            return;
        }

        [RDPDFDoc setOpenFlag:3];          // load linearized hint table (bit 1) + same page size (bit 2)
        RDPDFDoc *doc = [[RDPDFDoc alloc] init];
        int err = [doc openStream:stream :password];
        int linearized = (err == err_ok) ? [doc getLinearizedStatus] : 0;

        dispatch_async(dispatch_get_main_queue(), ^{
            switch (err) {
                case err_ok: {
                    NSLog(@"linearized status: %d", linearized);
                    self.httpStream = stream;
                    PDFReaderCtrl *reader = [[UIStoryboard storyboardWithName:@"PDFReaderCtrl" bundle:nil]
                                             instantiateViewControllerWithIdentifier:@"rdpdfreader"];
                    [reader setDoc:doc :YES];   // read-only: PDFHttpStream is not writeable
                    [self presentViewController:reader animated:YES completion:nil];
                    break;
                }
                case err_password: [self showError:@"Password required."]; break;
                case err_encrypt:  [self showError:@"Unknown encryption."]; break;
                case err_bad_file: [self showError:@"Damaged or invalid PDF."]; break;
                default:           [self showError:@"Cannot open the document."]; break;
            }
        });
    });
}

- (void)showError:(NSString *)msg
{
    UIAlertController *a = [UIAlertController alertControllerWithTitle:@"Error" message:msg
                                                        preferredStyle:UIAlertControllerStyleAlert];
    [a addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:a animated:YES completion:nil];
}

@end

openStream:: returns a PDF_ERR value: err_ok, err_open, err_password, err_encrypt or err_bad_file. The demo’s PDFNavCtrl checks the same values when it opens local files. Pass nil as the password for unprotected files. Store the stream in a property, because releasing it runs close, which stops downloads and deletes the cache file. If you open more than one remote file at a time, give each stream its own cache path.

In the Master edition, PDFHttpStream.h imports the bundled ASIHttpRequest headers. Copy that folder along with PDFLib if you move the class into your own project.

Linearization and setOpenFlag

A linearized (“fast web view”) PDF stores the first page and a hint table at the start of the file, so a reader can show page one after downloading only the beginning. RadaeePDF uses the hint table only when you ask for it. +[RDPDFDoc setOpenFlag:] is a static, global setting that affects every subsequent open call:

  • flag & 1 loads the linearized hint table.
  • flag & 2 treats all pages as the same size as the first page, so the SDK reads only the first page object when it opens the document. This bit has an effect only when bit 1 is also set.

The Cordova plugin calls [RDPDFDoc setOpenFlag:3] before openStream::. Use bit 2 only when you know the pages share one size, because otherwise the viewer lays out every page at the first page’s size. Since the flag is global, set it again before you open other documents that need different behavior. The C-level equivalents are Document_setOpenFlag in Classic (PDFIOS.h) and PDF_Document_setOpenFlag in Master (RDPDF.h).

After opening, -[RDPDFDoc getLinearizedStatus] returns one of these values:

  • 0: no linearized header was loaded. You also get this when the open flag is 0.
  • 1: a header is present, but its entry check failed.
  • 2: the header is valid, but the hint table is damaged.
  • 3: the linearized header loaded successfully.

If you get 0 or 1 for a file you serve often, linearize it on the server. Otherwise the parser has to fetch the cross-reference data at the end of the file before it can render anything.

App Transport Security

Serve your PDFs over HTTPS and you don’t need to change anything. If you must load plain HTTP from your own host, add an exception for that domain instead of disabling ATS globally:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>files.example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

The KB article suggests NSAllowsArbitraryLoads, which works but turns off ATS for every connection your app makes.

Related questions

Why does open:: return NO for a URL that works in Safari?

open:: needs a response with status below 400 and a positive expectedContentLength. Servers that stream with chunked encoding, or that send an HTML login page or a redirect to one, fail this check. Make sure the URL returns the PDF bytes directly with a Content-Length header.

Is the cache file reused the next time the app starts?

No. The stream creates it fresh in open:: and deletes it in close. If users need offline access, download the file with NSURLSession into your app’s Documents or Caches directory and open the local copy with [doc open:path :password].

Can users add annotations to a streamed document?

Not through PDFHttpStream, because it reports writeable as false and ignores writes. That’s why the example opens the viewer with setDoc:doc :YES. To edit, download the file first, or write your own PDFStream implementation that supports writing.

Can I open a PDF already held in memory instead?

Yes. -[RDPDFDoc openMem:::] takes a byte buffer, its length and a password. The buffer must stay allocated until the document is closed, and the KB notes that a document opened this way is read-only.

Related on radaeepdf.com

Where to go next

How to export and import PDF annotations between devices on iOS

Syncing PDF annotations between devices with XFDF

There are two ways to export PDF annotations on iOS with RadaeePDF and import them on another page or device: one annotation at a time, or the whole document as XFDF.

Applies to: Per-annotation export and import work in the RadaeePDF Classic and Master SDK for iOS. Document-level XFDF works only in the Master SDK for iOS. Both editions declare these selectors in PDFViewer/PDFLib/PDFObjc.h.

Short answer: You can serialize annotations at two levels:

  • One annotation at a time (Classic and Master): call -[RDPDFAnnot export::] to write the annotation into a byte buffer, save its rectangle with getRect:, and recreate it on any page with -[RDPDFPage importAnnot:::].
  • Every annotation in the document (Master only): call -[RDPDFDoc exportXFDF:] to get a single XML string, and apply it on another device with -[RDPDFDoc importXFDF:].

In both cases, your app sends the data over the network, and the SDK only does the serialization. Then save the target document.

Two ways to export PDF annotations on iOS

  • Per-annotation buffers are a compact binary format that only RadaeePDF can read. Use them to copy an annotation between pages or documents, or to sync single annotations as users create them. The headers say you need a premium license for both export:: and importAnnot:::.
  • XFDF is the standard XML format for PDF annotations. A single string holds the whole annotation set, so it suits backend storage and exchange with other tools. In the iOS headers, only the Master edition’s RDPDFDoc declares exportXFDF: and importXFDF:. These two methods wrap the C functions PDF_Document_exportXFDF and PDF_Document_importXFDF in RDPDF.h.

Export and import a single annotation (Classic and Master)

This follows the KB sample How to Export/Import annotations? (iOS). The KB uses the older PDFPage and PDFAnnot names and an argument-less objsStart. The current headers use RDPDFPage, RDPDFAnnot and objsStart:(bool). According to the KB, this works for ink, line, rectangle, ellipse and note annotations.

Export to a transferable payload

#import "PDFObjc.h"

// Serialize one annotation into a JSON-friendly dictionary.
- (NSDictionary *)exportAnnotAtIndex:(int)index page:(int)pageno doc:(RDPDFDoc *)doc
{
    RDPDFPage *page = [doc page:pageno];
    [page objsStart:false];                       // annotAtIndex: needs objsStart or a render
    if (index < 0 || index >= [page annotCount]) return nil;

    RDPDFAnnot *annot = [page annotAtIndex:index];
    PDF_RECT rect;
    [annot getRect:&rect];                        // position in PDF page coordinates

    unsigned char buf[4096];                      // size it for your densest ink strokes
    if ([annot export:buf :sizeof(buf)] < 0) {
        return nil;                               // -1 means the export failed
    }
    NSData *data = [NSData dataWithBytes:buf length:sizeof(buf)];
    return @{ @"page":   @(pageno),
              @"rect":   @[ @(rect.left), @(rect.top), @(rect.right), @(rect.bottom) ],
              @"data":   [data base64EncodedStringWithOptions:0] };
}

The header documents only that export:: returns -1 on failure. It doesn’t say the return value is a byte count. To stay on the safe side, the code sends the whole buffer, as the KB sample does. Choose a buffer size large enough for your annotations, as the KB suggests.

Import on another page or device

- (BOOL)importAnnot:(NSDictionary *)payload intoDoc:(RDPDFDoc *)doc
{
    int pageno = [payload[@"page"] intValue];
    NSArray<NSNumber *> *r = payload[@"rect"];
    NSData *data = [[NSData alloc] initWithBase64EncodedString:payload[@"data"] options:0];
    if (!data || r.count != 4) return NO;

    PDF_RECT rect;
    rect.left   = r[0].floatValue;
    rect.top    = r[1].floatValue;
    rect.right  = r[2].floatValue;
    rect.bottom = r[3].floatValue;

    RDPDFPage *page = [doc page:pageno];
    if (![page importAnnot:&rect :(const unsigned char *)data.bytes :(int)data.length]) {
        return NO;
    }
    return [doc save];                             // persist into the PDF file
}

importAnnot::: places the annotation at the rectangle you pass. To duplicate an annotation at another spot, change the rectangle. To send every annotation on a page, loop from 0 to annotCount - 1 and export each one.

Export and import all annotations as XFDF (Master only)

#import "PDFObjc.h"   // Master edition

// Device A: export
NSString *xfdf = [doc exportXFDF:nil];   // href: reference to the source PDF file, may be nil
if (xfdf.length == 0) {
    NSLog(@"XFDF export failed");
}

// Device B: import into the same PDF, then save
RDPDFDoc *target = [[RDPDFDoc alloc] init];
if ([target open:localPath :nil] == err_ok && [target canSave]) {
    if ([target importXFDF:xfdf]) {
        [target save];
    }
}

exportXFDF: returns an XML string with an encoding header, as the XFDF format defines. importXFDF: expects that same kind of string and returns true or false. The href argument becomes the reference to the source PDF inside the XFDF. Pass your file name or document ID if your backend needs one, or nil.

The header doesn’t say whether importXFDF: replaces annotations that are already on the page or adds to them. If you keep the full annotation set on the server, import it into a clean copy of the original PDF so the same annotation is never applied twice.

Sending the payload to your backend

The SDK has no networking layer. Use NSURLSession or any client you already have:

NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://api.example.com/docs/42/annotations"]];
req.HTTPMethod = @"PUT";
[req setValue:@"application/vnd.adobe.xfdf" forHTTPHeaderField:@"Content-Type"];
NSData *body = [xfdf dataUsingEncoding:NSUTF8StringEncoding];

[[[NSURLSession sharedSession] uploadTaskWithRequest:req fromData:body
    completionHandler:^(NSData *resp, NSURLResponse *response, NSError *error) {
        if (error) NSLog(@"upload failed: %@", error);
    }] resume];

For per-annotation sync, send the dictionary from exportAnnotAtIndex:page:doc: as JSON with NSJSONSerialization. Load annotations on the receiving device before you call setDoc: on PDFReaderCtrl. That way the viewer renders the merged result from the first frame.

Things to check

  • Licensing. The headers say you need a premium license for export::, importAnnot::: and flatAnnots. save always returns false with no license or a standard license.
  • The document must be writable. Check [doc canSave] before importing. A document opened from memory (KB 82) or through the read-only PDFHttpStream can’t be saved.
  • Coordinates are page-relative. Both approaches store positions in PDF page coordinates. Import into the same PDF, or at least into pages of the same size, or the annotations end up in the wrong place.
  • Keep the Master library current for XFDF. The Master iOS release notes list several XFDF fixes: v1.3.9 (“Fix issue with exporting/importing XFDF data”), v1.3.10 (“Fix coordinate error while importing XFDF data for some documents”) and v1.3.11 (“Fix XFDF data importing issue”). v1.3.12 also fixes ink annotations that could be lost in some documents. If imported annotations shift or go missing, update the library before you debug your own code.

XFDF in other RadaeePDF editions

XFDF is a Master-level feature across the product line. The same ExportXFDF and ImportXFDF pair is declared in the Master Android Document.java, in the Master Windows (UWP) UWPPDF.h and in the Java binding of RadaeePDF Server. An XFDF string exported on iOS can therefore be imported by an Android app or a server-side job built on those editions, and the other way around. The Classic iOS header doesn’t declare these methods.

Related questions

How do I make imported annotations permanent?

Call [page flatAnnots] on each page. It removes the annotations and draws them as normal page content that users can’t edit. PDFReaderCtrl also offers flatAnnots and +flatAnnotAtPage:doc:. KB 80 notes that you need a premium license.

Can I export only ink annotations?

Yes, with the per-annotation API. Loop over annotCount, check each RDPDFAnnot with its type method, and export only the types you want. XFDF export always covers the whole document.

Does XFDF include form field values?

The headers describe exportXFDF: only as an annotation export. For form data, RDPDFDoc has a separate exportForm method that returns an XML string. The header says it needs a premium license.

Can I use this from Swift?

Yes, through a bridging header that imports PDFObjc.h. The unlabeled selectors come into Swift with _ parameters. For example, doc.exportXFDF(nil), doc.importXFDF(xfdf) and page.importAnnot(&rect, bytes, len).

Related on radaeepdf.com

Where to go next

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