Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ jobs:
output/reference-env/bin/python -I tools/verify_quickstarts.py
- name: Verify the downloadable Node invoice and guide
run: python tools/verify_node_examples.py
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Install and execute the C# guide and invoice from NuGet
run: |
python -m pip install pypdf==6.1.1
python tools/verify_dotnet_examples.py
- uses: actions/upload-artifact@v4
if: always()
with:
name: dotnet-guide-verification
path: output/dotnet-verification
retention-days: 14
- name: Build and exercise the downloadable Next.js starter
run: python tools/verify_nextjs_starter.py
- uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ examples/rust/verification/
/output/
/target/
/invoice.pdf
examples/dotnet/bin/
examples/dotnet/obj/
Binary file added docs/assets/showcase/invoice-dotnet.pdf
Binary file not shown.
Binary file added docs/assets/showcase/invoice-dotnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
125 changes: 125 additions & 0 deletions docs/getting-started/dotnet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: Generate PDFs from C# and .NET with HTML and CSS
description: Install FullBleed.DotNet from NuGet, render a PDF from C#, and run a designed invoice with explicit fonts and PNG previews. No browser or Python runtime is needed for native rendering.
---
# Generate PDFs from C# and .NET

[`FullBleed.DotNet`](https://www.nuget.org/packages/FullBleed.DotNet/0.1.2)
brings Fullbleed's Rust rendering engine into a .NET process. Use static HTML
and CSS to create invoices, reports, and variable-data documents. Native
rendering needs neither Python nor a browser.

## Install and render your first PDF

With the .NET 8 SDK installed:

```bash
dotnet new console -n InvoiceDemo --framework net8.0
cd InvoiceDemo
dotnet add package FullBleed.DotNet --version 0.1.2
```

Replace `Program.cs` with:

```csharp
using FullBleed.DotNet;

using var engine = new FullBleedEngine(new FullBleedEngineOptions
{
DocumentLanguage = "en-US",
DocumentTitle = "Invoice INV-1042",
});

var html = "<h1>Invoice INV-1042</h1><p>Consulting: USD 1,200.00</p>";
var css = "@page { size: A4; margin: 20mm; } h1 { color: #175c52; }";
File.WriteAllBytes("invoice.pdf", engine.RenderPdf(html, css));

var inspection = FullBleedEngine.InspectPdf("invoice.pdf");
Console.WriteLine($"Created invoice.pdf: {inspection.PageCount} page(s).");
```

Run `dotnet run` and open `invoice.pdf` in the project directory. You can also
return the bytes from an HTTP handler or write them to your own storage.
The first example uses standard PDF fonts; register explicit font files for
your document's typography and character coverage.

The package targets `net8.0` and contains native libraries for Windows x64,
Linux x64, Intel macOS, and Apple Silicon macOS. It pins Fullbleed 2.5.6.
The managed assembly has no third-party NuGet runtime dependencies.

## Run a designed invoice

The Northstar sample combines explicit fonts, CSS grid, tables, print
dimensions, color, and typographic hierarchy. Its text and business details
are fictional.

[![Northstar invoice rendered from C#, with cream paper, dark green typography, and a large total panel.](../assets/showcase/invoice-dotnet.png)](../assets/showcase/invoice-dotnet.pdf)

[Open the PDF generated from C#](../assets/showcase/invoice-dotnet.pdf).

Clone the documentation repository and run the example with .NET 8:

```bash
git clone https://github.com/fullbleed-engine/docs.git fullbleed-docs
cd fullbleed-docs
dotnet run --project examples/dotnet -c Release -- output/northstar
```

Open `output/northstar/invoice.pdf` and `invoice_page1.png`. The sample
includes the HTML, CSS, font files, and their license notices. Rendering
uses those local assets and the prebuilt NuGet package. You do not need a
Rust compiler to run it.

[Read the complete C# sample](https://github.com/fullbleed-engine/docs/tree/main/examples/dotnet)
or [browse the document assets](https://github.com/fullbleed-engine/docs/tree/main/docs/assets/showcase).

## Register fonts and inspect the result

The sample registers every font used by its CSS before rendering:

```csharp
using var engine = new FullBleedEngine(new FullBleedEngineOptions
{
Assets = Directory.GetFiles("fonts", "*.ttf")
.Order(StringComparer.Ordinal)
.Select(path => FullBleedAsset.FromPath(path, FullBleedAssetKind.Font))
.ToList(),
});

var result = engine.RenderPdfWithDiagnostics(html, css);
if (result.Diagnostics.MissingGlyphs.Count != 0)
{
throw new InvalidOperationException("The supplied fonts do not cover every character.");
}
File.WriteAllBytes("invoice.pdf", result.Pdf);

engine.RenderImagePagesToDirectory(html, css, "preview", dpi: 96, stem: "invoice");
```

Keep the font notices when redistributing the assets. The PDF and PNG calls
above are separate render operations; use
`RenderFinalizedPdfImagePagesToDirectory` to preview an existing Fullbleed PDF.
Inspect the rendered pages after changing content or fonts.

## Use the native API or the CLI adapter

`FullBleedEngine` covers rendering, batches, diagnostics, previews, PDF
inspection, template composition, and compiled bindings. For repeated
documents, `BindingMap<T>` projects records through ordinary LINQ selectors.
Use fixed bindings for values with reserved geometry, and reflow bindings
when changing text must wrap or repaginate.

`FullBleedCliClient` is a separate adapter for an independently installed
Fullbleed Python CLI. It discovers that runtime's capabilities and schemas
and exposes verification, profiles, scaffolding, and other CLI workflows.
Installing the NuGet package alone does not install the Python CLI.

Selecting an output profile is not proof of conformance. Check the actual
document and retain the relevant verification evidence before making an
accessibility, archival, or print-standard claim.

[.NET API reference](https://github.com/fullbleed-engine/fullbleed-dotnet/blob/v0.1.2/docs/api.md)
· [LINQ and variable-data example](https://github.com/fullbleed-engine/fullbleed-dotnet/tree/v0.1.2/samples/FullBleed.DotNet.LinqVdp)
· [CSS coverage](../css-coverage.md)
· [More document designs](../examples.md)
· [Report a .NET issue](https://github.com/fullbleed-engine/fullbleed-dotnet/issues)
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ python -m pip install fullbleed

Python 3.10–3.14 · Windows, macOS, Linux

[Use the Rust crate](getting-started/rust.md) or [generate PDFs in Node.js](getting-started/node.md).
Generate PDFs with [Rust](getting-started/rust.md), [Node.js](getting-started/node.md),
or [C# and .NET](getting-started/dotnet.md).
</div>
<figure markdown>
[![Northstar Studio invoice with editorial typography, vermilion rules, and a forest-green total panel.](assets/showcase/invoice-1.png)](assets/showcase/invoice.pdf)
Expand Down
14 changes: 14 additions & 0 deletions examples/dotnet/NorthstarInvoice.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FullBleed.DotNet" Version="0.1.2" />
<None Include="../../docs/assets/showcase/invoice.html" Link="Assets/invoice.html" CopyToOutputDirectory="PreserveNewest" />
<None Include="../../docs/assets/showcase/invoice.css" Link="Assets/invoice.css" CopyToOutputDirectory="PreserveNewest" />
<None Include="../../docs/assets/playground/fonts/*" Link="Assets/fonts/%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
27 changes: 27 additions & 0 deletions examples/dotnet/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using FullBleed.DotNet;

var assets = Path.Combine(AppContext.BaseDirectory, "Assets");
var output = Path.GetFullPath(args.Length == 1 ? args[0] : "output/northstar");
Directory.CreateDirectory(output);
var html = File.ReadAllText(Path.Combine(assets, "invoice.html"));
var css = File.ReadAllText(Path.Combine(assets, "invoice.css"));
using var engine = new FullBleedEngine(new FullBleedEngineOptions
{
DocumentLanguage = "en-US",
DocumentTitle = "Northstar Studio - Invoice NS-1042",
Assets = Directory.GetFiles(Path.Combine(assets, "fonts"), "*.ttf")
.Order(StringComparer.Ordinal)
.Select(path => FullBleedAsset.FromPath(path, FullBleedAssetKind.Font))
.ToList(),
});
var rendered = engine.RenderPdfWithDiagnostics(html, css);
if (rendered.Diagnostics.MissingGlyphs.Count != 0)
{
throw new InvalidOperationException("The supplied fonts did not cover every document character.");
}

var pdfPath = Path.Combine(output, "invoice.pdf");
File.WriteAllBytes(pdfPath, rendered.Pdf);
var previews = engine.RenderImagePagesToDirectory(html, css, output, dpi: 96, stem: "invoice");
var inspection = FullBleedEngine.InspectPdf(pdfPath);
Console.WriteLine($"Created {pdfPath}: {inspection.PageCount} page(s), {previews.Paths.Count} PNG preview(s).");
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ nav:
- Your first PDF: getting-started/quickstart.md
- Use Rust: getting-started/rust.md
- Use Node.js: getting-started/node.md
- Use C# and .NET: getting-started/dotnet.md
- Use WooCommerce: guides/woocommerce.md
- Fonts and previews: getting-started/first-pdf.md
- Examples and downloads: examples.md
Expand Down
118 changes: 118 additions & 0 deletions tools/verify_dotnet_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Restore a fresh NuGet cache and execute the C# guide and designed invoice."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as ET

from pypdf import PdfReader

ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--package-source', default='https://api.nuget.org/v3/index.json')
parser.add_argument('--out', type=Path, default=Path('output/dotnet-verification'))
parser.add_argument('--update-assets', action='store_true')
args = parser.parse_args()
out = args.out.resolve()
out.mkdir(parents=True, exist_ok=True)
workspace = Path(tempfile.mkdtemp(prefix='fullbleed dotnet docs ')).resolve()
(workspace / 'global.json').write_text(json.dumps({
'sdk': {'version': '8.0.100', 'rollForward': 'latestFeature'}
}) + '\n', encoding='utf-8')
cache = workspace / 'nuget-cache'
env = dict(os.environ, NUGET_PACKAGES=str(cache),
NUGET_HTTP_CACHE_PATH=str(workspace / 'nuget-http-cache'))
env.pop('FULLBLEED_NATIVE_LIBRARY', None)
commands = []


def run(label, command, cwd=workspace):
result = subprocess.run(command, cwd=cwd, env=env, capture_output=True,
text=True, encoding='utf-8', timeout=240)
(out / f'{label}.stdout.txt').write_text(result.stdout, encoding='utf-8')
(out / f'{label}.stderr.txt').write_text(result.stderr, encoding='utf-8')
if result.returncode:
raise RuntimeError(f'{label} failed; see {out / (label + ".stdout.txt")}')
commands.append({'name': label, 'exit_code': 0})
return result.stdout


project_source = ROOT / 'examples/dotnet/NorthstarInvoice.csproj'
version = ET.parse(project_source).find('.//PackageReference').attrib['Version']
guide = (ROOT / 'docs/getting-started/dotnet.md').read_text(encoding='utf-8')
assert f'dotnet add package FullBleed.DotNet --version {version}' in guide
blocks = re.findall(r'```csharp\n(.*?)```', guide, re.S)
assert len(blocks) == 2

run('new-console', ['dotnet', 'new', 'console', '--framework', 'net8.0',
'--name', 'InvoiceDemo', '--output', 'InvoiceDemo', '--no-restore'])
snippet = workspace / 'InvoiceDemo'
(snippet / 'Program.cs').write_text(blocks[0], encoding='utf-8')
run('install-package', ['dotnet', 'add', 'package', 'FullBleed.DotNet', '--version',
version, '--source', args.package_source], snippet)
run('run-guide', ['dotnet', 'run', '-c', 'Release', '--no-restore'], snippet)
snippet_pdf = snippet / 'invoice.pdf'
reader = PdfReader(snippet_pdf, strict=True)
assert len(reader.pages) == 1
assert ' '.join(reader.pages[0].extract_text().split()) == (
'Invoice INV-1042 Consulting: USD 1,200.00'
)
shutil.copy2(snippet_pdf, out / 'quickstart.pdf')

sample = workspace / 'examples/dotnet'
sample.mkdir(parents=True)
for name in ['NorthstarInvoice.csproj', 'Program.cs']:
shutil.copy2(ROOT / 'examples/dotnet' / name, sample / name)
assets = workspace / 'docs/assets/showcase'
assets.mkdir(parents=True)
for name in ['invoice.html', 'invoice.css']:
shutil.copy2(ROOT / 'docs/assets/showcase' / name, assets / name)
shutil.copytree(ROOT / 'docs/assets/playground/fonts', workspace / 'docs/assets/playground/fonts')
run('restore-sample', ['dotnet', 'restore', '--source', args.package_source,
'--packages', str(cache), '--force-evaluate'], sample)
run('run-sample', ['dotnet', 'run', '-c', 'Release', '--no-restore', '--',
str(out / 'northstar')], sample)

installed = cache / 'fullbleed.dotnet' / version
provenance = json.loads((installed / 'native-provenance.json').read_text(encoding='utf-8'))
engine = next(p for p in provenance['dependencies'] if p['name'] == 'fullbleed')
assert f'It pins Fullbleed {engine["version"]}.' in guide
outputs = {}
for generated, published in [('invoice.pdf', 'invoice-dotnet.pdf'),
('invoice_page1.png', 'invoice-dotnet.png')]:
content = (out / 'northstar' / generated).read_bytes()
outputs[generated] = hashlib.sha256(content).hexdigest()
destination = ROOT / 'docs/assets/showcase' / published
if args.update_assets:
destination.write_bytes(content)
else:
assert content == destination.read_bytes(), f'Review regenerated {published}'
invoice = PdfReader(out / 'northstar/invoice.pdf', strict=True)
assert len(invoice.pages) == 1
text = ' '.join(invoice.pages[0].extract_text().split())
assert text.count('Maple & Finch') == 1 and text.count('$1,870.00') == 2
package = installed / f'fullbleed.dotnet.{version}.nupkg'
report = {
'ok': True,
'sdk': subprocess.check_output(['dotnet', '--version'], cwd=workspace, text=True).strip(),
'package_version': version,
'engine_version': engine['version'],
'package_source': args.package_source,
'package_sha256': hashlib.sha256(package.read_bytes()).hexdigest(),
'workspace': str(workspace),
'fresh_nuget_cache': True,
'fresh_nuget_http_cache': True,
'native_library_override': False,
'commands': commands,
'outputs': outputs,
'quickstart_pdf_sha256': hashlib.sha256(snippet_pdf.read_bytes()).hexdigest(),
'scope': 'The guide snippet and exact sample sources consume the NuGet package outside the checkout.',
}
(out / 'verification.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')
print(json.dumps({'ok': True, 'package': version, 'engine': engine['version'],
'commands': len(commands), 'package_source': args.package_source}))
Loading