Troubleshooting
Diagnose and fix common issues when generating PDF documents with Paperwork.Core.
Learning Objectives
By the end of this article, you’ll be able to:
- Diagnose common PDF generation errors
- Use logging to debug issues
- Fix template and parsing problems
- Resolve path and resource loading issues
- Optimize performance
Common Errors and Solutions
Error: “Could not find file”
Problem: Template file not found
System.IO.FileNotFoundException: Could not find file 'template.html'
Causes:
- Incorrect file path
- File doesn’t exist
- Wrong working directory
Solutions:
// ❌ Relative path might not resolve
builder.WithLayoutFile("template.html");
// âś… Use absolute path
string templatePath = Path.Combine(
AppContext.BaseDirectory,
"Templates",
"template.html"
);
builder.WithLayoutFile(templatePath);
// âś… Or use full path
string fullPath = Path.GetFullPath("./Templates/template.html");
builder.WithLayoutFile(fullPath);
// âś… Check if file exists first
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"Template not found: {templatePath}");
}
Error: “Document could not be created from the layout”
Problem: The template could not be parsed
303. Document could not be created from the layout, check the syntax and sources
Causes:
- Missing
<html>tag - Unclosed tags in an XHTML template (one with the
xmlnsnamespace) - Invalid XML, such as an unescaped
&
Solutions:
<!-- ❌ Unclosed tag with the XHTML namespace -->
<html xmlns='http://www.w3.org/1999/xhtml'>
<body>
<p>Content<br></p>
</body>
</html>
<!-- âś… Option 1: Close every tag -->
<html xmlns='http://www.w3.org/1999/xhtml'>
<body>
<p>Content<br /></p>
</body>
</html>
<!-- âś… Option 2: Remove the namespace, and the template is parsed as HTML5 -->
<html>
<body>
<p>Content<br></p>
</body>
</html>
The document builder chooses the parser from the <html> element: with the xmlns namespace the template must be valid XML, without it the more forgiving HTML5 parser is used.
Error: “Object reference not set to an instance”
Problem: Null reference exception
System.NullReferenceException: Object reference not set to an instance of an object
Common causes:
// ❌ Builder not created
IDocumentBuilder builder = null;
await builder.SaveAsync("output.pdf"); // NullReferenceException
// ❌ Missing data
builder.WithData("model", data); // data is null - may cause null reference in template
// âś… Check for null
if (data != null)
{
await factory.NewDocument()
.WithLayoutFile("template.html")
.WithData("model", data)
.SaveAsync("output.pdf");
}
Error: “Access to path is denied”
Problem: No permission to write file
System.UnauthorizedAccessException: Access to the path 'C:\output.pdf' is denied
Solutions:
// âś… Write to user documents folder
string outputPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"output.pdf"
);
// âś… Ensure directory exists and is writable
string directory = Path.GetDirectoryName(outputPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// âś… Check permissions
var fileInfo = new FileInfo(outputPath);
if (fileInfo.Exists && fileInfo.IsReadOnly)
{
fileInfo.IsReadOnly = false;
}
Logging and Diagnostics
Enable Logging
Set a log level on the document builder, and the log is added as pages at the end of the generated PDF.
using Paperwork.Generation;
await factory.NewDocument()
.WithLayoutFile("template.html")
.WithData("model", data)
.WithLogLevel(PaperworkRequestLogOption.Verbose)
.SaveAsync("output.pdf");
Remove the log level (or set it to Off) for production output.
Log Levels
| Level | When to Use |
|---|---|
Off |
No log (the default) |
Errors |
Only errors |
Warnings |
Errors and warnings |
Messages |
Info, warnings, and errors |
Verbose |
Everything (debugging) |
See Logging for more on the log output.
Template Debugging
Test Template Validity
BuildAsync reports problems in its result rather than throwing.
public async Task<string> CheckTemplate(IPaperworkFactory factory, string templatePath, object sampleData)
{
var result = await factory.NewDocument()
.WithLayoutFile(templatePath)
.WithData("model", sampleData)
.BuildAsync();
if (!result.Success)
return $"{result.Message} {result.Error}";
if (result.Document?.Binary == null || result.Document.Binary.Length == 0)
return "Generated PDF is empty";
return null; // No problems found
}
Resource Loading Issues
Image Not Found
Problem: Images don’t appear in PDF
<!-- ❌ Relative path might not resolve -->
<img src="./images/logo.png" />
Relative paths in a template loaded with WithLayoutFile are found relative to the template file. For a template given as a string or stream, there is no file to be relative to, so they are found from the application’s working directory.
Debug:
// Check the image exists next to the template
string imagePath = Path.Combine(Path.GetDirectoryName(templatePath), "images", "logo.png");
Console.WriteLine($"Image exists: {File.Exists(imagePath)}");
Solutions:
<!-- âś… Load the template with WithLayoutFile, and keep the path relative to the template -->
<img src="images/logo.png" />
<!-- âś… Use absolute path -->
<img src="C:/Projects/MyApp/images/logo.png" />
<!-- âś… Or embed as base64 -->
<img src="data:image/png;base64,iVBORw0KG..." />
External Stylesheet Not Loading
Problem: External CSS not applied
<!-- ❌ Path issues -->
<link rel="stylesheet" href="styles.css" />
Solutions:
<!-- âś… Use full path -->
<link rel="stylesheet" href="C:/Projects/MyApp/styles/common.css" />
<!-- âś… Or embed styles -->
<head>
<style>
/* Styles here */
</style>
</head>
Or add the stylesheet with the document builder, which reads the file from the path given.
builder.WithStyleFile("Templates/styles/common.css");
Remote Resource Timeout
Problem: Remote images/fonts timeout
<img src="https://example.com/slow-loading-image.jpg" />
Solutions:
// Configure the timeout on the HttpClient used by the factory
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var factory = PaperworkFactory.Create(client).Build();
// Or cache remote resources locally
string localImage = DownloadAndCache("https://example.com/image.jpg");
// Use local path in template
Performance Issues
Slow PDF Generation
Problem: PDF takes too long to generate
Diagnose:
Turn on the log, and it includes the time taken by each stage of generation.
await factory.NewDocument()
.WithLayoutFile("template.html")
.WithData("model", GetData())
.WithLogLevel(PaperworkRequestLogOption.Messages)
.SaveAsync("output.pdf");
Solutions:
- Re-use the factory, and cache template text:
private static readonly ConcurrentDictionary<string, string> _templateCache = new();
public IDocumentBuilder NewFromTemplate(IPaperworkFactory factory, string templatePath)
{
string html = _templateCache.GetOrAdd(templatePath, File.ReadAllText);
return factory.NewDocument().WithLayout(html);
}
- Optimize images:
<!-- ❌ Large unoptimized images -->
<img src="photo.jpg" /> <!-- 5MB file -->
<!-- âś… Optimized and sized -->
<img src="photo-optimized.jpg" style="width: 300pt;" /> <!-- 200KB -->
- Simplify CSS:
/* ❌ Complex selectors */
body div.content div.section div.subsection p.text span.highlight {
color: red;
}
/* âś… Simple selectors */
.highlight {
color: red;
}
- Specify table widths:
<!-- ❌ Auto-calculate widths (slower) -->
<table>
<tr>
<td>Content</td>
<td>More content</td>
</tr>
</table>
<!-- âś… Explicit widths (faster) -->
<table>
<colgroup>
<col style="width: 50%;" />
<col style="width: 50%;" />
</colgroup>
<tr>
<td>Content</td>
<td>More content</td>
</tr>
</table>
Memory Issues
Problem: Out of memory errors
Solutions:
// âś… Create one factory and dispose of it when finished
using var factory = PaperworkFactory.Create().Build();
// âś… Process in batches
foreach (var batch in items.Chunk(100))
{
ProcessBatch(batch);
GC.Collect(); // Force garbage collection between batches
}
// âś… Stream large images
// Instead of loading entire image into memory,
// stream from disk or URL
Data Binding Issues
Expression Not Evaluating
Problem: Data binding expressions show as literal text
<!-- Shows: {{model.name}} instead of actual name -->
<p>{{model.name}}</p>
Causes:
- Data not passed to document
- Wrong property name
- Null data
Debug:
// Check the data being passed
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(data));
Solutions:
// âś… Ensure data is passed
var model = new
{
name = "John Doe",
email = "john@example.com"
};
builder.WithData("model", model);
// âś… Verify property names match
// Template uses: {{model.name}}
// Data must have: .name property
Testing Strategies
Unit Test Template Generation
[Test]
public async Task TestInvoiceGeneration()
{
// Arrange
var testData = new Invoice
{
Number = "INV-001",
CustomerName = "Test Customer",
Items = new List<InvoiceItem>
{
new InvoiceItem { Description = "Item 1", Amount = 100.00m }
},
Total = 100.00m
};
// Act
using var factory = PaperworkFactory.Create().Build();
byte[] pdfBytes = await factory.NewDocument()
.WithLayoutFile("./Templates/invoice.html")
.WithData("model", testData)
.BuildBytesAsync();
// Assert
Assert.IsNotNull(pdfBytes);
Assert.Greater(pdfBytes.Length, 0);
Assert.That(pdfBytes[0], Is.EqualTo(0x25)); // PDF signature '%'
}
Integration Test with File Output
[Test]
public async Task TestPdfFileGeneration()
{
// Arrange
string outputPath = Path.Combine(Path.GetTempPath(), "test.pdf");
try
{
// Act
using var factory = PaperworkFactory.Create().Build();
await factory.NewDocument()
.WithLayoutFile("template.html")
.SaveAsync(outputPath);
// Assert
Assert.That(File.Exists(outputPath));
var fileInfo = new FileInfo(outputPath);
Assert.Greater(fileInfo.Length, 0);
// Verify PDF signature
byte[] header = new byte[4];
using (var fs = File.OpenRead(outputPath))
{
fs.Read(header, 0, 4);
}
Assert.That(header, Is.EqualTo(new byte[] { 0x25, 0x50, 0x44, 0x46 })); // %PDF
}
finally
{
// Cleanup
if (File.Exists(outputPath))
{
File.Delete(outputPath);
}
}
}
Getting Help
Check Documentation
- HTML Element Reference - Supported elements
- CSS Property Reference - Supported CSS
Community Resources
- GitHub Issues - Report layout and rendering bugs in the Scryber engine
- GitHub Discussions - Ask questions
Reporting Issues
When reporting an issue, include:
- Package versions:
Console.WriteLine(typeof(Paperwork.PaperworkFactory).Assembly.GetName().Version); Console.WriteLine(typeof(Scryber.Components.Document).Assembly.GetName().Version); - Minimal reproducible example:
- Simplest template that demonstrates the issue
- Minimal C# code
- Sample data if applicable
- Error message and stack trace:
try { // Your code } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); Console.WriteLine($"Stack: {ex.StackTrace}"); } - Environment:
- .NET version
- Operating system
- Hosting environment (console app, ASP.NET, Azure, etc.)
Quick Troubleshooting Checklist
When encountering issues, check:
- File paths are correct and files exist
- Template has valid HTML/XHTML structure
- XHTML templates (with the
xmlnsnamespace) are valid XML - Data is passed to the document with
WithData - Property names in template match data model
- Images and resources are accessible
- The factory is created once and re-used
- Sufficient permissions to write files
- Error handling is in place
- Logging is enabled for debugging
Next Steps
Now that you can troubleshoot issues:
- Data Binding - Make documents dynamic
- Document Configuration - Logging and error handling
- Practical Applications - Real-world examples
Start building dynamic documents → Data Binding & Expressions