Spreadsheet to PDF in C# — No Office, No Printer Driver, No PDF Library

· unvell team
Spreadsheet to PDF in C# — No Office, No Printer Driver, No PDF Library

“Just email them the report as a PDF.” It is one line in a ticket, and on a developer machine every approach works. Then it goes to the server, and each one fails in its own way: the machine has no Excel, the container has no print spooler, and the PDF that does come out has lost the column widths somebody spent an afternoon on.

The awkward part is that the spreadsheet already knows how it should look on paper. It has column widths, merges, borders, number formats, a print area, a page setup. Most PDF pipelines throw all of that away and rebuild it.

ReoGrid V5 exports PDF with the same rendering engine that paints the grid on screen. Everything PDF output needs is built into ReoGrid, so the export completes on its own, without any external library. This article covers the call, the page setup, the Japanese font, and the one behaviour that will bite a headless service if you do not know about it.


The four usual approaches, and what each costs

ApproachThe catch
Office Interop — automate Excel’s ExportAsFixedFormatNeeds Excel installed on the server, and Microsoft neither recommends nor supports server-side automation of Office. Windows only. Orphaned EXCEL.EXE processes are a genre of bug on their own
Microsoft Print to PDF — print to a virtual printerWindows only, and it is a printer driver: it wants a print spooler, a user session, and a writable spool directory. Containers have none of those
Render HTML, screenshot it with headless ChromiumShips a browser inside your image, and you have re-implemented pagination in CSS. Page breaks in @media print are not Excel’s page breaks
A PDF library (iText, QuestPDF, PdfSharp)Excellent libraries, but they draw your layout, not the sheet’s. Every column width, merge and border gets restated in code, and it drifts from the workbook the moment someone edits it

Each of these is the right answer to some problem. None of them is “render this worksheet, the way it is set up, on a server.”


What V5 does instead

In V5, the WinForms, WPF and Avalonia screens and the PDF exporter share one rendering engine. A PDF is simply one more place the grid draws to.

That has two consequences worth stating plainly:

  1. The PDF is what the grid draws. Conditional formatting, rich text, merges, dashed and double borders, number formats — the same drawing code produces them on screen and on the page, so the PDF looks the way the sheet does.
  2. Everything it needs is built in. ReoGrid carries every feature PDF output requires, so the export completes on its own, without any external library. It runs on Linux and macOS just as it does on Windows.

Page breaks come from the same logic as printing, which packs whole rows and whole columns into the printable area — each row stays on a single page, exactly as in Excel.

For a server or batch process, use the UI-free package unvell.ReoGrid.One.Core (.NET 10):

dotnet add package unvell.ReoGrid.One.Core

The WinForms, WPF and Avalonia packages already include every feature — formulas, and XLSX and PDF I/O — so a desktop app exports PDF with its one platform package.


The call

using unvell.ReoGrid.Core;
using unvell.ReoGrid.IO.Pdf;

PdfExporter.Export(wb, "book.pdf");              // every sheet in the workbook
PdfExporter.Export(ws, "sheet.pdf");             // one worksheet

byte[] bytes = PdfExporter.ExportToBytes(wb);    // no file involved

ExportToBytes is the one a web API wants — the PDF arrives as bytes in memory, with no file in between.

The end-to-end server job is three steps:

using unvell.ReoGrid.Core;
using unvell.ReoGrid.IO.Excel;
using unvell.ReoGrid.IO.Pdf;

var workbook = XlsxReader.Read("input.xlsx");

// XLSX carries the results Excel calculated, and those are used as-is on load.
// Call Recalculate when you change a value.
var sheet = workbook[0];
sheet.SetNumber(1, 1, 500);
sheet.Recalculate();

PdfExporter.Export(workbook, "output.pdf");

The output is PDF 1.7. An empty workbook still produces a one-page PDF that every reader opens.


Page setup

Every overload takes an optional PrintSettings. Omit it and each sheet falls back to its own ws.PrintSettings — which is what the XLSX reader populated from the file. A workbook whose summary sheet is A4 portrait and whose data sheet is A3 landscape exports the way it was authored, with no code on your side.

using unvell.ReoGrid.Core.Printing;

var settings = new PrintSettings
{
    Paper = PaperKind.A4,
    Orientation = PageOrientation.Landscape,
    ShowGridLines = false,
    FitToPagesWide = 1,
};

PdfExporter.Export(ws, "sheet.pdf", settings);

The knobs worth knowing:

PropertyDefaultNotes
PaperA4A5–A3, JIS B5/B4, Letter, Legal, Tabloid, Executive, Custom
OrientationPortraitLandscape swaps the paper dimensions
MarginLeft / Right / Top / Bottom54In points. 72 pt = 1 inch, so 54 is Excel’s 0.75” “Normal”
PrintAreanullRangePosition.Parse("A1:H60"). Null prints the used range
Scale1.00.1–4.0, matching Excel’s 10%–400%. Ignored once a fit-to-page target is set
FitToPagesWide / TallnullWide = 1, Tall = null is the common one: never spill sideways, run as long as it needs to
CenterHorizontally / VerticallyfalseCentres the content in the printable area
OrderDownThenOverExcel’s default page numbering when the area spans pages both ways
ShowGridLinestrue
ShowHeadersfalseRow numbers and column letters. Off by default, like Excel
ShowImagestrueOff gives a text-only export

Measuring margins in points surprises people, so it is worth being explicit: MarginLeft = 54 is three quarters of an inch, and 20 * 72 / 25.4 is 20 mm.

If you only need the page count — for a progress bar, or to reject a 400-page request before rendering it — paginate without rendering anything:

PrintLayout layout = Paginator.Paginate(ws, ws.PrintSettings);
Console.WriteLine($"{layout.Pages.Count} pages");

Repeated headers and page numbers

A 200-row table across six pages needs its header row on all six. That is Excel’s print titles, and it is two properties:

// Repeat rows 1-2 at the top of every page, and column A down the left.
ws.PrintSettings.RepeatRows = new LineSpan(0, 1);
ws.PrintSettings.RepeatColumns = new LineSpan(0, 0);

The repeated band is drawn as a strip of its own, separate from the body, so the header lands at the top of every page exactly once.

Headers and footers use Excel’s & codes, so a workbook authored in Excel keeps working:

var hf = ws.PrintSettings.HeaderFooter;
hf.Header.Center = "&A";                 // sheet name
hf.Footer.Right  = "&P / &N";            // 2 / 5
hf.Footer.Left   = "&D &T";              // date and time
hf.FontSizePt    = 9;

// A different banner on page 1 only.
hf.DifferentFirst = true;
hf.FirstHeader.Center = "Quarterly report";
CodeExpands to
&PPage number. &P+2 and &P-1 offset it
&NTotal page count
&ASheet name
&FSource file name
&ZSource folder path
&D / &TDate / time, in the current culture
&&A literal &

&F and &Z come from the third parameter, not from the output path:

PdfExporter.Export(wb, "out.pdf", settings, documentName: "/srv/reports/Q3.xlsx");

Leave it out and both print as blank.

Appearance codes (&B bold, &I italic, &K colour, &"font") are left out of the PDF, and each header and footer is drawn in one style. The codes stay in the settings, so a workbook saved back to XLSX keeps them as authored.


Japanese text

This is where server-side PDF usually falls apart. The container has no fonts installed, so CJK text renders as boxes — or the export throws.

A Japanese font (IPAexGothic) is built in. Japanese renders on a server with no fonts installed — the sheet goes straight to PDF.

Three details that matter more than they sound:

  • The text stays text. The PDF is searchable and copy-pastable: 商品名 comes back out of the PDF as 商品名.
  • The font is embedded only when it is used. Only a sheet containing Japanese carries the Japanese font; a Latin-only sheet uses the standard PDF Helvetica faces and stays small. When Japanese is present, expect the font to add roughly 4 MB to the file.
  • Line breaking follows 禁則処理. Where a break would put punctuation at the start of a line, the line breaks one character earlier, so 。 and 、 stay at the end of the line before.

Two points to plan around:

  • The Japanese font is embedded whole. For one report the size is fine; for ten thousand small PDFs it adds up to 40 GB of the same font, and you would want to merge or post-process
  • Japanese bold and italic render in the regular weight. Latin text is drawn with all four Helvetica faces (regular, bold, italic, bold italic)

Pictures

Cell-anchored pictures embed directly. PNG (transparency included) and JPEG are supported, and a JPEG keeps its original quality.

An image that appears on twelve pages — a logo in a repeated header — is embedded once and shared by every page, which keeps the file small. An image that cannot be read is skipped and the export carries on, so a nightly batch runs to completion even with one corrupt logo.


In an ASP.NET Core endpoint

With ExportToBytes, this is the entire handler:

using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.License;
using unvell.ReoGrid.IO.Excel;
using unvell.ReoGrid.IO.Pdf;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Once, at startup — before any export runs.
ReoGridLicense.SetLicense(builder.Configuration["ReoGrid:License"]!);

app.MapPost("/reports/pdf", async (IFormFile xlsx) =>
{
    await using var stream = xlsx.OpenReadStream();
    var workbook = XlsxReader.Read(stream);

    byte[] pdf = PdfExporter.ExportToBytes(workbook, documentName: xlsx.FileName);

    return Results.File(pdf, "application/pdf",
        Path.ChangeExtension(xlsx.FileName, ".pdf"));
}).DisableAntiforgery();

app.Run();

The Docker image is just mcr.microsoft.com/dotnet/aspnet:10.0 plus your app. It exports PDF as-is, with no fonts package, browser or print spooler added.

For a very large input, XlsxReader.OpenVirtual("large.xlsx", SheetLoadMode.OnDemand) streams sheets in on demand instead of loading the whole book.


The one behaviour to know about: the licence throws

This is the gotcha, and it is deliberate.

Without a valid licence key, PDF export throws ReoGridLicenseException instead of producing a watermarked PDF.

using unvell.ReoGrid.Core.License;

// Read it from configuration or the environment — not from a string literal in source.
ReoGridLicense.SetLicense(Environment.GetEnvironmentVariable("REOGRID_LICENSE")!);

The reasoning is worth spelling out, because the on-screen grid behaves the opposite way: an unlicensed control stays usable read-only and shows a watermark, because a person looking at the screen can see what is wrong and act on it. A headless service has no such channel. If file I/O degraded quietly, the watermark’s first reader would be your customer, on an invoice. So file I/O fails loudly instead — XLSX, CSV, PDF and reogrid-json all refuse rather than emit output.

The same applies to a trial key that expires: the day it lapses, the deployed service starts throwing on export. Worth a health check that calls ReoGridLicense.IsLicensed at startup rather than discovering it from a 500 at month end:

if (!ReoGridLicense.IsLicensed)
    throw new InvalidOperationException("ReoGrid licence missing or expired — PDF export will fail.");

One key covers every version and platform, and it is perpetual: a purchased key keeps working after the update subscription lapses, and a key issued for V4 activates V5 unchanged.


On V4

Since 4.6, ReoGrid V4 exports PDF directly from a WinForms app, paginated exactly as the sheet prints:

using unvell.ReoGrid.PDF;

PDFExporter.Export(sheet, "report.pdf");

It runs inside a desktop application — see Export as PDF. Before 4.6, V4 makes a PDF by printing to a virtual printer, as covered in Printing a Spreadsheet in C#. To export PDF on a server or in a container, use V5, which runs without a UI.


Summary

  • ReoGrid carries everything PDF output needs, so a server exports a worksheet to PDF without Office, a printer driver, a headless browser or a PDF library. For server use, install unvell.ReoGrid.One.Core (.NET 10)
  • The PDF comes out of the same rendering engine that paints the screen, so the sheet’s own layout — merges, borders, conditional formatting, number formats — carries over as-is
  • PdfExporter.Export(...) writes a file; an API endpoint takes the bytes from ExportToBytes(...)
  • Omit PrintSettings and each sheet uses its own page setup, loaded from the XLSX. Margins are in points
  • RepeatRows puts the header row on every page exactly once, and &P / &N / &A work as in Excel
  • A Japanese font is built in: the text stays searchable and copyable, 禁則処理 is applied, and the font is embedded only in PDFs that contain Japanese
  • A missing licence raises an exception instead of producing a watermarked PDF. Set the key at startup and check IsLicensed

See what’s new in V5 / Start a 30-day trial


Further reading

Try ReoGrid in your own project

The Excel-compatible spreadsheet component for .NET WinForms and WPF. 30-day free trial — no credit card required.

Newsletter

New releases, straight to your inbox

Occasional updates on ReoGrid releases, features, and technical articles. Unsubscribe anytime.

Related articles

Printing a Spreadsheet in C# — Print, Preview, and PDF in WinForms and WPF

How to print a worksheet from a .NET desktop app — print preview, the system print dialog, printing straight to PDF, plus paper size, margins, orientation, scaling, and page breaks. Working C# code for WinForms and WPF.

Excel Number Format Codes, Decoded — #,##0, [Red], ;;; and K/M Suffixes in a C# App

#,##0;[Red]-#,##0;"-" looks like line noise, but it is a small language — four sections, digit placeholders, colors, conditions and literal text — and once you can read it you can write the code you actually need. This guide decodes Excel's custom number formats: thousands and millions scaling, hiding values, units, [h]:mm for timesheets, and why three cells showing 33 can total 100. Then it applies the same codes in C# with ReoGrid V5, where a code copied out of Excel works as-is and survives the trip to XLSX.

Excel Tables in C# — What "Format as Table" Actually Saves, and How to Create One

Excel's "Format as Table" looks like formatting — a coloured header, striped rows — yet not one cell in the saved file carries a fill. What Excel stores is an object: a range, a header row and a style name. This guide opens up the table part of an .xlsx, explains what the 60 built-in styles really contain, and names the three details that decide whether Excel will open the file at all — then builds a table with filter buttons and a totals row in C# with ReoGrid V5.1.