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
PDDocumentandPDPageContentStreaminstances close automatically. - Write output to a
ByteArrayOutputStreamto avoid temporary files. - Handle
IOExceptionexplicitly, 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
MultipartFileparameters with@RequestParam. - Return
ResponseEntity<byte[]>withContent-Type: application/pdf. - Set the
Content-Dispositionheader 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.APPENDensures 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:
ConvertPDFToPDFA.java– PDF/A preflight validation and sanitization logic.CropController.java– Coordinate-based page manipulation.PosterPdfController.java– Multi-page generation from single sources.SvgToPdf.java– Graphics2D bridge usage for vector conversion.PdfJsonConversionService.java– Document reconstruction from data models.RepairController.java– Minimal load-save repair patterns.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
PDDocumentlifecycle management with try-with-resources, ensuring zero resource leaks. - REST controllers accept
MultipartFileuploads and returnResponseEntity<byte[]>with proper PDF content headers. - Source files are located in
app/core/src/main/java/stirling/software/SPDF/following strict package conventions (controller.apiandservice). - Testing uses
MockMultipartFileandPDDocument.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →