Generate PDF Reports
- Add the OpenPDF Dependency
- Display with IFrame (Browser PDF Viewer)
- Offering the PDF as a Download
- Display with PDF Viewer Add-On
- Display with PDF.js (Custom Viewer)
- Choosing a Viewer
- Beyond the Basics
This article shows how to generate a PDF document using OpenPDF and display it in a Vaadin view. It covers three approaches: using the browser’s built-in PDF viewer via IFrame, using the PDF Viewer add-on for a feature-rich viewer, and building a custom viewer with PDF.js for full control. For serving files as downloads instead, see Handle File Downloads.
Add the OpenPDF Dependency
OpenPDF is an open-source PDF library for Java (LGPL/MPL license). Add it to your project’s pom.xml (check Maven Central for the latest version):
Source code
XML
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>3.0.1</version>
</dependency>Display with IFrame (Browser PDF Viewer)
The simplest approach is to embed the PDF in an IFrame, which uses the browser’s built-in PDF viewer. This gives users familiar controls for page navigation, zoom, and printing with no extra dependencies.
A self-contained view that generates a simple PDF report and embeds it inline:
Source code
Java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.time.LocalDate;
import org.openpdf.text.Document;
import org.openpdf.text.FontFactory;
import org.openpdf.text.Paragraph;
import org.openpdf.text.pdf.PdfWriter;
import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.streams.DownloadHandler;
import com.vaadin.flow.server.streams.DownloadResponse;
@Route("pdf-report")
public class PdfReportView extends VerticalLayout {
public PdfReportView() {
IFrame pdfViewer = new IFrame( 1
DownloadHandler.fromInputStream(event -> { 2
try {
byte[] pdf = generateReport();
return new DownloadResponse(
new ByteArrayInputStream(pdf),
"report.pdf",
"application/pdf",
pdf.length);
} catch (Exception e) {
return DownloadResponse.error(500);
}
}));
pdfViewer.setWidth("100%");
pdfViewer.setHeight("800px"); 3
add(pdfViewer);
}
private byte[] generateReport() { 4
ByteArrayOutputStream out = new ByteArrayOutputStream();
Document document = new Document();
PdfWriter.getInstance(document, out);
document.open();
document.add(new Paragraph("Monthly Report",
FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18)));
document.add(new Paragraph("Generated: " + LocalDate.now()));
document.add(new Paragraph(" "));
document.add(new Paragraph(
"This is a sample report. Replace this with your own content."));
document.close();
return out.toByteArray();
}
}-
IFramedisplays the PDF inline using the browser’s built-in PDF viewer. It automatically sets the content disposition toinline. -
DownloadHandler.fromInputStream()provides the PDF bytes when the browser loads the iframe. -
Set an explicit height — iframes don’t grow with their content.
-
OpenPDF’s
Document/PdfWriterAPI. Replace this method with your own report logic.
Offering the PDF as a Download
To add a download link alongside the embedded viewer, reuse the same generation logic with an Anchor:
Source code
Java
Anchor downloadLink = new Anchor(
DownloadHandler.fromInputStream(event -> {
byte[] pdf = generateReport();
return new DownloadResponse(
new ByteArrayInputStream(pdf),
"report.pdf",
"application/pdf",
pdf.length);
}), "Download PDF");See Handle File Downloads for more download patterns.
Display with PDF Viewer Add-On
The PDF Viewer add-on is a ready-made Vaadin component that wraps PDF.js with built-in controls for page navigation, zoom, thumbnails, printing, and download. It gives you a full-featured PDF viewer without writing any client-side code.
|
Note
| PDF Viewer is a Component Factory add-on. Component Factory components are developed by Vaadin experts but are not maintained as part of the Vaadin platform. They are not covered by the Vaadin warranty, but on-demand maintenance is available through Expert on Demand. |
Install the add-on from the Vaadin Directory and use it as a drop-in viewer:
Source code
Java
import com.vaadin.componentfactory.pdfviewer.PdfViewer;
import com.vaadin.flow.server.streams.DownloadHandler;
PdfViewer pdfViewer = new PdfViewer();
pdfViewer.setSrc(DownloadHandler.fromInputStream(event -> {
try {
byte[] pdf = generateReport();
return new DownloadResponse(
new ByteArrayInputStream(pdf),
"report.pdf",
"application/pdf",
pdf.length);
} catch (Exception e) {
return DownloadResponse.error(500);
}
}));
pdfViewer.setWidth("100%");
pdfViewer.setHeight("800px");
add(pdfViewer);Display with PDF.js (Custom Viewer)
PDF.js is Mozilla’s JavaScript PDF renderer. It draws PDF pages onto HTML <canvas> elements, giving you full control over the viewing experience. This is useful when you need a consistent look across browsers, want to build custom viewer controls, or need to integrate features like text search or annotations.
This approach requires a client-side LitElement web component and a server-side Java wrapper.
Client-Side Component
Create a pdf-viewer.ts file in your src/main/frontend/ directory:
Source code
TypeScript
import { LitElement, html, css, PropertyValues } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 1
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).href;
@customElement('pdf-viewer')
export class PdfViewer extends LitElement {
static styles = css`
:host {
display: block;
overflow: auto;
background: var(--lumo-contrast-90pct, #525659);
font-family: var(--lumo-font-family, sans-serif);
}
canvas {
display: block;
margin: var(--lumo-space-s, 8px) auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.error {
color: var(--lumo-primary-contrast-color, white);
padding: var(--lumo-space-m, 16px);
text-align: center;
}
.truncated {
color: var(--lumo-contrast-30pct, #ccc);
padding: var(--lumo-space-m, 16px) var(--lumo-space-m, 16px) var(--lumo-space-l, 24px);
text-align: center;
font-size: var(--lumo-font-size-s, 14px);
}
`;
@property() src = ''; 2
@property({ type: Number, attribute: 'max-pages' }) maxPages = 50; 3
@state() private _error = '';
@state() private _truncated = false;
@state() private _totalPages = 0;
private _loadTask: ReturnType<typeof pdfjsLib.getDocument> | null = null;
render() {
if (this._error) {
return html`<div class="error">${this._error}</div>`;
}
return html`
<div id="pages"></div>
${this._truncated
? html`<div class="truncated">
Showing ${this.maxPages} of ${this._totalPages} pages.
Download the PDF to see the full document.
</div>`
: ''}
`;
}
disconnectedCallback() { 4
super.disconnectedCallback();
if (this._loadTask) {
this._loadTask.destroy();
this._loadTask = null;
}
}
protected updated(changed: PropertyValues) {
if (changed.has('src') && this.src) {
this._loadPdf();
}
}
private async _loadPdf() {
if (this._loadTask) {
this._loadTask.destroy();
this._loadTask = null;
}
this._error = '';
this._truncated = false;
this._totalPages = 0;
await this.updateComplete;
const container = this.shadowRoot!.querySelector('#pages') as HTMLDivElement;
if (!container) return;
container.innerHTML = '';
try {
this._loadTask = pdfjsLib.getDocument(this.src);
const pdf = await this._loadTask.promise;
this._totalPages = pdf.numPages;
const pagesToRender = Math.min(pdf.numPages, this.maxPages);
this._truncated = pdf.numPages > this.maxPages;
const availableWidth = this.clientWidth - 32; 5
for (let i = 1; i <= pagesToRender; i++) {
const page = await pdf.getPage(i);
const unscaled = page.getViewport({ scale: 1 });
const scale = availableWidth > 0
? availableWidth / unscaled.width
: 1.5;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
container.appendChild(canvas);
await page.render({
canvas,
canvasContext: canvas.getContext('2d')!,
viewport,
}).promise;
}
} catch (err: any) {
if (err?.name !== 'RenderingCancelledException') {
this._error = `Failed to load PDF: ${err?.message || err}`;
}
}
}
}-
PDF.js uses a Web Worker for parsing. The worker source must be set before loading any documents.
-
The
srcproperty receives a URL to the PDF. Vaadin sets this attribute from the server side. -
Hard upper limit on rendered pages. All pages are rendered up-front onto canvas elements, so a large PDF would consume a lot of memory. The default of 50 is a safe ceiling for most report-viewing use cases.
-
PDF.js holds document data in the Web Worker’s memory. Calling
destroy()when the component is removed from the DOM prevents a memory leak. -
The scale is computed from the container width so pages fit without horizontal scrolling. The 32px margin leaves room for the box shadow.
Server-Side Java Component
Create a PdfViewer.java wrapper that maps to the custom element:
Source code
Java
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.server.streams.DownloadHandler;
@Tag("pdf-viewer")
@NpmPackage(value = "pdfjs-dist", version = "5.4.624") 1
@JsModule("./pdf-viewer.ts") 2
public class PdfViewer extends Component implements HasSize {
public PdfViewer() {
}
public PdfViewer(DownloadHandler downloadHandler) {
setSrc(downloadHandler);
}
public void setSrc(DownloadHandler downloadHandler) {
getElement().setAttribute("src", downloadHandler); 3
}
public void setMaxPages(int maxPages) {
getElement().setProperty("maxPages", maxPages); 4
}
}-
@NpmPackagetells Vaadin to installpdfjs-distfrom npm during the build. -
@JsModulelinks to the LitElement file created above. -
Vaadin converts a
DownloadHandlerto a URL when set as an element attribute. This is the same mechanismIFrameuses internally. -
Sets the page limit on the client-side component. The default is 50.
Using the Component
The PdfViewer component is a drop-in replacement for IFrame:
Source code
Java
PdfViewer pdfViewer = new PdfViewer(
DownloadHandler.fromInputStream(event -> {
try {
byte[] pdf = generateReport();
return new DownloadResponse(
new ByteArrayInputStream(pdf),
"report.pdf",
"application/pdf",
pdf.length);
} catch (Exception e) {
return DownloadResponse.error(500);
}
}));
pdfViewer.setWidth("100%");
pdfViewer.setHeight("800px");
add(pdfViewer);The server-side API is identical to the IFrame version. The difference is entirely in how the browser renders the PDF.
Choosing a Viewer
| IFrame (Browser Viewer) | PDF Viewer Add-On | PDF.js (Custom Viewer) | |
|---|---|---|---|
Setup | No extra dependencies | Add-on from Vaadin Directory | Requires a LitElement component + npm package |
UI controls | Browser-provided (zoom, page nav, print, download) | Built-in (zoom, page nav, thumbnails, print, download) | You build your own (or start minimal) |
Cross-browser consistency | Varies — each browser has a different PDF viewer | Identical rendering everywhere | Identical rendering everywhere |
Customization | None — the viewer is a browser black box | Toolbar options and labels | Full control over layout, styling, and behavior |
Text search | Built into most browser viewers | Built-in | Requires PDF.js text layer (extra code) |
Mobile | Some mobile browsers don’t render PDFs inline | Works on all browsers that support | Works on all browsers that support |
Bundle size | Zero | ~400 KB (PDF.js library + worker, bundled) | ~400 KB (PDF.js library + worker) |
Maintenance | N/A — browser built-in | Community / Expert on Demand | You maintain the component |
Use IFrame when you want a quick, zero-dependency solution and the browser’s built-in viewer is good enough. Use the PDF Viewer add-on when you want a feature-rich viewer without building client-side code. Use a custom PDF.js component when you need full control over the viewer’s layout, styling, and behavior.
Beyond the Basics
-
Tables and styling — OpenPDF supports tables (
PdfPTable), fonts, colors, and page layouts for richer reports. -
Streaming large reports — for large documents, write directly to the
OutputStreamfromDownloadEventinstead of buffering in memory. See Handle File Downloads for streaming patterns. -
Alternative libraries — Apache PDFBox and iText are popular alternatives with different trade-offs in API style and licensing.