How to Convert Excel to PDF in Java

Converting Excel to PDF in Java helps applications share reports, invoices, and other spreadsheet documents in a format intended for viewing and printing. In this guide, we’ll use Axyra Sheets for Java to render an Excel workbook as PDF on the server, without installing Excel or running an Office process. We’ll start with a basic conversion, then configure sheet selection, page layout, fonts, watermarks, and PDF output options. For a short walkthrough of Axyra before diving in, see Quick Start.

The one-liner

import io.keikai.axyra.sheets.Workbook;
import java.nio.file.Path;

try (Workbook wb = Workbook.open(Path.of("report.xlsx"))) {
    wb.renderPdf(Path.of("report.pdf"));
}

That is the whole conversion. Everything below is about controlling the result.

To stream it to an HTTP response instead of writing a file:

try (Workbook wb = Workbook.open(Path.of("report.xlsx"))) {
    byte[] pdf = wb.renderPdf(PdfOptions.builder().build());
    response.setContentType("application/pdf");
    response.getOutputStream().write(pdf);
}

Choosing what goes in

By default every visible worksheet is rendered. PdfOptions narrows it and adds document metadata.

import io.keikai.axyra.sheets.io.PdfOptions;

PdfOptions options = PdfOptions.builder()
        .title("Q3 Report")
        .author("Finance")
        .sheetNames("Summary", "Detail")   // by name (case-sensitive)
        .pageRange("1-4")                  // only these pages of the result
        .bookmarks(true)                   // one outline entry per sheet
        .build();

wb.renderPdf(Path.of("report.pdf"), options);

Sheets can also be selected by 0-based index with .sheets(0, 2, 3). Listing a hidden sheet explicitly includes it, which is how you render something the workbook does not show interactively.

sheetNames is case-sensitive and an unknown name fails the render rather than being silently skipped. Feed it names you read back from wb.sheet(i).name(), not names typed by hand.

Page layout

Pagination comes from the workbook’s own page setup, so the highest-leverage control is on the Sheet, before you render:

import io.keikai.axyra.sheets.Sheet;

Sheet sheet = wb.sheet("Summary");

sheet.setPrintArea("A1:H60");        // or null to clear
sheet.setLandscape(true);
sheet.setPaperSize(9);               // ECMA-376 ST_PaperSize: 1=Letter, 9=A4
sheet.setPageMargins(0.5, 0.5, 0.75, 0.75, 0.3, 0.3);  // L R T B header footer, inches
sheet.setPrintTitleRows("$1:$1");    // repeat the header row on every page
sheet.setPrintGridlines(true);
sheet.setPageScale(80);              // percent; disables fit-to-page

To make a wide sheet fit without hand-tuning the scale, override fit-to-page for every selected sheet at render time:

PdfOptions fitted = PdfOptions.builder()
        .fitTo(PdfOptions.Fit.WIDTH)   // WIDTH | HEIGHT | PAGE
        .build();

Fit.WIDTH puts all columns on one page width and lets the height run on — usually what you want for a wide table. Fit.PAGE squeezes the whole sheet onto a single page, which on a large sheet produces something unreadable.

Margins are in inches and page scale is a percentage, both matching Excel’s own semantics. See Rendering for the full layout reference.

How many pages will it be?

Useful for progress reporting, or for splitting a big render into chunks:

int pages = wb.sheetPageCount(wb.sheet("Summary").index());

Page counts and the image methods take a sheet index. Resolve it from the name: a file saved in Evaluation Mode starts with an extra Axyra Evaluation Copy sheet, so index 0 is not necessarily your first sheet.

Fonts

This is the one that bites in production. The renderer needs the fonts the workbook asks for. Metric-compatible substitutes are bundled only for Calibri (Carlito), Cambria (Caladea), and Arial (Liberation Sans); any other missing font falls back to a font with different metrics, which changes line breaks and therefore pagination. CJK text needs CJK fonts, such as Noto, installed.

The renderer scans the usual macOS and Linux font directories, but not C:\Windows\Fonts. On Windows, pass that folder, or your application’s font folder, in fontDirs.

import java.util.List;

PdfOptions withFonts = PdfOptions.builder()
        .fontDirs(List.of("/usr/share/fonts", "/opt/corp-fonts"))
        .build();

Directories that do not exist are skipped rather than failing the render — which is convenient and also why a typo in a path shows up as subtly different output rather than as an error.

A slim container image usually ships no fonts at all. If output must be pixel-stable across machines, install the fonts in the image (or mount them) and point fontDirs at them. “It looks right on my laptop” is not evidence.

Watermarks

PdfOptions marked = PdfOptions.builder()
        .watermark("CONFIDENTIAL")
        .build();

Encryption and permissions

PdfOptions locked = PdfOptions.builder()
        .userPassword("open-me")     // required to open the document
        .ownerPassword("owner")      // required to change permissions
        .allowPrint(true)
        .allowCopy(false)
        .allowModify(false)
        .build();

Permissions are requests the PDF format makes of the reader application, not enforcement — anything that can display the document can extract its content. Treat them as intent, and use the user password when you need an actual gate.

Archival output

PdfOptions archival = PdfOptions.builder().pdfA(true).build();

PDF/A embeds fonts fully and drops encryption and transparency, so it cannot be combined with the password options above.

The complete program

Converts every visible worksheet to a landscape, A4, fit-to-width PDF with a repeated header row and per-sheet bookmarks.

import io.keikai.axyra.sheets.Sheet;
import io.keikai.axyra.sheets.SheetType;
import io.keikai.axyra.sheets.Visibility;
import io.keikai.axyra.sheets.Workbook;
import io.keikai.axyra.sheets.io.PdfOptions;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class ExcelToPdf {

    public static void main(String[] args) {
        Path source = Path.of("report.xlsx");
        Path target = Path.of("report.pdf");

        try (Workbook wb = Workbook.open(source)) {
            List<String> worksheets = new ArrayList<>();

            for (int i = 0; i < wb.sheetCount(); i++) {
                Sheet sheet = wb.sheet(i);
                // Naming a hidden sheet would render it, so skip those too.
                if (sheet.sheetType() != SheetType.WORKSHEET
                        || sheet.visibility() != Visibility.VISIBLE) {
                    continue;
                }
                worksheets.add(sheet.name());

                sheet.setLandscape(true);
                sheet.setPaperSize(9);                 // A4
                sheet.setPageMargins(0.5, 0.5, 0.75, 0.75, 0.3, 0.3);
                sheet.setPrintTitleRows("$1:$1");      // header on every page
            }

            PdfOptions options = PdfOptions.builder()
                    .title("Quarterly Report")
                    .author("Finance")
                    .sheetNames(worksheets.toArray(new String[0]))
                    .fitTo(PdfOptions.Fit.WIDTH)
                    .bookmarks(true)
                    .fontDirs(List.of("/usr/share/fonts"))
                    .build();

            wb.renderPdf(target, options);

            String first = worksheets.get(0);
            int pages = wb.sheetPageCount(wb.sheet(first).index());
            System.out.println("wrote " + target + "; " + first + " is " + pages + " page(s)");
        }
    }
}

Other output formats

The same layout engine drives images. Per sheet, to six raster or vector formats:

import io.keikai.axyra.sheets.io.ImageOptions;
import io.keikai.axyra.sheets.io.Renderer;

ImageOptions image = ImageOptions.builder()
        .scale(2.0)          // 2x nominal size
        .gridLines(true)
        .page(1)             // one page of a multi-page sheet
        .build();

int summary = wb.sheet("Summary").index();

byte[] png  = wb.renderSheetPng(summary, image);
byte[] svg  = wb.renderSheetSvg(summary, image);
byte[] jpg  = Renderer.toJpeg(wb, summary, image);
byte[] tiff = Renderer.toTiff(wb, summary, image);

Use targetWidthPx(...) instead of scale(...) when you need an exact pixel width — the engine derives the scale. For a browser preview rather than paginated output, HTML export is usually the better fit; see Rendering.

XPS output is not available. Use PDF for paginated output.

Common mistakes

Symptom Cause
Line breaks and pagination differ between machines Different fonts available. Ship the fonts and set fontDirs (on Windows, include C:\Windows\Fonts).
Columns spill onto a second page The sheet’s own scale/fit setting. Use fitTo(Fit.WIDTH) or setPageScale.
The header row appears only on page 1 setPrintTitleRows("$1:$1") was not set.
Render fails with an unknown sheet name sheetNames is case-sensitive and rejects unknown names. Read names from the workbook.
PDF/A output rejects the password options PDF/A forbids encryption. Pick one.
Blank or clipped pages A print area narrower than the data, or a stale one saved in the file. Check sheet.printArea().
Charts render but a map chart is an empty frame Map charts are read and written but currently render as a placeholder.

Next steps

Beyond Excel-to-PDF Conversion

PDF conversion can be the final step of an automated reporting workflow. Before rendering, Axyra Sheets can populate a workbook from application data, recalculate formulas, and add charts or formatting. You can then save an editable XLSX alongside the PDF, or render individual sheets as images for previews. This allows one workbook to supply both the working spreadsheet and the documents your application distributes.

Get code at GitHub