Building a Server-Side PDF Processing Pipeline with RadaeePDF

Server-side PDF SDK pipeline processing documents in batch

This guide shows how to build a PDF processing pipeline with a server-side PDF SDK: the RadaeePDF Server engine driven from shell scripts, Java and C# on Windows and Linux.

Applies to: RadaeePDF Server SDK (Java bindings, C# bindings and CLI tools) on Windows and Linux.

Key takeaways:

  • RadaeePDF Server is a server-side PDF SDK: a native C++ engine for Windows and Linux that runs headless.
  • You can drive it from shell scripts with the CLI tools, from Java through JNI, or from C#.
  • The same engine renders pages to images, extracts text, encrypts with AES-256 and merges documents.
  • One server license covers any number of concurrent processes on that machine.

Backend workflows often need PDF processing without any user interface: turning uploaded documents into page images, extracting text for search, protecting files before delivery, or assembling several documents into one. The RadaeePDF Server SDK is built for that job. It is compiled from native C++ code, runs on Windows and Linux, and exposes the same engine through Java, C# and a set of command-line tools.

This guide walks through a pipeline in three layers: shell scripts that call the CLI tools, Java code built on the classes in the PDFJniDemo project, and C# code built on the wrappers shipped in the C# folder. Every call shown here comes from the public RadaeePDF-Server repository.

What the server-side PDF SDK package contains

The repository is organized by integration type:

  • Binaries/: the native engine, RDDJni.dll for Windows (x64 and x86) and libRDDJni.so for Ubuntu 22.04 (x64 and ARM64) and Ubuntu 24.04 (x64).
  • CLI/: ready-to-run executables rdinfo, rdsec, rdpage and rdcvt, plus their C++ sources.
  • java/: the PDFJniDemo Eclipse project (packages com.radaee.comm, com.radaee.pdf, com.radaee.util) and a JSPDemo web sample.
  • C#/: wrapper sources for Windows and Linux (RDDCom.cs, RDDPDF.cs, RDDDOCX.cs) and two demo projects.
  • res/: resource files, including cmaps.dat and umaps.dat, which are required for correct rendering and text extraction.

Choosing the right layer

The three layers share one engine, so the choice is about how the pipeline is operated, not about features:

  • CLI tools fit scheduled jobs, shell-based workers and quick prototypes. Nothing has to be compiled, and every operation is one process call with a clear exit point.
  • Java bindings fit services that already run on the JVM, including servlet containers: the JSPDemo folder shows page rendering, splitting and DOCX conversion called from JSP pages.
  • C# bindings fit .NET services on Windows or Linux. The wrappers are plain source files that you add to your project, and they call the native library through DllImport.

A common pattern is to start with the CLI to validate the output on real documents, then move the same steps into Java or C# when you need finer control, such as custom file naming, per-page error handling or streaming results back to a web request.

Deploying the native engine

The rule repeated across the READMEs is simple: the native library and cmaps.dat/umaps.dat must be deployed together. On Linux, the Binaries README installs them like this:

sudo cp "Binaries/Ubuntu22.04.1 LTS x64/libRDDJni.so" /usr/local/lib/
sudo cp res/cmaps.dat res/umaps.dat /usr/local/lib/
sudo ldconfig

For Java, start the JVM with -Djava.library.path pointing to the folder that holds the library, for example java -Djava.library.path=/usr/local/lib -jar YourApp.jar.

There is one Linux detail worth knowing before your first run. Both Global.Init in the Java demo and RDGlobal.Init in the C# wrapper load the CMaps, the CMYK profile and the fallback fonts from hard-coded paths under /root/res/. On Windows they call a different loader. Edit those paths so they point to your real resource directory, or the engine starts without fonts and character maps.

Layer 1: batch jobs with the CLI tools

The CLI tools are the fastest way to add PDF processing to cron jobs, queues or CI scripts. Copy the whole content of CLI/Ubuntu-22.04-LTS (or CLI/Windows-x64) to the target folder, keep libRDDJni.so, cmaps.dat and umaps.dat next to the executables, and on Linux run chmod +x rdinfo rdsec rdpage rdcvt.

The tools need an activated license. If they report “license not activated”, run ./rdinfo -r to print the request code, obtain a key for it from RadaeePDF, and save the key in a file named key.txt in the program directory.

Each tool takes the file, then its password ("" when there is none):

  • rdinfo -info <file> <pswd> prints document information; rdinfo -set <file> <pswd> Author=... Title=... writes metadata.
  • rdpage -txt <file> <pswd> <list> <out.txt> extracts text; an empty list means all pages.
  • rdpage -exp <file> <pswd> <png|jpg> <dpi> <directory> exports every page as 0000.png, 0001.png and so on. A dpi of 72 means 100%, 144 means 200%.
  • rdpage -del, -rot and -imp delete, rotate (multiples of 90) and import pages.
  • rdsec -enc <file> <pswd> <out> <owner-pswd> <user-pswd> encrypts; rdsec -dec removes the password; rdsec -wm stamps a PNG (RGBA) watermark on every page.

A nightly job that processes a folder of incoming files can be written with nothing but these options:

#!/bin/bash
# libRDDJni.so, cmaps.dat, umaps.dat and key.txt
IN=/data/incoming
OUT=/data/processed

for f in "$IN"/*.pdf; do
  name=$(basename "$f" .pdf)
  mkdir -p "$OUT/$name/pages"

  ./rdinfo -info "$f" "" > "$OUT/$name/info.txt"
  ./rdpage -txt "$f" "" "" "$OUT/$name/text.txt"
  ./rdpage -exp "$f" "" png 144 "$OUT/$name/pages"
  ./rdsec -enc "$f" "" "$OUT/$name/$name-protected.pdf" "owner-secret" ""
done

To merge documents, ./rdpage -imp test.pdf "" src.pdf "" appends all pages of src.pdf to test.pdf, creating test.pdf if it does not exist. Options such as -del, -rot and -imp take no output path, so run them on a copy if you need to keep the original.

Layer 2: Java integration

When the pipeline lives inside a Java service, use the classes from java/PDFJniDemo/src. Initialization is a single call: Global.Init(String serial) loads the RDDJni library, activates the license and returns true on success. Later calls return immediately, so call it once at startup. Without an active license the demo shows that GetPage returns null after the first pages, so always check for it.

The following class renders every page to PNG. It follows the render_pages method of JSPDemo/images.jsp, including its 4096-pixel cap that keeps very large pages from exhausting memory.

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 PdfPipeline {

    public static void main(String[] args) throws IOException {
        if (!Global.Init("YOUR-LICENSE-KEY")) {
            System.err.println("License not activated: output will be limited.");
        }
        int err = renderPages(args[0], "", new File(args[1]), 2.0f);
        System.out.println("Result: " + err);
    }

    // Returns 0 on success or the Document.Open error code:
    // -1 password needed, -2 unknown encryption, -3 damaged file, -10 access denied
    static int renderPages(String pdfPath, String password, File outDir, float scale)
            throws IOException {
        Document doc = new Document();
        int err = doc.Open(pdfPath, password);
        if (err != 0) return err;
        outDir.mkdirs();
        try {
            int count = doc.GetPageCount();
            for (int pno = 0; pno < count; pno++) {
                Page page = doc.GetPage(pno);
                if (page == null) break; // license not activated
                float pw = doc.GetPageWidth(pno);
                float ph = doc.GetPageHeight(pno);
                float s = scale;
                if (pw * s > 4096.0f) s = 4096.0f / pw;
                if (ph * s > 4096.0f) s = 4096.0f / ph;
                int iw = (int) (pw * s + 0.5f);
                int ih = (int) (ph * s + 0.5f);

                BufferedImage bimg = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB);
                Matrix mat = new Matrix(s, -s, 0, ph * s); // flip the PDF y axis
                boolean ok = page.RenderToBuf(bimg, -1, mat); // -1 = white background
                mat.Destroy();
                page.Close();
                if (ok) {
                    ImageIO.write(bimg, "png", new File(outDir, String.format("p%06d.png", pno)));
                }
            }
        } finally {
            doc.Close();
        }
        return 0;
    }
}

Encrypting and merging in Java

Document.EncryptAs writes an encrypted copy. The bundled guide (java/doc/genc.html) explains the arguments: user password, owner password, permission bits (-1 grants all), method 3 for AES-256 (other values use AES-128), and a 32-byte file ID. The method requires a premium license.

import java.security.SecureRandom;
import com.radaee.pdf.Document;

public class PdfSecurity {
    static boolean encrypt(String src, String dst, String ownerPassword) {
        Document doc = new Document();
        if (doc.Open(src, null) != 0) return false;
        byte[] id = new byte[32];
        new SecureRandom().nextBytes(id);
        boolean ok = doc.EncryptAs(dst, "", ownerPassword, -1, 3, id);
        doc.Close();
        return ok;
    }
}

To decrypt, open the file with its password and call doc.SaveAs(path, true), which saves the document with its security removed.

Merging uses Document.ImportStart, which returns a Document.ImportContext. Keep the source document open until you call Destroy() on the context. Importing also requires a premium license.

import com.radaee.pdf.Document;

public class PdfMerge {
    // Appends all pages of srcPath to the end of dstPath
    static boolean append(String dstPath, String srcPath) {
        Document dst = new Document();
        Document src = new Document();
        if (dst.Open(dstPath, null) != 0 || src.Open(srcPath, null) != 0) {
            src.Close();
            dst.Close();
            return false;
        }
        Document.ImportContext ctx = dst.ImportStart(src);
        boolean ok = (ctx != null);
        if (ok) {
            int base = dst.GetPageCount();
            int count = src.GetPageCount();
            for (int i = 0; i < count && ok; i++) {
                ok = ctx.ImportPage(i, base + i);
            }
            ctx.Destroy();
        }
        if (ok) ok = dst.Save();
        src.Close();
        dst.Close();
        return ok;
    }
}

To start from an empty file instead, call Document.Create(path), which returns 0 on success. This is how split.jsp writes each page to its own PDF.

Layer 3: C# integration

The C# wrappers live in the RDDLib.comm and RDDLib.pdf namespaces. The Windows demo calls RDGlobal.Init() and then RDGlobal.Active(key), which returns 3 on success. Rendering uses an RDDIB pixel buffer, and RDDIB can save itself straight to PNG or JPEG, so no imaging library is needed.

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

public class Program
{
    static int Main(string[] args)
    {
        RDGlobal.Init();
        if (RDGlobal.Active("YOUR-LICENSE-KEY") != 3)
        {
            Console.WriteLine("License activation failed");
            return 1;
        }

        PDFDoc doc = new PDFDoc();
        int err = doc.Open(args[0], "");
        if (err != 0)
        {
            Console.WriteLine("Open failed: " + err);
            return 2;
        }

        const float scale = 2.0f; // 144 dpi
        int count = doc.GetPageCount();
        for (int pno = 0; pno < count; pno++)
        {
            PDFPage page = doc.GetPage(pno);
            if (page == null) break;
            float pw = doc.GetPageWidth(pno);
            float ph = doc.GetPageHeight(pno);
            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.SavePNG(string.Format("p{0:D6}.png", pno));
            page.Close();
        }
        doc.Close();
        return 0;
    }
}

The same PDFDoc class has EncryptAs, SaveAs, ImportStart (returning a PDFImporter with ImportPage and ImportEnd), and GetMeta/SetMeta. The Windows demo also converts Word files: DOCXDoc.ExportPDF(PDFDoc) writes a DOCX document into a PDF created with PDFDoc.Create.

Common pitfalls

  • Missing resource files: cmaps.dat and umaps.dat must sit next to the native library. On Linux, also check the /root/res/ paths in Global.Init or RDGlobal.Init.
  • Library not found on Linux: put libRDDJni.so in a directory listed in LD_LIBRARY_PATH or /etc/ld.so.conf, run ldconfig, and inspect dependencies with ldd libRDDJni.so.
  • Architecture or glibc mismatch: compare file libRDDJni.so with uname -m (x86_64 or aarch64). The Ubuntu 22.04 build needs glibc 2.35+, the 24.04 build glibc 2.39+.
  • Windows runtime: the Visual C++ Redistributable must be installed, and the DLL must match the process bitness (x64 or x86).
  • License-bound features: the source marks EncryptAs, ImportStart and NewPage as premium and SaveAs as professional or premium. The license is tied to a request code derived from hardware and network settings, so changing the network configuration can invalidate it.

Licensing for server deployment

Section 12 of the RadaeePDF licensing fact sheet covers server installation. Installing applications, command-line executables or services on a workstation or server requires a proper license, meaning a valid commercial license with a legitimate activation key. The server license does not restrict the number of concurrent processes or applications that invoke the SDK runtime, so one license covers a worker pool or several services on the same machine. Each workstation or server is identified by a unique ID generated by the operating system on its first run, which is why a new or rebuilt server needs its own request code and key.

Frequently asked questions

Can RadaeePDF run on a server without a user interface?

Yes. The RadaeePDF Server SDK is a native C++ engine for Windows and Linux that runs headless. It is meant for conversions, editing and content manipulation in backend services, batch jobs and command-line tools.

Which languages can I use with the RadaeePDF Server SDK?

Java through the JNI bindings in the PDFJniDemo project, C# through the wrappers in the C# folder, and any shell or scheduler that can call the CLI tools such as rdinfo, rdpage and rdsec.

Does the server license limit concurrent processes?

No. Section 12 of the RadaeePDF licensing fact sheet states that the server license does not restrict concurrent processes or applications invoking the SDK runtime. Each workstation or server is identified by a unique ID generated on its first run.

Related on radaeepdf.com

Where to go next

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

AES-256 PDF encryption with user and owner passwords

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

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

Key takeaways:

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

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

How to encrypt PDF with AES-256 using EncryptAs

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

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

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

User password vs owner password

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

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

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

Permission bits

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

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

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

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

AES-256 vs AES-128 by platform

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

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

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

Generating the 32-byte file ID

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

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

Android (Master or Classic), Java

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

import com.radaee.pdf.Document;

import java.security.SecureRandom;

public final class PdfEncryptor {

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

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

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

Server, Java

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

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

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

iOS (Master), Objective-C

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

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

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

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

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

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

Windows UWP, C#

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

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

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

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

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

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

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

Opening and checking an encrypted file

Every edition reports password problems through the Open return value:

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

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

Removing a password

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

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

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

Common pitfalls

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

Frequently asked questions

Which value of the method parameter enables AES-256?

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

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

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

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

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

Related on radaeepdf.com

Where to go next

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

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

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 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