Library Quick Start
Follow along if you want to understand some of the key features and benefits of the Paperwork.Core library. Here we add the package and build out a data template with dynamic styling and some repeating content. Based on your knowledge level you may then want to dive straight into your own use-cases to complete, or review the reference section to understand supported features.
On this page
Application Type
If you don’t have an existing application, or platform you want to add Paperwork.Core to, then create a new console application.
dotnet new console -n HelloPaperwork
cd HelloPaperwork
Paperwork.Core can be run and loaded from executables in all platforms that run dotnet, including client side Blazor WASM, but in this example we will not cover the outer execution.
Consult your own IDE documentation if you are not sure how to do this, or follow the Learning Guides articles.
Adding Paperwork.Core
The easiest way to get started is to add the NuGet package to a new or existing project, either from your IDE or from the command line in the project folder.
dotnet add package Paperwork.Core
The package is located at https://www.nuget.org/packages/Paperwork.Core
It brings in everything needed to generate documents, including the open-source Scryber engine that does the layout and rendering.
Create a template file
The first thing we can do is add a simple html template file (the ubiquitous Hello World) as a starter and call the file ‘hello.html’.
hello.html
Save the file in your project, and we can move on to generate our first PDF in code.
Generate our first template
At an appropriate location within your code (in the main function, responding to an event, or handling a web request) add the following.
PDF Generation
using Paperwork;
//Create a factory once, and re-use it for every document you generate
using var factory = PaperworkFactory.Create().Build();
//Start a new document, give it the template you saved, and write the PDF file
await factory.NewDocument()
.WithLayoutFile("hello.html")
.SaveAsync("hello.pdf");
The factory is the entry point to the library. Each call to NewDocument() returns a fluent builder that collects the layout, styles and data, and then generates the document when you call one of the output methods.
| Method | Returns |
|---|---|
SaveAsync(path) |
Writes the PDF to a file. |
BuildBytesAsync() |
The PDF as a byte[], to stream to a response or store. Throws if generation fails. |
BuildAsync() |
A PaperworkResult with Success, Message and Error, plus the generated Document.Binary. |
The layout can also be given as a string with WithLayout(html), a Stream, or a remote address with WithLayoutUrl(url).
Viewing the result
The resultant file should now be able to be opened in any PDF Viewer application or browser, and should look like this.

Troubleshooting
If it didn’t work at all, then check the package and the template structure (it needs to be valid xml and include the namespace).
If it did work, but doesn’t look like it should, then you can see what the library is doing by turning on the log. Any errors and messages are then added as pages at the end of the output.
using Paperwork.Generation;
await factory.NewDocument()
.WithLayoutFile("hello.html")
.WithLogLevel(PaperworkRequestLogOption.Messages)
.SaveAsync("hello.pdf");
The levels are Off (the default), Errors, Warnings, Messages and Verbose.
Making it dynamic
This is fine, but not exactly dynamic! We can add a little more ‘pizzazz’ with some data. We can do anything we want between parsing, setting content, adding components (tables, lists, sections etc), updating styles and much more.
But - one of the core principles of Paperwork is a clear separation of data, from layout and style. So we can add a named data object to the document, which the template can access during processing.
Add our user
await factory.NewDocument()
.WithLayoutFile("hello.html")
//set the 'user' to an instance value
.WithData("user", new { firstName = "John", lastName = "Smith" })
.SaveAsync("hello.pdf");
The data can be any object, or a JSON string, stream or file.
await factory.NewDocument()
.WithLayoutFile("hello.html")
.WithData("user", "{ \"firstName\": \"Jane\", \"lastName\": \"Doe\" }")
.SaveAsync("hello.pdf");
//or .WithDataFile("user", "user.json")
Content binding syntax
The template databinding syntax is based on {{handlebars}}, which can appear in the content, or within attribute values.
<h1>Hello {{user.firstName}}</h1>
The library supports the standard ‘dot’ notation to access inner values (along with indexors ‘[]’ for arrays and dictionarys).
Personalize the template
So we can update our template to use this data within the content.
Adding expressions
With dynamic content
Generating the output we should have custom values placed naturally within the document.

Style binding syntax
Along with content, the template styles can be ‘bound’ to dynamic values using the well known var() and calc() functions.
<h1 style='color: var(brand.mainColor, black)'>Hello {{concat(user.firstName, " ", user.lastName)}}</h1>
And these can either be used directly, or indirectly through css variables, within the template styles, or referenced stylesheets.
<style>
:root{
--brand-bg : var(brand.lightColor, transparent);
}
.branded{
background-color : var(--brand-bg);
color: var(brand.mainColor);
}
</style>
Looping through content.
The library also uses the standard {{#each items}} and {{#if test}} expressions for loops and decisions.
The current item within the loop can be accessed using the this reference.
{{#if count(container.items) > 0 }}
{{#each container.items}}<span class='item'>{{this.property}}</span><{{/each}}>
{{/if }}
Extending our sample
If we add more data to our user - a role and some skills with a level - and also add some custom branding.
Add our user
await factory.NewDocument()
.WithLayoutFile("hello.html")
//set the user to an instance (or JSON) value
.WithData("user", new { firstName = "John", lastName = "Smith", role = "Engineering Lead",
emailAddress = "j.smith@company.com", profilePicture = "",
skills = new[] {
new { name = "C Sharp", level = 90 },
new { name = "JavaScript", level = 75 },
new { name = "Scrum master", level = 60 },
new { name = "Team leader", level = 80 } } })
//add some custom style information.
.WithData("brand", new { mainColor = "#5980a6", darkColor = "#1f3347", lightColor = "#e8eef4" })
.SaveAsync("hello.pdf");
Update the template
We can now make some changes to our template and use the provided data to create a more useful template. Including:
- remote fonts from Google (‘Barlow’ and ‘Barlow Condensed’) and icons from Font Awesome
- css variables from the brand data, with fallback defaults
- a header band with a diagonal gradient, and positioned, rotated and transparent text
- a decision for a profile picture, or an icon if there isn’t one
- a clickable email link
- a collection function to count the skills, shown in a rotated stamp with a radial gradient
- a two column list of skills, each with a gradient level bar sized from the data
- a fallback when there are no skills
Checking the result
Hopefully you are able re-generate and see the following output.
We have added a lot into the template - looping, gradients, transforms, columns, images, links, styles, functions, positioning, remote fonts and stylesheets.
But the static styles can be moved to a separate linked file (or added with .WithStyleFile("styles.css")), and the actual body is only about 35 lines long, and quite readable.
Returning a PDF from a web request
In an ASP.NET application, register the factory with dependency injection once at startup.
using Paperwork.Generation;
builder.Services.AddPaperwork(builder.Configuration);
Then ask for an IPaperworkFactory wherever you need it, and return the bytes as the response.
app.MapGet("/hello", async (IPaperworkFactory factory) =>
{
var pdf = await factory.NewDocument()
.WithLayoutFile("hello.html")
.WithData("user", new { firstName = "John", lastName = "Smith" })
.BuildBytesAsync();
return Results.File(pdf, "application/pdf", "hello.pdf");
});
What’s next
This has been a quick dive into the capabilities of the core engine. There is still a lot that is possible!
-
The Factory and the Document Builder - every option for setting up the factory and building documents
-
Data binding functions and the reference listing
-
Dynamic SVG drawings and charts and the reference contents.
-
Typography and fonts for using the standard fonts, and including custom fonts.
-
Updating and modifying existing PDFs using Framesets and Frames
-
Page sizes and column layout.
-
Embedding content and Attachments in files.
-
Complete CSS property support reference
-
Security, permissions and document meta-data output
-
Logging and Tracing output.
Or just jump into the Learning Guides to take you through each feature in turn.