“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
| Approach | The catch |
|---|---|
Office Interop — automate Excel’s ExportAsFixedFormat | Needs 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 printer | Windows 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 Chromium | Ships 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:
- 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.
- 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:
| Property | Default | Notes |
|---|---|---|
Paper | A4 | A5–A3, JIS B5/B4, Letter, Legal, Tabloid, Executive, Custom |
Orientation | Portrait | Landscape swaps the paper dimensions |
MarginLeft / Right / Top / Bottom | 54 | In points. 72 pt = 1 inch, so 54 is Excel’s 0.75” “Normal” |
PrintArea | null | RangePosition.Parse("A1:H60"). Null prints the used range |
Scale | 1.0 | 0.1–4.0, matching Excel’s 10%–400%. Ignored once a fit-to-page target is set |
FitToPagesWide / Tall | null | Wide = 1, Tall = null is the common one: never spill sideways, run as long as it needs to |
CenterHorizontally / Vertically | false | Centres the content in the printable area |
Order | DownThenOver | Excel’s default page numbering when the area spans pages both ways |
ShowGridLines | true | |
ShowHeaders | false | Row numbers and column letters. Off by default, like Excel |
ShowImages | true | Off 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";
| Code | Expands to |
|---|---|
&P | Page number. &P+2 and &P-1 offset it |
&N | Total page count |
&A | Sheet name |
&F | Source file name |
&Z | Source folder path |
&D / &T | Date / 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 fromExportToBytes(...)- Omit
PrintSettingsand each sheet uses its own page setup, loaded from the XLSX. Margins are in points RepeatRowsputs the header row on every page exactly once, and&P/&N/&Awork 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
- PDF Export — the reference page for
PdfExporter - Running Headless — servers and batch jobs with no UI
- Printing and Page Setup — every
PrintSettingsoption in detail - XLSX Import and Export — reading the workbook in the first place
- Printing a Spreadsheet in C# — Print, Preview, and PDF in WinForms and WPF
- Building an Input Sheet Users Can’t Break