1. Introduction
In this guide, I will walk you step by step through the process of generating and securing a PDF in a Spring Boot REST API with Apache PDFBox and Kotlin. After this tutorial you will be able to create PDF reports containing text and tables and expose them through the REST API in Spring Boot. Additionally, I will show you how to secure them with owner and user passwords and apply policies to them.
To better visualize the final effect, these are the two pages generated in this tutorial:
2. Imports
Let’s start everything by adding a few dependencies to our Gradle project:
implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") implementation("org.apache.pdfbox:pdfbox:2.0.24") implementation("com.github.dhorions:boxable:1.6")
Personally, each time I start a new project, I use the Spring Initializr, which allows selecting necessary libraries pretty easily.
Similarly, I did this time, but you might have already noticed two more libraries we will need to generate the PDF:
Basically, the first one is an open-source Java tool for working with PDF documents. I will prove to you in the next chapters, how easy and neat the whole process of creating a PDF in Spring Boot becomes with this library.
Boxable, on the other hand, is a great extension for the first one, which will help us generate tables.
3. Create Test Data Provider
As the next step, let’s implement a logic responsible for example data generation:
object PeopleDataProvider { fun generateTestData(): List<Person> = listOf( Person("John", "Doe", "mail1@codersee.com", 22), Person("Emma", "Smith", "mail2@codersee.com", 20), Person("Wayne", "Johnson", "mail3@codersee.com", 30), Person("Robert", "Robertson", "mail4@codersee.com", 41), Person("Sophia", "Miller", "mail5@codersee.com", 27), Person("Adam", "Williams", "mail6@codersee.com", 52) ) data class Person( val firstName: String, val lastName: String, val email: String, val age: Int ) }
The PeopleDataProvider is a simple Kotlin object with one function returning a list of random people’s data.
In a real-life scenario, this structure can be easily replaced with some other logic, like fetching from the database.
4. Create Controller
After that, let’s prepare the code responsible for exposing a REST endpoint, which will be used to fetch the PDF in our Spring Boot Kotlin app. (And please don’t worry about missing PdfService, we will take care of it in the next chapter):
@RestController class PdfController( private val pdfService: PdfService ) { @PostMapping("/api/report") fun getAllUsersCsvExport(response: HttpServletResponse): ResponseEntity<ByteArray> { val report = pdfService.generate() return ResponseEntity.ok() .contentType(MediaType.APPLICATION_PDF) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.pdf\"") .body(report) } }
To put it simply, the above method handles all POST /api/report requests by invoking the generate() method. It produces a ByteArray object, which ends ups as a response body. Whatsoever, we explicitly specify two additionals headers:
- Content-Type– indicating the original media type of the resource (PDF in our case)
- Content-Disposition– instructing the client, whether the content is expected to be inlined in the browser, or downloaded as an attachment (and I believe there is no doubt about which one we’ve picked here)
5. Create PdfService
Nextly, let’s implement the missing PdfService:
@Service class PdfService( private val firstPageGenerator: FirstPageGenerator ) { fun generate(): ByteArray { val document = PDDocument() val firstPage = firstPageGenerator.generate(document) document.addPage(firstPage) val byteArray = generateByteArray(document) document.close() return byteArray } private fun generateByteArray(document: PDDocument): ByteArray { val outputStream = ByteArrayOutputStream() document.save(outputStream) return outputStream.toByteArray() } }
Just like in the previous example, the compiler complains about missing class and we will get rid of this error in the next chapter. But for now, let’s figure out what’s going on here.
In the first line, we create a PDDocument instance. At this point, it’s just an empty PDF document. Nevertheless, it’s not valid as long as we don’t add at least one page to it. As you might have already noticed, to do that we have to use the addPage() method, which as an argument takes the PDPage instance returned by the FirstPageGenerator.
Finally, we pass the document to the generateByteArray() function, which saves it to the output stream and converts it to the byte array.
6. Implement FirstPageGenerator
With all of that being done, let’s prepare a class responsible for the first page of the PDF:
@Component class FirstPageGenerator { companion object { private const val FONT_SIZE = 50 private const val TITLE = "Example test report" private val FONT = PDType1Font.TIMES_BOLD } fun generate(document: PDDocument): PDPage { val page = PDPage() val titleWidth = calculateTitleWidth() val horizontalOffset = calculateHorizontalOffset(page, titleWidth) val verticalOffset = calculateVerticalOffset(page) val contentStream = PDPageContentStream(document, page) editContent(contentStream, horizontalOffset, verticalOffset) return page } private fun editContent( contentStream: PDPageContentStream, horizontalOffset: Float, verticalOffset: Float ) { contentStream.beginText() contentStream.setFont(FONT, FONT_SIZE.toFloat()) contentStream.newLineAtOffset(horizontalOffset, verticalOffset) contentStream.showText(TITLE) contentStream.endText() contentStream.close() } private fun calculateVerticalOffset(page: PDPage): Float = (page.mediaBox.height) / 2 private fun calculateHorizontalOffset(page: PDPage, titleWidth: Float): Float = (page.mediaBox.width - titleWidth) / 2 private fun calculateTitleWidth(): Float = FONT.getStringWidth(TITLE) / 1000 * FONT_SIZE }
Basically, the whole purpose of this code is to generate a new PDPage instance, which will become the first page of the final document. However, the most important class here is the PDPageContentStream, which provides the ability to write a page content stream.
Moreover, we use the constructor overriding all existing content streams. In simple words, if the page instance passed to it contains any data, it will be removed. If we would like to put multiple content streams to it, then the library provides additional constructors allowing us to explicitly specify the behavior, like appending or prepending.
As you might have already noticed, the whole magic happens inside the editContent() function. The PDPageContentStream provides plenty of methods to work with, but it all depends on the particular things we would like to display. In our case, we write the predefined title with Times-Bold font and size set to 50 in the very center of the page.
Additionally, please remember that after we finish working with the content stream, we must call the close() method.
7. Testing PDF Spring Boot Generator
At this point, we should be able to finally generate the first version of the PDF document in our Spring Boot app. To do so, let’s run the app and invoke the cURL command:
curl -X POST http://localhost:8080/api/report --output file.pdf
As a result, the document should be generated and persisted within the directory. Please keep in mind, that it can be done with Postman, or another similar tool, as well.
8. Create Table Page
With that being done, let’s add a second page to our document.
8.1. Implement TablePageGenerator
Firstly, let’s create a TablePageGenerator class:
@Component class TablePageGenerator { companion object { private const val MARGIN = 30f private const val TOP_MARGIN = 30f private const val BOTTOM_MARGIN = 30f // specified in px private const val ROW_HEIGHT = 20f // % of table width private const val CELL_WIDTH = 20f private val TABLE_WIDTH = PDRectangle.A4.width - 60 private val Y_TABLE_START = PDRectangle.A4.height - 100 } fun generate(document: PDDocument, people: List<PeopleDataProvider.Person>): PDPage { val page = PDPage() val contentStream = PDPageContentStream(document, page) contentStream.beginText() val table = createTable(document, page) addHeaderRow(table) addDataRows(people, table) table.draw() contentStream.endText() contentStream.close() return page } private fun createTable(document: PDDocument, page: PDPage): BaseTable = BaseTable( Y_TABLE_START, Y_TABLE_START, TOP_MARGIN, BOTTOM_MARGIN, TABLE_WIDTH, MARGIN, document, page, true, true ) private fun addHeaderRow(table: BaseTable) { val row: Row<PDPage> = table.createRow(ROW_HEIGHT) createBoldHeaderCell(row, "Number") createBoldHeaderCell(row, "First Name") createBoldHeaderCell(row, "Last Name") createBoldHeaderCell(row, "Email") createBoldHeaderCell(row, "Age") table.addHeaderRow(row) } private fun createBoldHeaderCell(row: Row<PDPage>, value: String) { row .createCell(CELL_WIDTH, value) .font = PDType1Font.TIMES_BOLD } private fun addDataRows( people: List<PeopleDataProvider.Person>, table: BaseTable ) { people.forEachIndexed { index, person -> val row = table.createRow(ROW_HEIGHT) createCell(row, (index + 1).toString()) createCell(row, person.firstName) createCell(row, person.lastName) createCell(row, person.email) createCell(row, person.age.toString()) } } private fun createCell(row: Row<PDPage>, value: String) { row .createCell(value) .font = PDType1Font.TIMES_ROMAN } }
Just like in the previous example, everything starts and ends with the PDPage and PDPageContentStream instances. However, this time, we make use of the Boxable library. I’ve mentioned in the beginning that it really simplifies the whole process of table generation for PDF in Spring Boot and I believe the above code proves it best.
As we can see, to generate a new table we need to create a BaseTable instance and pass the necessary parameters to its constructor. Additionally, all of the arguments taken from the companion object are pretty descriptive. Nevertheless, as a word of explanation- the two last boolean flags set the drawLines and the drawContent to true. As always, I highly encourage you to check it out with different, custom settings.
After that is done, we add the header row and data rows for each person from the provided list. To put it simply, we use the table instance, to add a new Row object with the createRow method, which is then used to add cells with the createCell function.
8.2. Edit PdfService
Nextly, we need to get back to the PdfService class and make use of our new class:
@Component class PdfService( private val firstPageGenerator: FirstPageGenerator, private val tablePageGenerator: TablePageGenerator ) { fun generate(): ByteArray { val document = PDDocument() val people = PeopleDataProvider.generateTestData() val firstPage = firstPageGenerator.generate(document) val tablePage = tablePageGenerator.generate(document, people) document.addPage(firstPage) document.addPage(tablePage) //... the rest of the code
Similarly to the first page, we’ve just added the TablePageGenerator to the constructor (line 4), and added to the document (line 15) the PDPage instance (line 12) generated with the data from our test provider (line 10).
After it’s done, let’s rerun the command from chapter 5. As a result, we should see that the generated PDF contains two, beautiful pages with a title and a table.
9. Secure The Document
Finally, let’s learn how to secure a PDF generated in the Spring Boot app and set additional policies.
9.1. Create ProtectionPolicyService
Let’s add the ProtectionPolicyService:
@Component class ProtectionPolicyService { companion object { // either 40, 128 or 256 private const val ENCRYPTION_KEY_LENGTH = 256 private const val OWNER_PASSWORD = "owner" private const val USER_PASSWORD = "user" } fun generateStandardProtectionPolicy(): StandardProtectionPolicy { val accessPermission = AccessPermission() accessPermission.setCanPrint(false) accessPermission.setCanExtractContent(false) return getStandardProtectionPolicy(accessPermission) } private fun getStandardProtectionPolicy(accessPermission: AccessPermission): StandardProtectionPolicy { val protectionPolicy = StandardProtectionPolicy(OWNER_PASSWORD, USER_PASSWORD, accessPermission) protectionPolicy.encryptionKeyLength = ENCRYPTION_KEY_LENGTH return protectionPolicy } }
The StandardProtectionPolicy is a simpler implementation of the ProtectionPolicy class. With it we can configure password-based protection and specific permissions for users. It’s worth mentioning, that it allows us to only distinguish between the owner (full permissions) and a “standard” user (with permissions defined by the AccessPermission instance). If we would like to gain more control over the recipients’ privileges, then the PublicKeyProtectionPolicy would be the better choice. In the contrary, it allows us to handle multiple recipients with different permissions.
But for now, let’s focus on the code we’ve just written. I’ve mentioned above the AccessPermission class. As we can see, it allows us to define what exactly users can or can not do. By default, all permissions are granted. But given the above snippet we can clearly see that with this configuration users won’t be able to print or extract the content of the document. Nevertheless, it’s just a small part of all permissions we can grant and I highly encourage you to check out other possibilities.
Finally, we set the owner’s and users’ passwords followed by the encryption key length.
9.2. Modify PdfService
As the last step, we need to make use of the created code:
@Service class PdfService( private val firstPageGenerator: FirstPageGenerator, private val tablePageGenerator: TablePageGenerator, private val protectionPolicyService: ProtectionPolicyService ) { fun generate(): ByteArray { val document = PDDocument() val people = PeopleDataProvider.generateTestData() val firstPage = firstPageGenerator.generate(document) val tablePage = tablePageGenerator.generate(document, people) val protectionPolicy = protectionPolicyService.generateStandardProtectionPolicy() document.addPage(firstPage) document.addPage(tablePage) document.protect(protectionPolicy) //... the rest of the code
Similarly to the 8.2, we’ve added the constructor parameter (line 5) and protected the document (line 18) with the generated policy (line 14).
And again, let’s rerun the application and invoke the test command to validate that everything behaves as expected. We should be able to print the document specifying the owner password. In contrary, it should be forbidden when the standard user password has been used.
10. Generate PDF With Spring Boot Summary
That’s all for today’s tutorial. I really hope that you enjoyed it and that it will help you generating your own PDFs using Apache PDFBox. The knowledge you’ve gathered today should be enough to start exploring this library and apply functionalities in your own use-cases.
Finally, if you would like to get the source code, please visit this GitHub repository.
As always, if you would like to ask about anything or need some more explanation, please do it in the comment section below, or by using the contact form.