Codersee

How to Generate a PDF in a Spring Boot REST API with Apache PDFBox and Kotlin

A featured image for category: Spring

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:

image shows a PDF 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 you in the next chapters, how easy and neat the whole process becomes with this library. Boxable, on the other hand, is a great extension for the first one, when it comes to creating 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 data. On the other hand, this structure could be easily replaced with some real-life logic, like fetching from the database for instance.

4. Create Controller

After that, let’s prepare the code responsible for exposing a REST endpoint, which will be used to fetch the PDF. (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 end 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 attachement (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 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 to a page content stream.

In our example, 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

At this point, we should be able to finally generate a first version of the document. 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 other similar tool, as well.

8. Create Table Page

As the next step, 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 the table generation and I believe the above code proves it best.

Basically, to generate a new table we need to create a BaseTable instance and pass the necessary parameters to its constructor. As we can see, 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 being 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

Secondly, 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 the 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 add some security and policies to our document.

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

 

If you find this material useful, you might be interested in my previous articles:

Leave a Reply

Your email address will not be published.

Categories

Author

Hi there! 👋

Hi there! 👋

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

Join the FREE weekly newsletter and get two free eBooks:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy.

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.