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 from the same renderer that paints the grid on screen, with no external packages at all. 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

The V5 core is UI-independent. Drawing goes through an IGridGraphics interface, and each platform supplies an implementation — GDI+ on WinForms, DrawingContext on WPF and Avalonia. unvell.ReoGrid.IO.Pdf supplies one more: PdfGraphics, which writes PDF content streams.

That has two consequences worth stating plainly:

  1. The PDF is what the grid draws. Not a re-implementation of it. Conditional formatting, rich text, merges, dashed and double borders, number formats — they render because the same viewport code produces them.
  2. There is nothing to install. ReoGrid.IO.Pdf targets net10.0, has no PackageReference at all, and does not touch System.Drawing. It runs on Linux and macOS as happily as on Windows.

Pagination is not part of the PDF code either. Page breaks come from the shared Paginator, which packs whole rows and whole columns into the printable area — a row is never sliced across two pages, exactly as in Excel.

For a server or batch process, the package is unvell.ReoGrid.One.Core (net10.0, no dependencies, no UI):

dotnet add package unvell.ReoGrid.One.Core

The WinForms, WPF and Avalonia packages already contain everything — the core, the formula engine, and XLSX and PDF I/O — so a desktop app does not add .Core alongside them.


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 — you never touch the filesystem.

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 values Excel cached, and those are read as-is — nothing is
// re-evaluated on load. Recalculate only when you have actually changed something.
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 valid one-page file rather than a zero-page PDF that some readers refuse to open.


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
PaperA4A5A3, JIS B5/B4, Letter, Legal, Tabloid, Executive, Custom
OrientationPortraitLandscape swaps the paper dimensions
MarginLeft / Right / Top / Bottom54Points, not inches or millimetres. 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 pinned strip and left out of the scrolling body, so a row never prints twice on the same page — the failure mode you get from naively re-drawing the header.

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 expand to empty rather than to an invented name — a footer that quietly claims the wrong source file is worse than one with a gap in it.

Appearance codes (&B bold, &I italic, &K colour, &"font") are parsed and dropped, since a band is drawn in one style. They survive in the stored string, so a workbook round-trips through XLSX with its codes intact.


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.

IPAexGothic ships inside the DLL, zlib-compressed as an embedded resource, and is emitted as a Type0 / CIDFontType2 font with Identity-H encoding. Japanese renders on a machine with no fonts installed at all.

Three details that matter more than they sound:

  • The text stays text. A ToUnicode CMap is written alongside the glyphs, so the PDF is searchable and copy-pastable — 商品名 comes back out of the PDF as 商品名, not as glyph indices. Reports that are only searchable as images are a recurring complaint about generated PDFs; this is the thing that prevents it.
  • You do not pay for it unless you use it. The font is inflated lazily on first CJK draw, and a sheet with no CJK text emits no embedded font at all — an ASCII-only PDF uses the four standard Helvetica faces and stays small. When Japanese is present, expect roughly 4 MB of font in the file.
  • Line breaking follows 禁則処理. Wrapped Japanese breaks between any two characters, except where it would strand punctuation: a line never opens with . The break moves back a character instead.

Two limitations to plan around:

  • No font subsetting. A PDF containing Japanese embeds the whole face. For one report that is fine; for ten thousand small PDFs it is 40 GB of duplicated font, and you would want to merge or post-process
  • No bold or italic CJK face. Japanese set in bold renders in the regular weight. Latin text has all four Helvetica faces

Pictures

Cell-anchored pictures embed directly: PNG (FlateDecode, with alpha becoming a /SMask) and JPEG (the original bytes pass straight through as /DCTDecode, so nothing is re-encoded and nothing is lost).

An image that appears on twelve pages — a logo in a repeated header — is embedded once and referenced twelve times. An image that cannot be decoded is skipped and the export continues; one corrupt logo does not fail a nightly batch.


In an ASP.NET Core endpoint

The whole point of ExportToBytes is that 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();

There is no Dockerfile section to this article, because there is nothing to add to it. mcr.microsoft.com/dotnet/aspnet:10.0 and your app — no fonts package, no libgdiplus, no Chromium, no spooler.

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. It does not emit 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

ReoGrid V4 has no PDF exporter. Its only route to a PDF is the print pipeline pointed at a virtual printer:

sheet.PrintSettings.PrinterName = "Microsoft Print to PDF";
sheet.CreatePrintSession().Print();

That works well on a desktop — it is covered in Printing a Spreadsheet in C# — but it inherits every constraint of a printer driver, and the model was tightly coupled to the control, so server-side use was never practical. The UI/core separation in V5 exists precisely so this scenario works.


Summary

  • Exporting a worksheet to PDF on a server does not need Office, a printer driver, a headless browser, or a PDF package. unvell.ReoGrid.One.Core is net10.0 with zero dependencies and no System.Drawing
  • The PDF comes out of the same renderer that paints the screen, so the sheet’s own layout — merges, borders, conditional formatting, number formats — carries over instead of being rebuilt
  • PdfExporter.Export(...) writes a file; ExportToBytes(...) is the one an API endpoint wants
  • 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 without printing it twice, and &P / &N / &A work as in Excel
  • Japanese is already handled: IPAexGothic is embedded, the text stays searchable via a ToUnicode CMap, 禁則処理 is applied, and non-CJK exports carry no font at all
  • A missing licence throws, it does not watermark. Set the key at startup and assert IsLicensed — the design assumes a headless service would rather fail than ship a watermarked invoice

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