Your first PDF in two minutes
Majorsilence.Pdf is a zero-dependency, self-contained PDF library for .NET 8 and 10. One package, no native binaries — create documents, draw text, shapes, and images, embed TrueType fonts, and save to a file or stream.
Installation
A single NuGet package — no companion libraries required:
# .NET CLI dotnet add package Majorsilence.Pdf # Package Manager Console Install-Package Majorsilence.Pdf
Hello, PDF
Create a document with PdfDocument.Create(), add a page, draw on the canvas, and save. Coordinates are in PDF points (1 pt = 1/72 inch), with the top-left corner as the origin — Y increases downward.
using Majorsilence.Pdf; PdfDocument.Create() .AddPage(PageSizes.A4, canvas => { canvas.DrawText("Hello, PDF!", 72, 72, TextStyle.Default.WithSize(24).WithBold()); canvas.DrawLine(72, 96, 520, 96); }) .Save("hello.pdf");
API styles — callback vs incremental
Choose whichever fits your code structure:
Callback style (fluent chain)
Pass a drawing lambda to AddPage. The document is returned so you can keep chaining. Best for short documents built in one pass.
PdfDocument.Create()
.WithTitle("My Report")
.AddPage(PageSizes.A4, canvas =>
{
canvas.DrawText("Chapter 1", 72, 72,
TextStyle.Default.WithSize(18).WithBold());
})
.AddPage(PageSizes.A4, canvas =>
{
canvas.DrawText("Chapter 2", 72, 72,
TextStyle.Default.WithSize(18).WithBold());
})
.Save("report.pdf");
Incremental style
AddPage without a callback returns the PdfCanvas directly. Useful when building content imperatively, e.g. inside a loop.
var doc = PdfDocument.Create().WithTitle("My Report"); foreach (var chapter in chapters) { var canvas = doc.AddPage(PageSizes.A4); canvas.DrawText(chapter.Title, 72, 72, TextStyle.Default.WithSize(18).WithBold()); // ... draw body ... } doc.Save("report.pdf");
Text & TextStyle
TextStyle is immutable. Every With* method returns a new instance — the original is unchanged. Build a base style once, then derive variants from it.
var heading = TextStyle.Default .WithFamily("Helvetica") .WithSize(22) .WithBold() .WithColor(PdfColor.FromHex("#1A56A0")); var body = TextStyle.Default .WithFamily("Times-Roman") .WithSize(11); var mono = TextStyle.Default .WithFamily("Courier") .WithSize(10); canvas.DrawText("Invoice #1024", 72, 72, heading); canvas.DrawText("Due: July 17, 2026", 72, 106, body); // Alignment canvas.DrawText("Right-aligned", 540, 72, body.WithAlignment(TextAlignment.Right)); // Decorations canvas.DrawText("underlined", 72, 140, body.WithUnderline()); canvas.DrawText("strikethrough", 72, 160, body.WithStrikethrough()); canvas.DrawText("overline", 72, 180, body.WithOverline());
TextStyle reference
| Method | Description |
|---|---|
| .WithFamily(name) | Standard font by name. Built-in: Helvetica, Times-Roman, Courier, Symbol, ZapfDingbats. Also accepts any family registered in FontRegistry. |
| .WithFontFile(path) | Embed a TrueType / OpenType font by absolute file path. Takes precedence over WithFamily. |
| .WithSize(pts) | Font size in PDF points. Default: 12. |
| .WithColor(color) | Text foreground colour. Default: PdfColor.Black. |
| .WithBold() | Bold weight. |
| .WithItalic() | Italic style. |
| .WithAlignment(a) | TextAlignment.Left (default), Center, Right. X is the reference point. |
| .WithUnderline() | Underline decoration. |
| .WithStrikethrough() | Strikethrough decoration. |
| .WithOverline() | Overline decoration. |
| .WithVertical() | Rotate text 90° counter-clockwise. |
PdfColor
// Named colours PdfColor.Black PdfColor.White PdfColor.Red PdfColor.Green PdfColor.Blue PdfColor.Yellow PdfColor.Orange PdfColor.Gray PdfColor.LightGray PdfColor.DarkGray // From CSS hex string PdfColor.FromHex("#1A56A0") // From RGB bytes (0–255) PdfColor.FromRgb(26, 86, 160)
Measuring text width
Use MeasureTextWidth to position labels relative to other content:
float w = canvas.MeasureTextWidth("Hello", body); canvas.DrawText("World", 72 + w + 4, 72, body);
Shapes
ShapeStyle controls fill and stroke independently. All shape methods accept an optional ShapeStyle; the default is a 1 pt black stroke with no fill.
// Rectangles canvas.DrawRectangle(50, 50, 200, 80, ShapeStyle.Filled(PdfColor.LightGray)); canvas.DrawRectangle(50, 150, 200, 80, ShapeStyle.Stroked(PdfColor.Black, width: 2)); canvas.DrawRectangle(50, 250, 200, 80, ShapeStyle.Filled(PdfColor.FromHex("#E8F4FF")) .WithStroke(PdfColor.Blue, width: 1)); canvas.DrawRectangle(50, 350, 200, 80, ShapeStyle.Stroked(PdfColor.DarkGray).Dashed()); // Ellipses (same ShapeStyle options) canvas.DrawEllipse(300, 50, 150, 80, ShapeStyle.Filled(PdfColor.Blue)); // Polygons var triangle = new List<(float x, float y)> { (300, 200), (400, 350), (200, 350) }; canvas.DrawPolygon(triangle, ShapeStyle.Filled(PdfColor.Orange) .WithStroke(PdfColor.DarkGray));
ShapeStyle reference
| Factory / method | Description |
|---|---|
| ShapeStyle.Filled(color) | Solid fill, no stroke. |
| ShapeStyle.Stroked(color, width) | Stroke only, no fill. Width defaults to 1. |
| .WithFill(color) | Add or replace fill colour. |
| .WithStroke(color, width) | Add or replace stroke colour and width. |
| .WithNoFill() | Remove fill. |
| .WithNoStroke() | Remove stroke. |
| .Dashed() | Dashed line style for the stroke. |
| .Dotted() | Dotted line style for the stroke. |
Lines & curves
StrokeStyle is to lines what ShapeStyle is to filled shapes.
// Straight lines canvas.DrawLine(72, 100, 520, 100); // 1 pt solid black canvas.DrawLine(72, 130, 520, 130, StrokeStyle.Default.WithWidth(2).WithColor(PdfColor.Blue)); canvas.DrawLine(72, 160, 520, 160, StrokeStyle.Default.WithWidth(1).Dashed()); // Smooth curve through control points (Catmull-Rom spline) var pts = new List<(float, float)> { (72, 300), (150, 260), (250, 320), (350, 270), (450, 310), (520, 280) }; canvas.DrawCurve(pts, StrokeStyle.Default.WithWidth(2).WithColor(PdfColor.Red));
Images
DrawImage accepts either raw JPEG bytes or raw RGB24 bytes (3 bytes per pixel, row-major). The image is placed with its top-left corner at (x, y) and scaled to the specified dimensions.
// JPEG from disk byte[] jpeg = File.ReadAllBytes("logo.jpg"); canvas.DrawImage(jpeg, pixelWidth: 400, pixelHeight: 200, isJpeg: true, x: 72, y: 72, width: 200, height: 100); // Raw RGB24 (e.g. a gradient generated in code) const int W = 200, H = 150; var rgb = new byte[W * H * 3]; for (int row = 0; row < H; row++) for (int col = 0; col < W; col++) { int i = (row * W + col) * 3; rgb[i] = (byte)(col * 255 / W); // R rgb[i + 1] = (byte)(row * 255 / H); // G rgb[i + 2] = 128; // B } canvas.DrawImage(rgb, W, H, isJpeg: false, x: 72, y: 72, width: 200, height: 150);
Links & tooltips
Add clickable hyperlinks or hover tooltips over any rectangular area. The area is defined as top-left (x, y) plus width and height.
var linkStyle = TextStyle.Default .WithColor(PdfColor.Blue).WithUnderline(); canvas.DrawText("Visit majorsilence.com", 72, 110, linkStyle); canvas.AddLink(72, 96, width: 220, height: 18, uri: "https://majorsilence.com"); // Tooltip (shows in supporting PDF viewers on hover) canvas.DrawRectangle(72, 140, 200, 40, ShapeStyle.Filled(PdfColor.LightGray).WithStroke(PdfColor.Gray)); canvas.DrawText("Hover for info", 82, 165, TextStyle.Default); canvas.AddTooltip(72, 140, 200, 40, tooltip: "This text appears on hover.");
TrueType fonts
The five built-in standard fonts (Helvetica, Times-Roman, Courier, Symbol, ZapfDingbats) cover ASCII + Latin-1. For Unicode text, emoji, or CJK, embed a TrueType font.
Embed by file path
The simplest option: point TextStyle directly at a .ttf or .otf file. The font is read once per document and cached.
var custom = TextStyle.Default .WithFontFile("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf") .WithSize(14); canvas.DrawText("café résumé naïve", 72, 72, custom); canvas.DrawText("Bold variant", 72, 96, custom.WithBold());
FontRegistry — named families + fallback chain
Register one or more families under logical names, define the fallback chain (used when the primary font has no glyph), and attach the registry to the document. The canvas then segments text automatically, routing each character to the first font that can render it.
var fonts = new FontRegistry() // Register individual variants .AddFamily("LiberationSans", regular: "Fonts/LiberationSans-Regular.ttf", bold: "Fonts/LiberationSans-Bold.ttf", italic: "Fonts/LiberationSans-Italic.ttf", boldItalic: "Fonts/LiberationSans-BoldItalic.ttf") // Or scan a directory — detects variants from filenames .AddDirectory("Fonts/") // NotoSans fills in any glyph LiberationSans is missing .AddFallback("NotoSans"); PdfDocument.Create() .WithFontRegistry(fonts) .AddPage(PageSizes.A4, canvas => { var style = TextStyle.Default .WithFamily("LiberationSans").WithSize(14); canvas.DrawText("Hello κόσμε мир", 72, 72, style); canvas.DrawText("Bold text", 72, 96, style.WithBold()); canvas.DrawText("Italic text", 72, 118, style.WithItalic()); }) .Save("output.pdf");
AddDirectory expects filenames in the pattern FamilyName-Regular.ttf, FamilyName-Bold.ttf, etc. Characters that no registered font can render are emitted as .notdef boxes — no exception is thrown.
Multi-page documents
Call AddPage as many times as needed. Pages are written in the order added.
var doc = PdfDocument.Create() .WithTitle("Report") .WithAuthor("Majorsilence") .WithFontRegistry(fonts); string[] chapters = { "Introduction", "Methods", "Results" }; var title = TextStyle.Default.WithSize(24).WithBold(); var footer = TextStyle.Default.WithSize(9).WithColor(PdfColor.Gray); for (int i = 0; i < chapters.Length; i++) { int pageNum = i + 1; doc.AddPage(PageSizes.A4, canvas => { canvas.DrawText(chapters[i], 72, 72, title); canvas.DrawText( $"Page {pageNum} of {chapters.Length}", PageSizes.A4.Width / 2, PageSizes.A4.Height - 30, footer.WithAlignment(TextAlignment.Center)); }); } doc.Save("report.pdf");
PageSizes.Letter.Landscape() (or any size) to swap width and height. Mix orientations freely across pages in the same document.
Save options
// Write to a file doc.Save("report.pdf"); // Write to any Stream (e.g. an ASP.NET response body) doc.Save(Response.Body); // Get bytes — useful for returning from an API endpoint byte[] bytes = doc.ToBytes(); return File(bytes, "application/pdf", "report.pdf");
Task.Run if you need to call from async code without blocking the thread pool.
Metadata & PDF version
Metadata appears in the viewer's document properties. The PDF version controls the file header and cross-reference format.
PdfDocument.Create()
.WithTitle("Invoice #INV-2026-0042")
.WithAuthor("Majorsilence Corp")
.WithSubject("Sales invoice")
.WithCreator("MyApp 1.0")
// PDF 1.4 (default) — broadest reader compatibility
.WithVersion(PdfVersion.Pdf14)
// PDF 2.0 — adds XMP metadata stream + compressed xref
// .WithVersion(PdfVersion.Pdf20)
.AddPage(PageSizes.Letter, canvas => { /* ... */ })
.Save("invoice.pdf");
| Version | Notes |
|---|---|
| PdfVersion.Pdf14 | Default. %PDF-1.4 header. Traditional cross-reference table. Supported by virtually every PDF viewer. |
| PdfVersion.Pdf20 | ISO 32000-2. %PDF-2.0 header. XMP metadata stream attached to the document catalog. Compressed cross-reference stream instead of a plain xref table. |