> Markdown version of [Generate PDF Reports](https://vaadin.com/docs/next/building-apps/forms-data/generate-pdf-reports). Section index: [llms.txt](https://vaadin.com/docs/next/building-apps/llms.txt)

# Generate PDF Reports

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](https://vaadin.com/directory/component/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](https://vaadin.com/docs/next/building-apps/forms-data/handle-downloads.md).

## <a id="add-the-openpdf-dependency"></a>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](https://central.sonatype.com/artifact/com.github.librepdf/openpdf) for the latest version):

```xml
<dependency>
    <groupId>com.github.librepdf</groupId>
    <artifactId>openpdf</artifactId>
    <version>3.0.1</version>
</dependency>
```

## <a id="display-with-iframe-browser-pdf-viewer"></a>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:

```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();
    }
}
```

1. `IFrame` displays the PDF inline using the browser’s built-in PDF viewer. It automatically sets the content disposition to `inline`.

2. `DownloadHandler.fromInputStream()` provides the PDF bytes when the browser loads the iframe.

3. Set an explicit height — iframes don’t grow with their content.

4. OpenPDF’s `Document` / `PdfWriter` API. Replace this method with your own report logic.

## <a id="offering-the-pdf-as-a-download"></a>Offering the PDF as a Download

To add a download link alongside the embedded viewer, reuse the same generation logic with an `Anchor`:

```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](https://vaadin.com/docs/next/building-apps/forms-data/handle-downloads.md) for more download patterns.

## <a id="display-with-pdf-viewer-add-on"></a>Display with PDF Viewer Add-On

The [PDF Viewer](https://vaadin.com/directory/component/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](https://vaadin.com/components/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](https://vaadin.com/directory/component/pdf-viewer) and use it as a drop-in viewer:

```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);
```

## <a id="display-with-pdf-js-custom-viewer"></a>Display with PDF.js (Custom Viewer)

[PDF.js](https://mozilla.github.io/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.

### <a id="client-side-component"></a>Client-Side Component

Create a `pdf-viewer.ts` file in your `src/main/frontend/` directory:

```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}`;
      }
    }
  }
}
```

1. PDF.js uses a Web Worker for parsing. The worker source must be set before loading any documents.

2. The `src` property receives a URL to the PDF. Vaadin sets this attribute from the server side.

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

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

5. The scale is computed from the container width so pages fit without horizontal scrolling. The 32px margin leaves room for the box shadow.

> **Note:** The CSS uses [Lumo](https://vaadin.com/docs/next/styling/themes/lumo/lumo-style-properties.md) theme custom properties (e.g., `--lumo-contrast-90pct`, `--lumo-font-family`). If your application uses the [Aura](https://vaadin.com/docs/next/styling/themes/aura.md) theme, replace these with the corresponding Aura custom properties.

### <a id="server-side-java-component"></a>Server-Side Java Component

Create a `PdfViewer.java` wrapper that maps to the custom element:

```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)
    }
}
```

1. `@NpmPackage` tells Vaadin to install `pdfjs-dist` from npm during the build.

2. `@JsModule` links to the LitElement file created above.

3. Vaadin converts a `DownloadHandler` to a URL when set as an element attribute. This is the same mechanism `IFrame` uses internally.

4. Sets the page limit on the client-side component. The default is 50.

### <a id="using-the-component"></a>Using the Component

The `PdfViewer` component is a drop-in replacement for `IFrame`:

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

## <a id="choosing-a-viewer"></a>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 `<canvas>`          | Works on all browsers that support `<canvas>`   |
| 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.

## <a id="beyond-the-basics"></a>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 `OutputStream` from `DownloadEvent` instead of buffering in memory. See [Handle File Downloads](https://vaadin.com/docs/next/building-apps/forms-data/handle-downloads.md) for streaming patterns.

- **Alternative libraries** — Apache PDFBox and iText are popular alternatives with different trade-offs in API style and licensing.
