A .NET library that converts Excel workbooks into PDF and turns Excel files into reusable, data-driven report templates — without depending on Microsoft Office or COM Interop.
- Excel → PDF: Pure managed conversion via ClosedXML + PdfSharp.
- Template engine: Place
$symbolsand#directivesdirectly in cells and overwrite the workbook with your data at runtime. - Multi-page reports: Split long lists across
First/Body/Lastpage templates with automatic page numbering. - Built-in renderers: Drop in dynamic images and QR codes from cell directives, or register your own.
- GDI+ printing: Bind the same rendering pipeline to
System.Drawing.Printing.PrintDocument(Windows) for preview / direct printing.
| Excel → PDF | Quotation template |
|---|---|
![]() | ![]() |
# Core: Excel → PDF + template engine
PM>Install-Package Excel.Report.PDF
# Optional: Bind to System.Drawing.Printing (Windows only — preview / printer output)
PM>Install-Package Excel.Report.PrintDocumentExcel.Report.PDF targets .NET 6.0 and runs on Windows / Linux / macOS.
Excel.Report.PrintDocument is Windows-only because it depends on GDI+ via System.Drawing.Common.
PdfSharp does not ship with fonts. Implement IFontResolver once at startup and return whichever font bytes you want PdfSharp to embed.
usingPdfSharp.Fonts;publicclassCustomFontResolver:IFontResolver{publicbyte[]GetFont(stringfaceName)=>faceName.EndsWith("#b")?Resources.NotoSansJP_ExtraBold:Resources.NotoSansJP_Regular;publicFontResolverInfoResolveTypeface(stringfamilyName,boolisBold,boolisItalic){varfaceName=familyName;if(isBold)faceName+="#b";returnnewFontResolverInfo(faceName);}}GlobalFontSettings.FontResolver=newCustomFontResolver();A more sophisticated example that loads fonts directly from the Windows Fonts registry lives in
Source/TestWinFormsApp/WindowsInstalledFontResolver.cs.
See docs/getting-started.md for the full setup walkthrough.
usingExcel.Report.PDF;// Whole workbook → multi-page PDFusingvarpdf=ExcelConverter.ConvertToPdf("report.xlsx");File.WriteAllBytes("report.pdf",pdf.ToArray());// Specific sheet by 1-based positionusingvarpdfSheet1=ExcelConverter.ConvertToPdf("report.xlsx",1);// Specific sheet by nameusingvarpdfNamed=ExcelConverter.ConvertToPdf("report.xlsx","Summary");// Stream overloads are also availableusingvarfs=File.OpenRead("report.xlsx");usingvarpdfFromStream=ExcelConverter.ConvertToPdf(fs);The renderer respects Excel's page setup (paper size, margins, scaling, page breaks, centering) and reproduces fonts, fills, borders (including Double), text rotation, vertical text, and embedded pictures.
Print scaling — set a fixed zoom percentage (
PageSetup.Scale) or fit the sheet to the page width (#FitColumn/PageSetup.PagesWide). See docs/special-directives.md → Print scaling.
Drop $symbols and #directives straight into your .xlsx template, then bind a data object at runtime.
usingClosedXML.Excel;usingExcel.Report.PDF;vardata=newQuotation{Title="Banquet ingredients",Client="Excel Consulting Inc.",PersonInCharge="Shoichi Otani",};data.Details.Add(new(){Title="Sea bream",Detail="Fresh",Price=10000,Discount=0});data.Details.Add(new(){Title="Yellowtail",Detail="Fresh",Price=20000,Discount=0});data.Details.Add(new(){Title="Hamachi",Detail="Bargain",Price=30000,Discount=2000});data.Details.Add(new(){Title="Octopus",Detail="Bargain",Price=40000,Discount=1000});usingvarbook=newXLWorkbook("Quotation.xlsx");// Overwrite a single sheetawaitbook.Worksheet(1).OverWrite(newObjectExcelSymbolConverter(data));// ...or overwrite every sheet (and expand multi-page #PagedLoopRows templates)// await book.OverWrite(new ObjectExcelSymbolConverter(data));// Render the populated workbook to PDFusingvarms=newMemoryStream();book.SaveAs(ms);usingvarpdf=ExcelConverter.ConvertToPdf(ms,1);File.WriteAllBytes("Quotation.pdf",pdf.ToArray());ObjectExcelSymbolConverter resolves symbols against the public properties of the bound object (and supports nested loops). To map symbols to a database row, an API response, or any other source, implement IExcelSymbolConverter — see docs/template-overwrite.md.
All directives live in cell text. $ resolves to a value; # invokes a function, loop, or rendering command.
| Directive | Where | Purpose |
|---|---|---|
$name | any cell | Replace the cell value with converter.GetData("name"). |
#LoopRow($items, item, n) | column A | Insert-mode loop. Copies n rows above the current row, once per element of $items. |
#LoopRowData($items, item, n) | column A | Data-only loop. Reuses the existing row format and writes values without inserting rows. |
#PagedLoopRows(pageType, rowsPerPage, $items, item, blockRowCount) | column A | Distributes a long list across First / Body / Last template sheets — see docs/multi-page.md. |
#Image($bytesOrStream[, widthScale[, heightScale]]) | any cell | Insert a picture from byte[] or Stream. |
#QR($text[, pixelsPerModule]) | any cell | Insert a QR code (PNG, ECC level M). Default pixelsPerModule = 10. |
#Page | any cell except column A | Render the current page number when converting to PDF. |
#PageCount | any cell except column A | Render the total page count (resolved after layout). |
#PageOf("/") | any cell except column A | Render current<separator>total. The separator is the literal in the parentheses. |
#Empty | any cell | Reserve the cell area for layout calculations but draw nothing. |
#FitColumn | A1 only | Scale the rendered output so the used column width fills the printable page width (see Print scaling). |
Multiple cell directives can coexist on the same cell separated by | (for example #Empty | #FitColumn).
Detailed semantics, edge cases, and worked examples are split across the documents below.
#Image and #QR are themselves implementations of the public IOverWriteFunction interface. You can register additional #YourName(...) directives in two lines of code — there are no internal hooks involved.
usingClosedXML.Excel;usingExcel.Report.PDF;publicsealedclassUpperFunction:IOverWriteFunction{publicstringName=>"Upper";// matched as "#Upper(...)"publicTaskInvokeAsync(IXLWorksheetsheet,introwIndex,intcolIndex,object?[]args){vartext=args.ElementAtOrDefault(0)?.ToString()??string.Empty;sheet.Cell(rowIndex,colIndex).SetValue(XLCellValue.FromObject(text.ToUpperInvariant()));returnTask.CompletedTask;}}// Register once at startupExcelOverWriter.RegisterOverWriteFunction(newUpperFunction());Then in any cell:
#Upper($Client)
args already has $symbols resolved by your IExcelSymbolConverter. See docs/built-in-functions.md for argument-parsing rules, real-world recipes (barcodes, signature stamps, computed totals, async DB lookups), and the full extensibility contract.
- Getting started — install, font resolvers, every
ExcelConverter.ConvertToPdfoverload, troubleshooting. - Template overwrite —
$symbols,#LoopRowvs#LoopRowData, nested loops, customIExcelSymbolConverter. - Multi-page reports —
#PagedLoopRows, theFirst/Body/Lastpage model, and how rows are distributed. - Built-in cell functions —
#Image,#QR, and writing your ownIOverWriteFunction. - PrintDocument integration — bind the renderer to
System.Drawing.Printing.PrintDocumentfor print preview / direct printing on Windows. - Special rendering directives —
#Empty,#FitColumn, page-number directives, vertical / rotated text. - Public API reference — every public type, method, and extension across both packages.
| Package | Target | Key dependencies |
|---|---|---|
Excel.Report.PDF | net6.0 | ClosedXML 0.105.x, PdfSharp 6.2.x, QRCoder 1.7.x |
Excel.Report.PrintDocument | net6.0 (Windows) | Excel.Report.PDF, System.Drawing.Common 8.0.x |
Both libraries are pure managed code — no Microsoft Office, no COM, no native interop.
MIT © Codeer Software

