PDF Export

PDF export converts application data into a fixed-layout document that looks the same on every device and prints cleanly.In Spring Boot, a PDF library (here OpenPDF) builds the document in memory as a byte[], and the controller returns it with the application/pdf content type.Unlike CSV or Excel, a PDF is meant for reading and printing, not for editing or calculating.It is used for academic reports, certificates, invoices, receipts, and official letters.

Features

  • Printable, fixed-layout output that looks identical everywhere
  • Download as a file or open directly in the browser (?inline=true)
  • Fonts, colors, tables, and alignment control with OpenPDF
  • Table header row that repeats on every page
  • Page numbers in the footer
  • Alternating row colors and a colored PASS/FAIL result
  • Built from database data, with calculations (total, percentage, grade) done in Java
  • Subject-wise marks saved with the student through a one-to-many relationship

Hands-on Experiment: Creating a spring boot application to understand how application data can be transformed into printable PDF documents

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.properties

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: pdfexport
  • Name: pdfexport
  • Package name: com.example.pdfexport
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver.

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-pdf-demo</artifactId>
    <version>1.0.0</version>
    <name>student-pdf-demo</name>
    <description>PDF Export - Student academic report</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- OpenPDF: free library to create PDF documents (package com.lowagie.text) -->
        <dependency>
            <groupId>com.github.librepdf</groupId>
            <artifactId>openpdf</artifactId>
            <version>1.3.30</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.properties

spring.application.name=student-pdf-demo

# ---------- MySQL ----------
# Create the database first:  CREATE DATABASE student_pdf_db;
spring.datasource.url=jdbc:mysql://localhost:3306/student_pdf_db
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
server.port=8082

# ---------- Report header ----------
app.college.name=Sunrise Institute of TechnologyCode language: PHP (php)

Step 5: Create Entity Class

package com.example.pdfexport.entity;

import jakarta.persistence.*;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    private String course;

    private String department;

    private LocalDate admissionDate;

    // Subject-wise marks; saved together with the student (cascade)
    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<SubjectMark> marks = new ArrayList<>();

    public Student() {
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getCourse() { return course; }
    public void setCourse(String course) { this.course = course; }

    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }

    public LocalDate getAdmissionDate() { return admissionDate; }
    public void setAdmissionDate(LocalDate admissionDate) { this.admissionDate = admissionDate; }

    public List<SubjectMark> getMarks() { return marks; }
    public void setMarks(List<SubjectMark> marks) { this.marks = marks; }
}
package com.example.pdfexport.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;

@Entity
@Table(name = "subject_marks")
public class SubjectMark {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String subjectName;

    @Column(nullable = false)
    private Integer marks;

    // Defaults to 100 when not sent
    private Integer maxMarks;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "student_id")
    @JsonIgnore
    private Student student;

    public SubjectMark() {
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getSubjectName() { return subjectName; }
    public void setSubjectName(String subjectName) { this.subjectName = subjectName; }

    public Integer getMarks() { return marks; }
    public void setMarks(Integer marks) { this.marks = marks; }

    public Integer getMaxMarks() { return maxMarks; }
    public void setMaxMarks(Integer maxMarks) { this.maxMarks = maxMarks; }

    public Student getStudent() { return student; }
    public void setStudent(Student student) { this.student = student; }
}

Step 6: Create below Repository Interface

package com.example.pdfexport.repository;

import com.example.pdfexport.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    boolean existsByEmail(String email);
}

Step 7: Create below Service class

package com.example.pdfexport.service;

import com.example.pdfexport.entity.Student;
import com.example.pdfexport.entity.SubjectMark;
import com.example.pdfexport.repository.StudentRepository;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.HeaderFooter;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.time.LocalDate;
import java.util.List;

@Service
public class StudentService {

    private static final Color PRIMARY = new Color(31, 78, 121);
    private static final Color LIGHT = new Color(238, 241, 244);
    private static final Color GREEN = new Color(30, 130, 76);
    private static final Color RED = new Color(192, 57, 43);

    private final StudentRepository studentRepository;

    @Value("${app.college.name}")
    private String collegeName;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    // ---------------- Student operations ----------------

    @Transactional
    public Student create(Student student) {
        if (studentRepository.existsByEmail(student.getEmail())) {
            throw new IllegalArgumentException("Email already registered: " + student.getEmail());
        }
        // Link every mark to its student and apply the default maximum
        if (student.getMarks() != null) {
            for (SubjectMark m : student.getMarks()) {
                m.setStudent(student);
                if (m.getMaxMarks() == null) {
                    m.setMaxMarks(100);
                }
            }
        }
        return studentRepository.save(student);
    }

    @Transactional(readOnly = true)
    public List<Student> getAll() {
        List<Student> students = studentRepository.findAll();
        students.forEach(s -> s.getMarks().size());   // load marks before the transaction ends
        return students;
    }

    @Transactional(readOnly = true)
    public Student getById(Long id) {
        Student student = studentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Student not found: " + id));
        student.getMarks().size();
        return student;
    }

    // ---------------- PDF generation ----------------

    @Transactional(readOnly = true)
    public byte[] generateAcademicReport(Long id) throws DocumentException {
        Student student = getById(id);
        List<SubjectMark> marks = student.getMarks();

        // ----- calculations -----
        int total = 0;
        int maxTotal = 0;
        boolean allSubjectsPassed = true;
        for (SubjectMark m : marks) {
            total += m.getMarks();
            maxTotal += m.getMaxMarks();
            if (percent(m.getMarks(), m.getMaxMarks()) < 40) {
                allSubjectsPassed = false;
            }
        }
        double overallPercent = maxTotal == 0 ? 0 : percent(total, maxTotal);
        boolean pass = !marks.isEmpty() && allSubjectsPassed;

        // ----- fonts -----
        Font collegeFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 20, PRIMARY);
        Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14, Color.DARK_GRAY);
        Font labelFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 10, Color.DARK_GRAY);
        Font textFont = FontFactory.getFont(FontFactory.HELVETICA, 10, Color.BLACK);
        Font headFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 10, Color.WHITE);
        Font smallFont = FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 8, Color.GRAY);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Document document = new Document(PageSize.A4, 40, 40, 50, 50);
        PdfWriter.getInstance(document, out);

        // page number in the footer
        HeaderFooter footer = new HeaderFooter(new Phrase("Page ", smallFont), true);
        footer.setAlignment(Element.ALIGN_CENTER);
        footer.setBorder(Rectangle.NO_BORDER);
        document.setFooter(footer);

        document.open();

        // ----- title -----
        Paragraph college = new Paragraph(collegeName, collegeFont);
        college.setAlignment(Element.ALIGN_CENTER);
        document.add(college);

        Paragraph title = new Paragraph("ACADEMIC REPORT", titleFont);
        title.setAlignment(Element.ALIGN_CENTER);
        title.setSpacingAfter(16);
        document.add(title);

        // ----- student details (2 label/value pairs per row) -----
        PdfPTable details = new PdfPTable(new float[]{1.2f, 2.3f, 1.2f, 2.3f});
        details.setWidthPercentage(100);
        addDetail(details, "Student ID", String.valueOf(student.getId()), labelFont, textFont);
        addDetail(details, "Name", student.getName(), labelFont, textFont);
        addDetail(details, "Email", student.getEmail(), labelFont, textFont);
        addDetail(details, "Course", nullSafe(student.getCourse()), labelFont, textFont);
        addDetail(details, "Department", nullSafe(student.getDepartment()), labelFont, textFont);
        addDetail(details, "Admission Date", String.valueOf(student.getAdmissionDate() == null ? "" : student.getAdmissionDate()),
                labelFont, textFont);
        document.add(details);

        // ----- marks table -----
        Paragraph marksTitle = new Paragraph("Subject-wise Performance", titleFont);
        marksTitle.setSpacingBefore(18);
        marksTitle.setSpacingAfter(8);
        document.add(marksTitle);

        PdfPTable table = new PdfPTable(new float[]{0.6f, 3f, 1.2f, 1.2f, 1.2f, 1f});
        table.setWidthPercentage(100);
        String[] heads = {"#", "Subject", "Marks", "Max", "Percent", "Grade"};
        for (String h : heads) {
            PdfPCell cell = new PdfPCell(new Phrase(h, headFont));
            cell.setBackgroundColor(PRIMARY);
            cell.setHorizontalAlignment(Element.ALIGN_CENTER);
            cell.setPadding(6);
            table.addCell(cell);
        }
        table.setHeaderRows(1);   // header repeats on every page

        int index = 1;
        for (SubjectMark m : marks) {
            Color rowColor = (index % 2 == 0) ? LIGHT : Color.WHITE;
            double p = percent(m.getMarks(), m.getMaxMarks());
            addBodyCell(table, String.valueOf(index), textFont, rowColor, Element.ALIGN_CENTER);
            addBodyCell(table, m.getSubjectName(), textFont, rowColor, Element.ALIGN_LEFT);
            addBodyCell(table, String.valueOf(m.getMarks()), textFont, rowColor, Element.ALIGN_CENTER);
            addBodyCell(table, String.valueOf(m.getMaxMarks()), textFont, rowColor, Element.ALIGN_CENTER);
            addBodyCell(table, String.format("%.1f%%", p), textFont, rowColor, Element.ALIGN_CENTER);
            addBodyCell(table, grade(p), textFont, rowColor, Element.ALIGN_CENTER);
            index++;
        }
        document.add(table);

        // ----- summary -----
        PdfPTable summary = new PdfPTable(new float[]{2f, 1f});
        summary.setWidthPercentage(50);
        summary.setHorizontalAlignment(Element.ALIGN_RIGHT);
        summary.setSpacingBefore(14);
        addSummary(summary, "Total Marks", total + " / " + maxTotal, labelFont, textFont);
        addSummary(summary, "Percentage", String.format("%.2f%%", overallPercent), labelFont, textFont);
        addSummary(summary, "Overall Grade", marks.isEmpty() ? "-" : grade(overallPercent), labelFont, textFont);

        Font resultFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 11, pass ? GREEN : RED);
        addSummary(summary, "Result", marks.isEmpty() ? "NO MARKS" : (pass ? "PASS" : "FAIL"), labelFont, resultFont);
        document.add(summary);

        // ----- footer note -----
        Paragraph note = new Paragraph(
                "Pass mark: 40% in every subject.  Report generated on " + LocalDate.now()
                        + ". This is a system-generated document.", smallFont);
        note.setSpacingBefore(30);
        document.add(note);

        document.close();
        return out.toByteArray();
    }

    // ---------------- helpers ----------------

    private double percent(int marks, int max) {
        return max == 0 ? 0 : (marks * 100.0) / max;
    }

    private String grade(double percent) {
        if (percent >= 90) return "A+";
        if (percent >= 80) return "A";
        if (percent >= 70) return "B";
        if (percent >= 60) return "C";
        if (percent >= 40) return "D";
        return "F";
    }

    private String nullSafe(String value) {
        return value == null ? "" : value;
    }

    private void addDetail(PdfPTable table, String label, String value, Font labelFont, Font valueFont) {
        PdfPCell l = new PdfPCell(new Phrase(label, labelFont));
        l.setBorder(Rectangle.BOTTOM);
        l.setBorderColor(Color.LIGHT_GRAY);
        l.setPadding(6);
        table.addCell(l);

        PdfPCell v = new PdfPCell(new Phrase(value, valueFont));
        v.setBorder(Rectangle.BOTTOM);
        v.setBorderColor(Color.LIGHT_GRAY);
        v.setPadding(6);
        table.addCell(v);
    }

    private void addBodyCell(PdfPTable table, String text, Font font, Color background, int align) {
        PdfPCell cell = new PdfPCell(new Phrase(text, font));
        cell.setBackgroundColor(background);
        cell.setHorizontalAlignment(align);
        cell.setPadding(6);
        cell.setBorderColor(Color.LIGHT_GRAY);
        table.addCell(cell);
    }

    private void addSummary(PdfPTable table, String label, String value, Font labelFont, Font valueFont) {
        PdfPCell l = new PdfPCell(new Phrase(label, labelFont));
        l.setBackgroundColor(LIGHT);
        l.setPadding(6);
        l.setBorderColor(Color.LIGHT_GRAY);
        table.addCell(l);

        PdfPCell v = new PdfPCell(new Phrase(value, valueFont));
        v.setHorizontalAlignment(Element.ALIGN_CENTER);
        v.setPadding(6);
        v.setBorderColor(Color.LIGHT_GRAY);
        table.addCell(v);
    }
}

Step 8: Create Controller class

package com.example.pdfexport.controller;

import com.example.pdfexport.entity.Student;
import com.example.pdfexport.service.StudentService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @PostMapping
    public ResponseEntity<?> create(@RequestBody Student student) {
        try {
            return ResponseEntity.status(HttpStatus.CREATED).body(studentService.create(student));
        } catch (IllegalArgumentException ex) {
            return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
        }
    }

    @GetMapping
    public List<Student> getAll() {
        return studentService.getAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<?> getById(@PathVariable Long id) {
        try {
            return ResponseEntity.ok(studentService.getById(id));
        } catch (IllegalArgumentException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
        }
    }

    // PDF report. Default = download. Add ?inline=true to open it inside the browser tab.
    @GetMapping("/{id}/report/pdf")
    public ResponseEntity<?> academicReport(@PathVariable Long id,
                                            @RequestParam(defaultValue = "false") boolean inline) {
        try {
            byte[] pdf = studentService.generateAcademicReport(id);
            String disposition = (inline ? "inline" : "attachment")
                    + "; filename=\"student-report-" + id + ".pdf\"";
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, disposition)
                    .contentType(MediaType.APPLICATION_PDF)
                    .contentLength(pdf.length)
                    .body(pdf);
        } catch (IllegalArgumentException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
        } catch (Exception ex) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("error", "PDF generation failed: " + ex.getMessage()));
        }
    }
}

Step 9: Main Application

package com.example.pdfexport;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PdfExportApplication {

    public static void main(String[] args) {
        SpringApplication.run(PdfExportApplication.class, args);
    }
}

Step 10: Run the StudentRestCurdApplication

Scroll to Top