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

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 processWithPDFBox(PDDocument, int) validates and sanitizes documents when Ghostscript is unavailable.
Page Cropping CropController.java cropWithPDFBox extracts rectangular regions using PDPage and PDPageContentStream.
Poster Generation PosterPdfController.java Splits pages into grids by copying content streams into new PDDocument instances.
SVG to PDF SvgToPdf.java Renders vector graphics via PDFBoxGraphics2D onto new pages.
JSON Reconstruction PdfJsonConversionService.java Rebuilds documents from JSON models, handling fonts and text encoding.
Repair Fallback 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 and PosterPdfController.java.

WatermarkService.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:

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.

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:

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →