How to Add QR Code to PDF in Java (With Encryption & Custom Data)

Why You Need This Guide

Ever tried adding a QR code to a PDF programmatically, only to realize you need to embed custom data—like a signature timestamp, author name, or unique ID—and encrypt it all? If you’ve been piecing together multiple libraries or writing custom serialization logic from scratch, there’s a simpler way.

In this tutorial, you’ll learn how to add encrypted QR codes to PDFs using Java in about 15 minutes. We’re talking about QR codes that can carry your custom metadata (dates, names, IDs, whatever you need) and protect that data with encryption—all without reinventing the wheel.

What you’ll accomplish:

  • Generate QR codes with custom data fields (IDs, timestamps, author info)
  • Encrypt the serialized data inside your QR codes
  • Embed these secure QR codes into PDFs with precise positioning
  • Do all of this using GroupDocs.Signature for Java (a library that handles the heavy lifting)

This isn’t just about slapping a basic QR code on a document. It’s about creating secure, data-rich signatures that you can use for contracts, invoices, certificates, or any document that needs tamper-evident verification.

Why Choose QR Codes for PDF Signatures?

You might be wondering: “Why QR codes? Why not just a regular digital signature?”

Here’s the thing—QR codes offer unique advantages:

  1. Visual Verification: Anyone with a smartphone can scan and verify the signature instantly (no special software required)
  2. Metadata Storage: Pack multiple data points (author, date, ID, version) into one scannable element
  3. Offline Accessibility: The verification data lives in the document, not on a remote server
  4. Universal Compatibility: Works across all PDF readers without plugins or extensions
  5. Tamper Evidence: Encrypted data reveals any document modifications after signing

Think of it like this: a traditional signature says “I signed this,” but an encrypted QR code says “I signed this, here’s when, here’s my ID, and here’s proof it hasn’t been altered.”

Prerequisites

Before we start coding, make sure you’ve got these basics covered:

What You’ll Need

Software Requirements:

  • Java Development Kit (JDK) 8 or higher installed
  • Maven or Gradle for dependency management (or you can download JARs directly)
  • Your favorite IDE (IntelliJ IDEA, Eclipse, VS Code—whatever you’re comfortable with)

Library Requirements:

  • GroupDocs.Signature for Java (version 23.12 or higher)

Knowledge Prerequisites:

  • Basic Java programming skills (you should know what a class and method are)
  • Familiarity with file paths and working with PDFs in Java
  • Optional: Understanding of object serialization helps, but we’ll explain it

What You Don’t Need:

  • Deep cryptography knowledge (the library handles encryption)
  • Prior experience with QR code generation
  • Advanced Maven/Gradle expertise (we’ll show you the exact dependency snippets)

Setting Up GroupDocs.Signature for Java

Let’s get the library installed. Choose your build tool below:

Option 1: Maven Installation

Add this dependency to your pom.xml file:

<dependency>
    <groupId>com.groupdocs</groupId>
    <artifactId>groupdocs-signature</artifactId>
    <version>23.12</version>
</dependency>

After adding this, run mvn clean install to download the library.

Option 2: Gradle Installation

For Gradle users, add this line to your build.gradle file:

implementation 'com.groupdocs:groupdocs-signature:23.12'

Then sync your project with gradle build.

Option 3: Manual Download

Prefer doing things manually? Download the JAR files directly from GroupDocs.Signature for Java releases and add them to your project’s classpath.

Getting a License (Important!)

GroupDocs.Signature isn’t completely free, but you have options:

  1. Free Trial: Download and test all features for 30 days (perfect for this tutorial)
  2. Temporary License: Request a 30-day temporary license with no feature restrictions
  3. Purchase: Buy a full license for production use

For learning purposes, the free trial works great. You’ll see a watermark on output documents, but all functionality is available.

Quick Setup Test

Once installed, verify everything works with this minimal code:

import com.groupdocs.signature.Signature;

public class SetupGroupDocs {
    public static void main(String[] args) {
        Signature signature = new Signature("YOUR_DOCUMENT_DIRECTORY/sample.pdf");
        System.out.println("GroupDocs.Signature loaded successfully!");
        // Your code here...
    }
}

Replace "YOUR_DOCUMENT_DIRECTORY/sample.pdf" with an actual PDF path on your system. If it runs without errors, you’re good to go!

Understanding Custom Serialization for QR Codes

Before we jump into code, let’s clarify what we mean by “custom serialization” (because this trips up a lot of developers).

What Is Serialization (In Plain English)?

Serialization is just converting your data into a format that can be stored or transmitted. Think of it like packing a suitcase—you’re organizing your clothes (data) into a compact, transportable form (serialized format).

In our case, we want to pack multiple pieces of information—like a signature ID, author name, timestamp, and a custom data value—into a single QR code. Without serialization, you’d have to generate separate QR codes for each piece of data (messy and impractical).

Why Custom Serialization Matters

GroupDocs.Signature lets you define exactly how your data should be formatted inside the QR code. Using annotations (like @FormatAttribute), you can specify:

  • Property names: Shortened labels to save space (e.g., “SignID” instead of “SignatureIdentifier”)
  • Date formats: How timestamps should appear (e.g., “yyyy-MM-dd”)
  • Number formats: Precision for decimal values (e.g., “N2” for two decimal places)

This matters because QR codes have limited data capacity. By customizing serialization, you maximize the information you can embed while keeping the QR code scannable.

Implementation Guide: Building Your Custom QR Code Signature

Now let’s build this step-by-step. We’ll create a custom data class, implement encryption, and sign a PDF with our QR code.

Step 1: Create the Custom Serialization Class

First, we need a class to hold the data we want to embed in the QR code. Here’s what we’re working with:

import com.groupdocs.signature.domain.extensions.serialization.FormatAttribute;
import java.math.BigDecimal;
import java.util.Date;

class DocumentSignatureData {
    @FormatAttribute(propertyName = "SignID")
    public String ID;

    public void setID(String value) { 
        this.ID = value; 
    }

    @FormatAttribute(propertyName = "SAuth")
    public String author;

    public void setAuthor(String value) {
        this.author = value;
    }

    @FormatAttribute(propertyName = "SDate", propertyFormat = "yyyy-MM-dd")
    public Date signed = new Date();

    public void setSigned(Date value) {
        this.signed = value;
    }

    @FormatAttribute(propertyName = "SDFact", propertyFormat = "N2")
    public BigDecimal dataFactor = new BigDecimal(0.01);

    public void setDataFactor(BigDecimal value) {
        this.dataFactor = value;
    }
}

What’s happening here:

  • @FormatAttribute: This annotation tells GroupDocs how to serialize each field

    • propertyName: The shortened name that appears in the QR code data (saves space)
    • propertyFormat: How to format the value (date patterns, number precision)
  • The four data fields:

    • ID: A unique identifier for this signature (we’ll use UUIDs in a moment)
    • author: Who signed the document (could be a username or email)
    • signed: When the document was signed (defaults to current date/time)
    • dataFactor: A custom numeric value (maybe a version number or confidence score—whatever your use case needs)

Why this structure? It’s flexible. You can add or remove fields based on your needs. Need to track document versions? Add a version field. Need geographic data? Add location. The @FormatAttribute annotation ensures everything serializes efficiently.

Step 2: Implement the QR Code Signature with Encryption

Now we’ll bring it all together—create an instance of our data class, encrypt it, and embed it in a PDF:

import com.groupdocs.signature.Signature;
import com.groupdocs.signature.domain.Padding;
import com.groupdocs.signature.domain.enums.HorizontalAlignment;
import com.groupdocs.signature.domain.enums.VerticalAlignment;
import com.groupdocs.signature.domain.extensions.encryption.IDataEncryption;
import com.groupdocs.signature.domain.qrcodes.QrCodeTypes;
import com.groupdocs.signature.exception.GroupDocsSignatureException;
import com.groupdocs.signature.options.sign.QrCodeSignOptions;
import java.io.File;
import java.math.BigDecimal;
import java.util.Date;
import java.util.UUID;

class SignWithQRCodeCustomSerialization {
    String filePath = "YOUR_DOCUMENT_DIRECTORY/sample.pdf"; 
    String outputFilePath = new File("YOUR_OUTPUT_DIRECTORY", "SignedQRCodeCustomSerialization.pdf").getPath();

    public void signDocument() throws Exception {
        Signature signature = new Signature(filePath);

        // Implement your custom encryption logic here
        IDataEncryption encryption = new CustomXOREncryption();

        DocumentSignatureData documentSignature = new DocumentSignatureData();
        documentSignature.setID(UUID.randomUUID().toString());
        documentSignature.setAuthor(System.getenv("USERNAME")); 
        documentSignature.setSigned(new Date());
        documentSignature.setDataFactor(new BigDecimal("11.22"));

        QrCodeSignOptions options = new QrCodeSignOptions();
        options.setData(documentSignature);
        options.setEncodeType(QrCodeTypes.QR);
        options.setDataEncryption(encryption);

        // Configure alignment and appearance
        options.setHeight(100);
        options.setWidth(100);
        options.setVerticalAlignment(VerticalAlignment.Bottom);
        options.setHorizontalAlignment(HorizontalAlignment.Right);
        
        Padding padding = new Padding();
        padding.setRight(10);
        padding.setBottom(10);
        options.setMargin(padding);

        signature.sign(outputFilePath, options);
    }
}

Breaking down the code:

  1. Initialize the Signature object: new Signature(filePath) loads your source PDF
  2. Set up encryption: CustomXOREncryption() is your custom encryption class (you’ll implement IDataEncryption interface—more on this in a moment)
  3. Populate the data object:
    • Generate a unique ID using UUID.randomUUID()
    • Grab the current username from environment variables
    • Set the current date/time
    • Add a custom decimal value (in this example, 11.22)
  4. Configure QR code options:
    • Attach your data object with setData()
    • Specify QR code type (standard QR format)
    • Enable encryption with setDataEncryption()
  5. Position the QR code:
    • 100x100 pixel size (adjust as needed)
    • Bottom-right corner of the PDF
    • 10-pixel padding from edges
  6. Sign the document: signature.sign() creates the new PDF with the QR code embedded

About the Encryption Part

You might’ve noticed the CustomXOREncryption() reference. This is where you implement your own encryption logic by creating a class that implements the IDataEncryption interface.

Why custom encryption? GroupDocs.Signature gives you the flexibility to use any encryption algorithm you want—XOR (simple but not secure), AES (industry standard), RSA (public-key cryptography), or even your proprietary method.

Here’s a basic structure for your encryption class:

import com.groupdocs.signature.domain.extensions.encryption.IDataEncryption;

public class CustomXOREncryption implements IDataEncryption {
    @Override
    public String encode(String source) {
        // Your encoding logic here
        // Example: XOR each character with a secret key
        return encodedString;
    }

    @Override
    public String decode(String source) {
        // Your decoding logic here
        return decodedString;
    }
}

Important note: The example uses XOR encryption for simplicity, but for production use, you should implement AES-256 or another secure algorithm. XOR is easy to break and shouldn’t be used for sensitive data.

Common Pitfalls and How to Avoid Them

Let’s talk about the mistakes developers make when implementing this (so you don’t have to learn the hard way):

Pitfall #1: File Paths Are Incorrect

The problem: You get a “File not found” exception even though the file exists.

The fix: Always use absolute paths or properly resolve relative paths. Instead of:

String filePath = "sample.pdf"; // ❌ This might fail

Do this:

String filePath = new File("YOUR_DOCUMENT_DIRECTORY", "sample.pdf").getAbsolutePath(); // ✅ Reliable

Pro tip: For output files, create the directory if it doesn’t exist:

File outputDir = new File("YOUR_OUTPUT_DIRECTORY");
if (!outputDir.exists()) {
    outputDir.mkdirs();
}

Pitfall #2: Weak or Broken Encryption

The problem: Your encryption class throws exceptions or produces unreadable QR codes.

The fix: Test your encryption/decryption methods independently before integrating:

CustomXOREncryption encryption = new CustomXOREncryption();
String testData = "TestSignature123";
String encoded = encryption.encode(testData);
String decoded = encryption.decode(encoded);
assert testData.equals(decoded); // Should match!

If the assertion fails, your encryption logic has issues.

Pitfall #3: QR Code Is Too Small or Positioned Poorly

The problem: The QR code is unreadable or gets cut off by PDF margins.

The fix: Adjust size and padding based on your PDF’s dimensions. For most documents:

options.setHeight(120); // Slightly larger
options.setWidth(120);
Padding padding = new Padding();
padding.setRight(20); // More breathing room
padding.setBottom(20);

Test with different PDF sizes (A4, Letter, etc.) to ensure compatibility.

Pitfall #4: Not Handling Exceptions Properly

The problem: Your application crashes silently when signing fails.

The fix: Wrap the signing logic in try-catch blocks:

try {
    signature.sign(outputFilePath, options);
    System.out.println("Document signed successfully!");
} catch (GroupDocsSignatureException e) {
    System.err.println("Signing failed: " + e.getMessage());
    e.printStackTrace();
    // Log error or notify user
}

Pitfall #5: Forgetting to Dispose Resources

The problem: Memory leaks in long-running applications.

The fix: Always dispose of the Signature object:

try (Signature signature = new Signature(filePath)) {
    // Your signing code here
} // Automatically disposes when done

Or manually:

Signature signature = new Signature(filePath);
try {
    signature.sign(outputFilePath, options);
} finally {
    signature.dispose();
}

Best Practices for Production Use

If you’re planning to use this in a real application (not just for learning), follow these guidelines:

1. Use Strong Encryption

Swap out the example CustomXOREncryption with AES-256:

  • Use Java’s built-in javax.crypto package
  • Store encryption keys securely (environment variables, key vaults—never hardcode them)
  • Consider implementing key rotation for long-term security

2. Validate Input Data

Before signing, verify your data:

if (documentSignature.getID() == null || documentSignature.getID().isEmpty()) {
    throw new IllegalArgumentException("Signature ID cannot be null or empty");
}

3. Log Signing Operations

For auditing and troubleshooting:

Logger logger = Logger.getLogger(SignWithQRCodeCustomSerialization.class.getName());
logger.info("Signing document: " + filePath);
logger.info("Signature ID: " + documentSignature.getID());

4. Optimize QR Code Size

Larger QR codes are easier to scan but take up more space. Find the sweet spot:

  • Documents for print: 100-150px
  • Digital-only documents: 80-100px
  • Mobile-first viewing: 120-150px

5. Test on Multiple Devices

QR codes can behave differently across scanners. Test with:

  • Smartphone cameras (iOS and Android)
  • Dedicated QR scanner apps
  • Desktop QR code readers

6. Document Your Encryption Method

Future developers (or future you) will thank you:

/**
 * Encrypts signature data using AES-256-GCM.
 * Key derivation: PBKDF2 with 100,000 iterations.
 * IV: Randomly generated per signature.
 */
public class SecureAESEncryption implements IDataEncryption {
    // Implementation...
}

Real-World Implementation Scenarios

Let’s look at how you’d actually use this in different contexts:

Use case: Law firm needs tamper-evident signatures on client contracts.

Implementation:

documentSignature.setID(contract.getContractNumber());
documentSignature.setAuthor(lawyer.getBarNumber());
documentSignature.setSigned(new Date());
documentSignature.setDataFactor(new BigDecimal(contract.getVersion()));

The QR code now contains: contract number, lawyer’s bar number, signing timestamp, and document version—all encrypted.

Verification: Clients can scan the QR code to verify the signature’s authenticity and see when it was signed, without accessing external databases.

Scenario 2: Invoice Authentication

Use case: Accounting software needs to prevent invoice tampering.

Implementation:

documentSignature.setID(invoice.getInvoiceNumber());
documentSignature.setAuthor("AccountingSystem_v2.3");
documentSignature.setSigned(new Date());
documentSignature.setDataFactor(invoice.getTotalAmount());

Benefit: If someone alters the invoice amount, the QR code’s embedded total won’t match, revealing the tampering.

Scenario 3: Academic Certificates

Use case: University issues digital diplomas with verification QR codes.

Implementation:

documentSignature.setID(student.getStudentID());
documentSignature.setAuthor(registrar.getEmployeeID());
documentSignature.setSigned(graduationDate);
documentSignature.setDataFactor(new BigDecimal(student.getGPA()));

Use case: Employers can scan the QR code to verify the certificate’s authenticity, graduation date, and GPA without contacting the university.

Scenario 4: Shipping Documents

Use case: Logistics company tracks shipments with signed waybills.

Implementation:

documentSignature.setID(shipment.getTrackingNumber());
documentSignature.setAuthor(driver.getEmployeeID());
documentSignature.setSigned(new Date());
documentSignature.setDataFactor(new BigDecimal(shipment.getWeight()));

Benefit: Warehouse staff scan the QR code to verify the driver, timestamp, and shipment weight match the physical goods.

When to Use This Approach (And When Not To)

✅ Use This When:

  • You need offline verification (QR code can be scanned without internet access)
  • You want visual proof of signatures (anyone can see the QR code is there)
  • You need to embed multiple data points in a compact format
  • You’re working with documents that get printed (QR codes survive printing)
  • You want cross-platform compatibility (works on all PDF readers)

❌ Avoid This When:

  • You need invisible signatures (QR codes are always visible—use digital certificates instead)
  • Your PDFs are extremely large (GroupDocs.Signature loads entire files into memory)
  • You’re on a very tight budget (the library requires a paid license for production)
  • You only need basic text signatures (QR codes are overkill for simple name/date stamps)
  • You need long-term archival with cryptographic verification (consider PAdES-LTV instead)

Troubleshooting Common Errors

Error: “License is not set”

Cause: You’re using the trial version, which shows this warning.

Solution: Either acquire a license or ignore the watermark for testing purposes.

Error: “Cannot create QR Code - data is too large”

Cause: You’re trying to embed more data than a QR code can hold.

Solution:

  • Shorten your propertyName values in @FormatAttribute
  • Remove unnecessary data fields
  • Use QR Code version 40 (largest capacity): options.setEncodeType(QrCodeTypes.QR)

Error: “Encryption failed - invalid key”

Cause: Your encryption key is null, empty, or in the wrong format.

Solution: Verify your key initialization:

String encryptionKey = System.getenv("SIGNATURE_ENCRYPTION_KEY");
if (encryptionKey == null || encryptionKey.length() < 16) {
    throw new IllegalStateException("Invalid encryption key");
}

Error: “Output file already exists”

Cause: You’re trying to overwrite an existing file without permission.

Solution: Either delete the old file or append a timestamp to the filename:

String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String outputFilePath = "SignedDoc_" + timestamp + ".pdf";

Wrapping Up

You now know how to add encrypted QR codes with custom data to PDFs using Java—a skill that’s surprisingly rare and incredibly valuable. Whether you’re securing legal documents, tracking shipments, or verifying academic credentials, this approach gives you flexibility and security without the complexity of building everything from scratch.

Key takeaways:

  • Custom serialization lets you pack multiple data points into a single QR code efficiently
  • Encryption protects sensitive metadata from unauthorized access
  • GroupDocs.Signature handles the heavy lifting, but you maintain full control over data format and security
  • Always test your encryption implementation independently before integrating
  • Use absolute file paths and proper exception handling to avoid common pitfalls

Next steps:

  1. Implement AES-256 encryption for production use (replace the XOR example)
  2. Add signature verification logic to read and decrypt QR codes from PDFs
  3. Build a web service to handle bulk document signing
  4. Integrate with your existing document management system

Got questions or running into issues not covered here? Check out the GroupDocs.Signature documentation or reach out to their support team—they’re surprisingly responsive.