← Home
Quick start

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

MethodDescription
.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 / methodDescription
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");
Landscape pages — call 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");
All three overloads are synchronous. Wrap in 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");
VersionNotes
PdfVersion.Pdf14Default. %PDF-1.4 header. Traditional cross-reference table. Supported by virtually every PDF viewer.
PdfVersion.Pdf20ISO 32000-2. %PDF-2.0 header. XMP metadata stream attached to the document catalog. Compressed cross-reference stream instead of a plain xref table.