# How to Implement Custom PDF Processing Using PDFBox in Stirling-PDF

> Learn to implement custom PDF processing in Stirling-PDF using Apache PDFBox. Create Spring services with PDDocument and REST controllers to enhance your PDF workflows.

- Repository: [Stirling Tools/Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF)
- Tags: how-to-guide
- Published: 2026-03-01

---

**Stirling-PDF leverages Apache PDFBox as its core Java library for backend PDF manipulation, enabling developers to implement custom processing by creating Spring services that work with PDDocument objects and exposing them through REST controllers that return byte arrays.**

Stirling-Tools/Stirling-PDF is a robust, open-source PDF toolkit built on Spring Boot that handles everything from simple conversions to complex document restructuring. When you need to implement custom PDF processing using PDFBox in this codebase, you follow a well-established pattern that ensures proper resource management and seamless integration with the existing API architecture.

## The PDFBox Architecture Pattern in Stirling-PDF

Stirling-PDF delegates all low-level PDF manipulation to Apache PDFBox, falling back to this library when external tools like Ghostscript are unavailable. The codebase demonstrates consistent usage across multiple modules, from PDF/A conversion to page cropping and poster generation.

The standard lifecycle follows three phases: **load**, **manipulate**, and **save**. Every service loads the source document via `PDDocument.load(InputStream)`, applies transformations using PDFBox APIs, and writes the result to a `ByteArrayOutputStream` before returning the raw bytes to the controller.

Key implementations demonstrating this pattern include:

| Feature | Implementation File | PDFBox Usage |
|---------|-------------------|--------------|
| **PDF/A Conversion** | [`ConvertPDFToPDFA.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/ConvertPDFToPDFA.java) | `processWithPDFBox(PDDocument, int)` validates and sanitizes documents when Ghostscript is unavailable. |
| **Page Cropping** | [`CropController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CropController.java) | `cropWithPDFBox` extracts rectangular regions using `PDPage` and `PDPageContentStream`. |
| **Poster Generation** | [`PosterPdfController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/PosterPdfController.java) | Splits pages into grids by copying content streams into new `PDDocument` instances. |
| **SVG to PDF** | [`SvgToPdf.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/SvgToPdf.java) | Renders vector graphics via `PDFBoxGraphics2D` onto new pages. |
| **JSON Reconstruction** | [`PdfJsonConversionService.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/PdfJsonConversionService.java) | Rebuilds documents from JSON models, handling fonts and text encoding. |
| **Repair Fallback** | [`RepairController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/RepairController.java) | Simple load-save cycle to fix structural corruption. |

## Step-by-Step Implementation Guide

### Create the Service Layer

Custom processing begins in the service layer. Create a class annotated with `@Service` in the `stirling.software.SPDF.service` package. The method must accept a `MultipartFile` (or `byte[]`), load it into a `PDDocument`, and return the processed bytes.

Critical implementation details:
- Use **try-with-resources** to ensure `PDDocument` and `PDPageContentStream` instances close automatically.
- Write output to a `ByteArrayOutputStream` to avoid temporary files.
- Handle `IOException` explicitly, letting Spring's exception handling translate it to appropriate HTTP responses.

### Build the REST Controller

Expose your service through a class annotated with `@RestController` in the `stirling.software.SPDF.controller.api` (or `api.misc`) package. Follow the existing URL convention `/api/v1/{category}/{operation}`.

The controller should:
- Accept `MultipartFile` parameters with `@RequestParam`.
- Return `ResponseEntity<byte[]>` with `Content-Type: application/pdf`.
- Set the `Content-Disposition` header to force download with appropriate filenames.

### Register in Spring Context

Component scanning is already configured in Stirling-PDF. Simply placing your `@RestController` class in the `app/core/src/main/java/stirling/software/SPDF/controller/api` package ensures automatic registration. No additional configuration beans are required.

## Complete Code Example: Adding a Watermark Feature

The following implementation adds text watermarks to every page, following the exact patterns found in [`CropController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CropController.java) and [`PosterPdfController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/PosterPdfController.java).

**WatermarkService.java:**

```java
package stirling.software.SPDF.service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
public class WatermarkService {

    /** Adds a simple text watermark to every page of the supplied PDF. */
    public byte[] applyWatermark(MultipartFile file, String watermarkText) throws IOException {
        try (PDDocument doc = PDDocument.load(file.getBytes())) {
            for (var page : doc.getPages()) {
                try (PDPageContentStream cs = new PDPageContentStream(doc, page,
                        PDPageContentStream.AppendMode.APPEND, true, true)) {
                    cs.beginText();
                    cs.setFont(PDType1Font.HELVETICA_BOLD_OBLIQUE, 72);
                    cs.setNonStrokingColor(200, 200, 200); // light gray
                    cs.setTextMatrix(0.5f, 0, 0, 0.5f,
                            page.getMediaBox().getWidth() / 2,
                            page.getMediaBox().getHeight() / 2);
                    cs.showText(watermarkText);
                    cs.endText();
                }
            }

            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                doc.save(out);
                return out.toByteArray();
            }
        }
    }
}

```

**WatermarkController.java:**

```java
package stirling.software.SPDF.controller.api;

import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.service.WatermarkService;

@RestController
@RequestMapping("/api/v1/misc")
@RequiredArgsConstructor
public class WatermarkController {

    private final WatermarkService watermarkService;

    @PostMapping(value = "/add-watermark", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<byte[]> addWatermark(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "text", defaultValue = "Sample Watermark") String text) throws IOException {

        byte[] result = watermarkService.applyWatermark(file, text);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"watermarked.pdf\"")
                .contentType(MediaType.APPLICATION_PDF)
                .body(result);
    }
}

```

Key implementation notes:
- **`AppendMode.APPEND`** ensures the watermark overlays existing content without erasure, matching the pattern used in overlay controllers.
- **Matrix transformations** position the text at the page center with scaling.
- **Resource safety** is guaranteed by nested try-with-resources blocks, preventing memory leaks on large documents.

## Unit Testing Your Implementation

Stirling-PDF uses standard JUnit 5 tests with `MockMultipartFile` for service-layer validation. Place tests in `app/core/src/test/java/stirling/software/SPDF/service/` following the conventions of `AttachmentServiceTest` and `CropControllerTest`.

```java
package stirling.software.SPDF.service;

import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;

class WatermarkServiceTest {

    @Test
    void addsWatermark() throws IOException {
        // Load a tiny test PDF from the test resources
        var pdfBytes = getClass().getResourceAsStream("/testfiles/blank.pdf").readAllBytes();
        var multipart = new MockMultipartFile("file", "blank.pdf",
                "application/pdf", pdfBytes);

        var service = new WatermarkService();
        byte[] result = service.applyWatermark(multipart, "TEST");

        try (PDDocument doc = PDDocument.load(result)) {
            assertEquals(1, doc.getNumberOfPages(),
                    "The watermark operation should preserve page count");
        }
    }
}

```

## Key Files to Study for Advanced Patterns

For complex operations, examine these authoritative implementations in the Stirling-Tools/Stirling-PDF repository:

- **[`ConvertPDFToPDFA.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/ConvertPDFToPDFA.java)** – PDF/A preflight validation and sanitization logic.
- **[`CropController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CropController.java)** – Coordinate-based page manipulation.
- **[`PosterPdfController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/PosterPdfController.java)** – Multi-page generation from single sources.
- **[`SvgToPdf.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/SvgToPdf.java)** – Graphics2D bridge usage for vector conversion.
- **[`PdfJsonConversionService.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/PdfJsonConversionService.java)** – Document reconstruction from data models.
- **[`RepairController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/RepairController.java)** – Minimal load-save repair patterns.
- **[`AttachmentService.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/AttachmentService.java)** – Binary stream embedding techniques.

## Summary

- **Stirling-PDF** implements custom PDF processing through a standardized service-controller pattern using Apache PDFBox.
- **Service classes** handle `PDDocument` lifecycle management with try-with-resources, ensuring zero resource leaks.
- **REST controllers** accept `MultipartFile` uploads and return `ResponseEntity<byte[]>` with proper PDF content headers.
- **Source files** are located in `app/core/src/main/java/stirling/software/SPDF/` following strict package conventions (`controller.api` and `service`).
- **Testing** uses `MockMultipartFile` and `PDDocument.load()` to verify output integrity without external dependencies.

## Frequently Asked Questions

### Where is PDFBox declared in the Stirling-PDF build configuration?

PDFBox dependencies are managed in `app/core/build.gradle`. The library is already available to all modules in the core app, so no additional Gradle configuration is required when implementing custom processing classes.

### How do I prevent memory leaks when processing large PDFs with PDFBox?

Always use **try-with-resources** when instantiating `PDDocument` and `PDPageContentStream` objects, as demonstrated in [`CropController.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CropController.java) and the WatermarkService example above. This ensures the COS document streams close properly even if processing throws an `IOException`.

### Can I combine PDFBox with external tools like Ghostscript in the same controller?

Yes. The [`ConvertPDFToPDFA.java`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/ConvertPDFToPDFA.java) controller demonstrates this hybrid approach: attempt Ghostscript conversion first, then fall back to PDFBox's `processWithPDFBox` method if the external tool fails or is unavailable. Check for the external tool's presence via configuration properties before invoking it.

### What is the correct package location for new custom controllers?

Place REST controllers in `app/core/src/main/java/stirling/software/SPDF/controller/api/` or a subpackage (such as `api.misc` for miscellaneous tools). The existing component scan automatically picks up any class annotated with `@RestController` in these paths.