This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Scryber.Core is a sophisticated PDF generation engine for .NET that converts HTML/XML templates with CSS styling into PDF documents. It supports .NET 9, 8, 6, and Standard 2.0, with WASM compatibility for Blazor applications.
For developers using Scryber as a NuGet package (not working with the source code)
# Core library
dotnet add package Scryber.Core
# For ASP.NET MVC integration
dotnet add package Scryber.Core.Mvcusing Scryber.Components;
using System.IO;
// Parse HTML template
using (var doc = Document.ParseDocument("template.html"))
{
// Add data to template
doc.Params["model"] = new
{
title = "My Report",
items = new[] { "Item 1", "Item 2" }
};
// Generate PDF - SaveAsPDF handles everything!
using (var stream = new FileStream("output.pdf", FileMode.Create))
{
doc.SaveAsPDF(stream);
}
}Important: Don't call doc.ProcessDocument() - it doesn't exist. SaveAsPDF() handles all processing automatically.
Scryber supports 80+ HTML elements. Common elements include:
Layout & Structure:
<div>,<span>,<p>,<section>,<article>,<header>,<footer>,<main>,<aside>,<nav>
Text & Formatting:
<h1>through<h6>,<strong>,<em>,<b>,<i>,<u>,<small>,<mark>,<del>,<ins>,<sup>,<sub><blockquote>,<pre>,<code>
Lists:
<ul>,<ol>,<li>- Full support for nested lists with CSS counters
Tables:
<table>,<thead>,<tbody>,<tfoot>,<tr>,<th>,<td>- Supports
colspan,rowspan, and complex table layouts
Images & Media:
<img>- JPEG, PNG, GIF, TIFF formats<svg>- Full SVG path support (circles, rectangles, paths, text)- Data URLs:
<img src="data:image/png;base64,..." />
Forms (visual only, not interactive):
<form>,<input>,<textarea>,<select>,<button>,<label>,<fieldset>,<legend>
Semantic:
<time>,<address>,<details>,<summary>
Special:
<br />,<hr />,<link>(for CSS),<style>,<template>(for data binding)
Scryber implements most CSS 2.1 properties plus common CSS3 features:
Box Model:
margin,margin-top/right/bottom/leftpadding,padding-top/right/bottom/leftborder,border-width,border-style,border-color,border-radiuswidth,height,min-width,min-height,max-width,max-height
Important - Margin Collapsing: Unlike browsers, Scryber does NOT collapse adjacent top/bottom margins between sibling elements. If two siblings have margin-bottom: 20pt and margin-top: 20pt, the total space will be 40pt (not 20pt as in browsers). Plan your spacing accordingly and consider using smaller margins or margin on only one side.
Typography:
font-family,font-size,font-weight,font-stylecolor,text-align,text-decoration,text-transformline-height,letter-spacing,word-spacingwhite-space,text-indent,vertical-align
Background:
background-color,background-image,background-position,background-repeat,background-size
Positioning:
position(relative, absolute),top,right,bottom,leftdisplay(block, inline, inline-block, none, table, table-row, table-cell)float,clear
Important - Float Behavior in Scryber:
- Element Order: Elements with
float: rightmust appear BEFORE non-floating inline content in HTML source order, otherwise they will wrap to the next line.
❌ Wrong (float: right wraps to next line):
<div>
<span class="label">Text</span>
<span class="value" style="float: right;">Value</span>
</div>✅ Correct (float: right appears first):
<div>
<span class="value" style="float: right;">Value</span>
<span class="label">Text</span>
</div>- Element Width: Floating elements should have explicit width to prevent page overflow. If a floated element's width is not constrained, its inner content may use full width and push beyond page boundaries.
❌ Wrong (float: right with unconstrained width can overflow):
<div class="header" style="float: right;">
<h1>Long Title Text</h1> <!-- Takes full width -->
</div>✅ Correct (float: right with explicit width):
<div class="header" style="float: right; width: 300pt;">
<h1>Long Title Text</h1> <!-- Constrained to 300pt -->
</div>Layout:
overflow(hidden, visible, clip)visibility(visible, hidden)z-index
Page:
@pagerules:size(A4, Letter, custom),marginpage-break-before,page-break-after,page-break-inside
Lists:
list-style-type,list-style-position,list-style-imagecounter-reset,counter-increment,content: counter()
Advanced:
- CSS Variables:
--custom-propertyandvar(--custom-property) calc()expressions:width: calc(100% - 20px)- Pseudo-elements:
:before,:after
Units Supported: pt, px, mm, cm, in, em, rem, %
Scryber provides a comprehensive template expression system using Handlebars syntax with 6 helpers, 15 operators, and 90+ functions.
Template expressions use handlebars syntax: {{expression}}
-
Use
{{expression}}NOT${expression}- ✅ Correct:
{{model.price}}- Handlebars syntax, will be evaluated - ❌ Wrong:
${model.price}- JavaScript template literal syntax, will NOT work - ✅ Dollar signs:
${{model.price}}- The$is just a character,{{...}}is the expression - Why: HTML templates are parsed, not evaluated as JavaScript
- ✅ Correct:
-
Cannot call C# static methods in templates
- ❌ Wrong:
{{DateTime.Now}},{{String.Format(...)}},{{Math.PI}} - ✅ Correct: Pass data from C# code, then reference in template
- Templates can only access:
- Data passed via
doc.Params["key"] - Expression functions (like
format(),concat(),pi()) - Current context (
this,model, variables)
- Data passed via
- ❌ Wrong:
Access Data:
{{model.title}}
{{model.user.firstName}}
{{model.items[0].name}}Mathematical Operations:
{{price * 1.2}}
{{width + 10}}
{{total / count}}
<var data-id="result" data-value="{{value / maxValue * 100}}" />Important: Use standard operators (+, -, *, /), not calc() function in expressions.
Template Variables:
<!-- Define -->
<var data-id="myVar" data-value="{{someCalculation}}" />
<!-- Use directly by name -->
{{myVar}}
{{myVar * 2}}Template Variables Inside Loops:
<!-- Variables inside loops update automatically each iteration -->
{{#each model.items}}
<var data-id="itemX" data-value="{{this.value * 10}}" />
<var data-id="itemY" data-value="{{110 + (@index * 35)}}" />
<!-- Use the variables - they update each iteration -->
<rect x="{{itemX}}" y="{{itemY}}" width="50" height="25" />
{{/each}}Important: Don't try to create unique variable names with @index:
❌ Wrong (nested binding doesn't work):
{{#each collection}}
<var data-id="myVar_{{@index}}" data-value="{{calculation}}" />
<rect x="{{myVar_{{@index}}}}" /> <!-- Doesn't work! -->
{{/each}}✅ Correct (variables update each iteration):
{{#each collection}}
<var data-id="myVar" data-value="{{calculation}}" />
<rect x="{{myVar}}" /> <!-- Works! Updates each iteration -->
{{/each}}Scryber implements 6 Handlebars helpers that compile to underlying Scryber XML components:
1. {{#each}} - Iteration Helper
{{#each model.items}}
<p>{{this.name}}</p>
{{/each}}- Compiles to:
<template data-bind="{{collection}}">using ForEach component - Special variables:
@index(zero-based),@first(boolean),@last(boolean) - Context:
thisrefers to current item,../accesses parent scope
{{#each model.products}}
<p>{{@index}}. {{this.name}} - ${{this.price}}</p>
{{#if @first}}<hr />{{/if}}
{{/each}}2. {{#with}} - Context Switching
{{#with model.user}}
<p>Name: {{this.firstName}} {{this.lastName}}</p>
<p>Email: {{this.email}}</p>
{{/with}}- Compiles to:
<template data-bind="{{object}}">with context switching - Aliasing:
{{#with object as | alias |}} - Parent access: Use
../to access parent scope
3. {{#if}} / {{else if}} / {{else}} - Conditionals
{{#if model.score >= 90}}
<p class="grade-a">Excellent!</p>
{{else if model.score >= 70}}
<p class="grade-b">Good</p>
{{else}}
<p class="grade-c">Needs improvement</p>
{{/if}}- Compiles to:
<choose><when><otherwise>element structure - Supported operators:
==,!=,<,<=,>,>=,&&,|| - Works with:
#eachand#withfor fallback cases
4. {{log}} - Debugging Helper
{{log "Debug message: " model.value}}
{{log "Error occurred" level="error" category="validation"}}- Levels:
debug(Verbose),info(Message),warn(Warning),error(Error) - Category: Optional category for filtering logs
- Output: Writes to Scryber trace log during data binding
Operators work in expressions with proper precedence levels:
Arithmetic Operators (Precedence 3-5):
+Addition (precedence 5)-Subtraction (precedence 5)*Multiplication (precedence 4)/Division (precedence 4)%Modulus (precedence 4)^Power/Exponentiation (precedence 3)
Comparison Operators (Precedence 6-7):
==Equality (precedence 7)!=Inequality (precedence 7)<Less than (precedence 6)<=Less than or equal (precedence 6)>Greater than (precedence 6)>=Greater than or equal (precedence 6)
Logical Operators (Precedence 9-10):
&&Logical AND (precedence 9)||Logical OR (precedence 10)??Null coalescing (precedence 8)
Example with precedence:
{{model.quantity * model.price + model.tax}} <!-- * before + -->
{{model.score > 70 && model.attendance >= 0.8}} <!-- > before && -->
{{model.value ?? model.defaultValue}} <!-- Use default if null -->Functions are organized into 8 categories:
Conversion Functions (7 functions):
int(),long(),double(),decimal()- Numeric conversionsbool()- Boolean conversiondate()- Date parsing/conversiontypeof()- Type information
String Functions (18 functions):
format(value, formatString)- Format numbers/dates (also aliased asstring())concat(str1, str2, ...)- Concatenate stringsjoin(array, delimiter)- Join array with delimitersubstring(str, start, length)- Extract substringreplace(str, find, replaceWith)- Replace texttoLower(str),toUpper(str)- Case conversiontrim(str),trimEnd(str)- Remove whitespacelength(str)- String lengthcontains(str, search),startsWith(str, prefix),endsWith(str, suffix)- String testingindexOf(str, search)- Find positionpadLeft(str, length, char),padRight(str, length, char)- Paddingsplit(str, delimiter)- Split into arrayregexIsMatch(str, pattern),regexMatches(str, pattern),regexReplace(str, pattern, replacement)- Regex operations
Mathematical Functions (21 functions):
abs(),ceiling(),floor(),round(),truncate()- Roundingsqrt(),pow(),exp(),log(),log10()- Power and logarithmssign()- Sign determinationsin(),cos(),tan(),asin(),acos(),atan()- Trigonometrydegrees(),radians()- Angle conversionpi(),e()- Constantsrandom()- Random number generation
Date/Time Functions (19 functions):
- Add functions (6):
addDays(),addMonths(),addYears(),addHours(),addMinutes(),addSeconds(),addMilliseconds() - Between functions (4):
daysBetween(),hoursBetween(),minutesBetween(),secondsBetween() - Extract functions (9):
yearOf(),monthOfYear(),dayOfMonth(),dayOfWeek(),dayOfYear(),hourOf(),minuteOf(),secondOf(),millisecondOf()
Logical Functions (3 functions):
if(condition, trueValue, falseValue)- Inline conditional (ternary)ifError(expression, fallbackValue)- Error handling with fallbackin(value, collection)- Membership test
Collection Functions (13 functions):
count(collection)- Count itemscountOf(collection, .property, value)- Conditional countsum(collection)- Sum valuessumOf(collection, .property)- Sum property valuesmin(collection),max(collection)- Find extremesminOf(collection, .property),maxOf(collection, .property)- Property extremescollect(collection, .property)- Extract property arrayselectWhere(collection, .property, value)- Filter collectionfirstWhere(collection, .property, value)- Find first matchsortBy(collection, .property)- Sort ascendingreverse(collection)- Reverse order
Important: Property parameters use dot notation (.property), not strings ('property')
Statistical Functions (5 functions):
average(collection),averageOf(collection, .property)- Arithmetic meanmean(collection)- Mathematical mean (synonym for average)median(collection)- Middle value (robust against outliers)mode(collection)- Most frequent value
CSS Functions (2 functions):
calc(expression)- Generate CSS calc() expression for dynamic stylesvar(variableName, fallbackValue)- Reference CSS custom properties
Inline Conditionals:
{{if(age >= 18, 'Adult', 'Minor')}}
{{if(stock > 0, concat('$', string(price)), 'Out of Stock')}}Collection Operations:
<p>Total: ${{sumOf(model.items, .price)}}</p>
<p>Average: ${{round(averageOf(model.items, .price), 2)}}</p>
<p>Items: {{count(model.items)}}</p>Date Formatting:
<p>Date: {{format(model.orderDate, 'MMMM dd, yyyy')}}</p>
<p>Time: {{format(model.timestamp, 'h:mm tt')}}</p>
<p>Days until delivery: {{daysBetween(model.today, model.deliveryDate)}}</p>Important - JSON Date Strings: When working with JSON data, date properties are strings and must be converted to DateTime objects before formatting:
❌ Wrong (trying to format a string):
<!-- JSON: "reportDate": "2024-03-15T00:00:00" -->
<p>{{format(model.reportDate, 'MMMM dd, yyyy')}}</p> <!-- Won't work - it's a string! -->✅ Correct (convert string to DateTime first):
<!-- JSON: "reportDate": "2024-03-15T00:00:00" -->
<p>{{format(date(model.reportDate), 'MMMM dd, yyyy')}}</p> <!-- Works! -->The date() function parses the ISO 8601 date string into a DateTime object that format() can work with. This applies to all date operations:
<!-- Formatting dates from JSON -->
<p>Report Date: {{format(date(model.reportDate), 'MMMM dd, yyyy')}}</p>
<p>Due: {{format(date(model.dueDate), 'MMM dd')}}</p>
<!-- Date math with JSON dates -->
<p>Days remaining: {{daysBetween(date(model.startDate), date(model.endDate))}}</p>String Manipulation:
<p>{{toUpper(model.code)}}</p>
<p>{{concat(model.firstName, ' ', model.lastName)}}</p>
<p>{{join(model.tags, ', ')}}</p>Statistical Analysis:
<p>Mean: {{round(mean(model.scores), 1)}}</p>
<p>Median: {{median(model.scores)}}</p>
<p>Range: {{min(model.scores)}} - {{max(model.scores)}}</p>{{#each model.items}}
<!-- Current item -->
{{this.name}}
<!-- Dot prefix = current item -->
{{.name}}
<!-- Root parameters (no ../ needed) -->
{{model.title}}
<!-- Parent scope with ../ (for nested loops) -->
{{#each this.children}}
{{../this.name}} <!-- Parent loop item -->
{{model.title}} <!-- Root parameter - no ../ needed -->
{{/each}}
<!-- Special iteration variables -->
{{@index}} <!-- Zero-based index -->
{{@first}} <!-- true if first item -->
{{@last}} <!-- true if last item -->
{{/each}}Important: Root parameters (like model) are always accessible directly - you don't need ../ to access them:
- ✅ Correct:
{{model.propertyName}} - ❌ Wrong:
{{../model.propertyName}}
The parent selector ../ is only needed when navigating between nested loops, not for accessing root parameters.
IMPORTANT: Templates cannot call static C# methods like DateTime.Now:
❌ Wrong - Static methods don't work in templates:
<!-- This will NOT work - templates can't call C# static methods -->
<p>Generated: {{DateTime.Now}}</p>
<p>Date: {{DateTime.Today}}</p>✅ Correct - Pass data from C# code:
// C# code - DateTime.Now is fine here
doc.Params["model"] = new {
generatedDate = DateTime.Now, // OK in C# code
reportDate = DateTime.Today // OK in C# code
};<!-- Template - use the passed data -->
<p>Generated: {{format(model.generatedDate, 'yyyy-MM-dd HH:mm:ss')}}</p>
<p>Date: {{format(model.reportDate, 'MMMM dd, yyyy')}}</p>For Documentation & Examples: Use fixed dates for reproducible output:
// Use fixed dates in documentation examples
doc.Params["model"] = new {
reportDate = new DateTime(2024, 3, 15), // Fixed date
dueDate = new DateTime(2024, 4, 14) // Predictable output
};This ensures:
- Templates work (can't call C# static methods)
- PDF output is reproducible (no live timestamps changing on each generation)
- Examples are testable (predictable output)
1. External CSS for Clean Separation:
<html>
<head>
<link rel="stylesheet" href="styles.css" type="text/css" />
</head>
<body>
<div class="header">{{model.title}}</div>
</body>
</html>2. JSON Data Binding:
// Scryber handles JSON automatically
string jsonContent = File.ReadAllText("data.json");
var model = JsonSerializer.Deserialize<object>(jsonContent);
doc.Params["model"] = model; // That's it!3. Dynamic Tables:
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{{#each model.products}}
<tr>
<td>{{this.name}}</td>
<td>{{format(this.price, 'C2')}}</td>
</tr>
{{/each}}
</tbody>
</table>4. Conditional Styling:
{{#each model.items}}
<div class="{{if(this.isActive, 'active', 'inactive')}}">
{{this.name}}
</div>
{{/each}}5. SVG Graphics:
<svg width="200" height="100">
{{#each model.data}}
<var data-id="barX" data-value="{{@index * 40}}" />
<var data-id="barHeight" data-value="{{this.value}}" />
<rect x="{{barX}}"
y="{{100 - barHeight}}"
width="35"
height="{{barHeight}}"
fill="#336699" />
{{/each}}
</svg>6. Page Headers/Footers:
<html>
<head>
<style>
@page {
size: A4;
margin: 20mm;
}
</style>
</head>
<body>
<header>
<div>Report: {{model.title}}</div>
</header>
<main>
<!-- Content -->
</main>
<footer>
<div>Page <page-number /></div>
</footer>
</body>
</html>How Layout Engine Handles Headers/Footers:
Scryber's layout engine intelligently calculates available content space:
- First: Applies
@pagemargin and padding from CSS - Second: Positions and measures
<header>and<footer>elements - Third: Calculates remaining available space
- Finally: Flows content within that available space
@page {
size: A4;
margin: 0;
padding: 10mm; /* Applied first */
}With this CSS, Scryber will:
- Apply 10mm padding on all sides
- Render the header/footer and measure their actual height
- Calculate:
availableContentHeight = pageHeight - padding - headerHeight - footerHeight - Flow content in the remaining space (no overlap with header/footer)
Benefits:
- No need to guess footer height
- Adapts automatically if header/footer content changes
- Content never overlaps with headers or footers
- Both
marginandpaddingon@pageare respected
7. Cover Pages with Custom Footers:
For multi-page reports with cover pages, you can prevent footers from appearing on the cover page:
<html>
<head>
<style>
@page {
size: A4;
margin: 15mm;
}
@page cover {
margin: 0; /* No margins for cover */
}
.cover-page {
page: cover; /* Use named page type */
page-break-after: always;
width: 210mm;
height: 297mm;
background-image: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
color: white;
position: absolute;
left: 0;
top: 0;
}
</style>
</head>
<body>
<!-- Cover page -->
<div class="cover-page">
<h1>Report Title</h1>
<p>{{model.reportMonth}} {{model.reportYear}}</p>
</div>
<!-- Content pages -->
<div class="section">
<h2>Section 1</h2>
<p>Content...</p>
</div>
<!-- Empty footer (won't show anywhere) -->
<footer></footer>
<!-- Continuation footer (shows on all pages except cover) -->
<continuation-footer class="footer">
<p>{{model.companyName}} - Report</p>
<p>Page <page-number /> of <page-count /></p>
</continuation-footer>
</body>
</html>Key Points:
@page cover { margin: 0; }creates a named page type with no margins.cover-pageusespage: cover;to use the named page type- Empty
<footer></footer>ensures no footer appears on any page <continuation-footer>shows footer only on continuation pages (not cover)- Cover page uses
position: absolutewith explicit dimensions for precise layout
8. Multi-File Structure (Template + CSS + Data):
template.html:
<html>
<head>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
{{#each model.sections}}
<section class="report-section">
<h2>{{this.title}}</h2>
<p>{{this.content}}</p>
</section>
{{/each}}
</body>
</html>styles.css:
@page { size: A4; margin: 20mm; }
body { font-family: Arial; }
.report-section { margin-bottom: 30pt; }data.json:
{
"sections": [
{ "title": "Introduction", "content": "..." },
{ "title": "Analysis", "content": "..." }
]
}generator.cs:
var model = JsonSerializer.Deserialize<object>(File.ReadAllText("data.json"));
using (var doc = Document.ParseDocument("template.html"))
{
doc.Params["model"] = model;
using (var stream = new FileStream("report.pdf", FileMode.Create))
{
doc.SaveAsPDF(stream);
}
}using Scryber.Components.Mvc;
public class ReportController : Controller
{
public IActionResult DownloadPdf()
{
var model = new ReportViewModel
{
Title = "Sales Report",
Data = GetSalesData()
};
// Return PDF directly
return this.PDF("~/Views/Report/Template.html", model);
}
}- Reuse Templates: Parse template once, generate multiple PDFs with different data
- External Resources: Use absolute paths or data URLs for images in production
- WASM: Use
SaveAsPDFAsync()for Blazor WebAssembly - Large Documents: Enable compression in PDF writer for smaller file sizes
- Fonts: Standard PDF fonts (Helvetica, Times, Courier) are embedded - no external files needed
Avoiding Blank Pages Between Sections:
When using both page-break-after and page-break-before, you may create unwanted blank pages:
❌ Wrong (creates blank page between TOC and first section):
.toc-page {
page-break-after: always; /* Forces page break after TOC */
}
.section {
page-break-before: always; /* Forces page break before section */
}
/* Result: TOC → blank page → Section */✅ Correct (no blank page):
.toc-page {
/* No page-break-after */
}
.section {
page-break-before: always; /* Only one page break directive */
}
/* Result: TOC → Section (no blank page) */Best Practice: Use page-break-before: always on content sections, but don't add page-break-after: always on preceding content unless you specifically want a blank page separator.
Namespace Considerations:
The <var> element is an HTML element, while SVG elements are in the SVG namespace. Complex nested variable references across namespaces can cause XML parsing errors.
❌ Problematic (too many intermediate variables):
<svg>
<var data-id="maxRevenue" data-value="{{maxOf(model.regions, .revenue)}}" />
<var data-id="chartHeight" data-value="250" />
<var data-id="barWidth" data-value="120" />
{{#each model.regions}}
<var data-id="barX" data-value="{{100 + (@index * 170)}}" />
<var data-id="barHeight" data-value="{{this.revenue / maxRevenue * chartHeight}}" />
<var data-id="barY" data-value="{{280 - barHeight}}" />
<rect x="{{barX}}" y="{{barY}}" width="{{barWidth}}" height="{{barHeight}}" />
{{/each}}
</svg>✅ Better (inline calculations with parentheses):
<svg>
<var data-id="maxRevenue" data-value="{{maxOf(model.regions, .revenue)}}" />
{{#each model.regions}}
<var data-id="barX" data-value="{{100 + (@index * 170)}}" />
<var data-id="barHeight" data-value="{{(this.revenue / maxRevenue) * 250}}" />
<var data-id="barY" data-value="{{280 - barHeight}}" />
<rect x="{{barX}}" y="{{barY}}" width="120" height="{{barHeight}}" />
{{/each}}
</svg>Guidelines:
- Minimize intermediate variables defined outside SVG elements
- Use parentheses for clarity in calculations:
{{(value / max) * 250}} - Define variables locally within loops where they're used
- Use literal values for constants rather than variables
Common Issues:
- "ProcessDocument() not found" - Don't call it! Use
SaveAsPDF()directly - Variables not working - Access by name:
{{varName}}, not{{Document.Params.varName}} - Math not working - Use standard operators:
{{a + b}}, not{{calc(a, '+', b)}} - Expressions not evaluating - Use
{{expression}}NOT${expression}. Scryber uses Handlebars syntax, not JavaScript template literals - DateTime.Now not working - Templates can't call static C# methods. Using
{{DateTime.Now}}won't work. Pass dates from C# code:doc.Params["date"] = DateTime.Now;then use{{date}}in template - Date formatting not working with JSON - JSON date properties are strings, not DateTime objects. Convert them first:
{{format(date(model.reportDate), 'MMMM dd, yyyy')}}NOT{{format(model.reportDate, 'MMMM dd, yyyy')}}. Thedate()function converts the string to a DateTime object thatformat()can process. - Special characters showing as ? or boxes - Standard PDF fonts (Helvetica, Times, Courier) don't support Unicode symbols like ✓ ○ ★. Best solution: Use Font Awesome v5 icons with
<i class="fas fa-check-circle"></i>. Alternative: Use ASCII like[X]and[ ]. - Images not loading - Check file paths are absolute or use data URLs
- CSS not applied - Ensure
<link>has correcthrefpath - Blank pages appearing - Check for duplicate page breaks (
page-break-afteron one element andpage-break-beforeon the next). Use only one page break directive between sections. - XML parsing errors with SVG - Reduce intermediate
<var>elements and use inline calculations with parentheses for complex expressions
Scryber can append a detailed processing trace log to the end of generated PDFs for debugging and performance analysis.
Method 1: Processing Instruction in Template
<!DOCTYPE html>
<?scryber append-log='true' ?>
<html>
<!-- your template content -->
</html>Method 2: Programmatically on Document
using (var doc = Document.ParseDocument("template.html"))
{
doc.AppendTraceLog = true; // Enable trace log
doc.Params["model"] = data;
using (var stream = new FileStream("output.pdf", FileMode.Create))
{
doc.SaveAsPDF(stream);
}
}The trace log includes:
- Parse time
- Data binding time
- Layout calculation time
- Rendering time
- Component breakdown
- Performance metrics
Use Cases:
- Debug template processing issues
- Identify performance bottlenecks
- Verify component initialization
- Understand PDF generation pipeline
Important: Remove appendlog='true' from production templates as it increases file size and exposes internal processing details.
Scryber documentation is organized into two main sections:
Contains articles, guides, and learning materials for understanding Scryber concepts and features.
Comprehensive reference documentation organized by feature area:
1. /reference/htmlelements/ - HTML Elements Reference
Documentation for supported HTML elements (div, p, table, etc.)
2. /reference/htmlattributes/ - HTML Attributes Reference
Documentation for HTML attributes (class, style, id, data-*, etc.)
3. /reference/cssselectors/ - CSS Selectors Reference
Documentation for supported CSS selectors and specificity rules
4. /reference/cssproperties/ - CSS Properties Reference
Documentation for supported CSS properties (margin, padding, color, font-family, etc.)
5. /reference/svgelements/ - SVG Elements Reference
Documentation for SVG elements (svg, path, rect, circle, etc.)
6. /reference/svgattributes/ - SVG Attributes Reference
Documentation for SVG-specific attributes (viewBox, fill, stroke, etc.)
7. /reference/binding/ - Data Binding Reference (129 files - newly created)
Complete reference for the template expression system:
-
helpers/(6 files) - Handlebars helper documentation- each.md, with.md, if.md, else.md, elseif.md, log.md
- Shows underlying XML compilation for each helper
-
operators/(15 files) - Operator documentation with precedence- Arithmetic: addition.md, subtraction.md, multiplication.md, division.md, modulus.md, power.md
- Comparison: equality.md, inequality.md, lessthan.md, lessorequal.md, greaterthan.md, greaterorequal.md
- Logical: and.md, or.md, nullcoalesce.md
-
functions/(108 files) - Expression function documentation by category:- Conversion (7), String (18), Mathematical (21)
- Date/Time: Add (6), Between (4), Extract (9)
- Logical (3), Collection (13), Statistical (5), CSS (2)
Documentation Features:
- Each reference file includes signature, parameters, return type, multiple examples with data/output
- Helper files show underlying XML compilation (e.g.,
{{#each}}compiles to<template>) - All examples use fixed dates (never DateTime.Now) for PDF static document compatibility
- Cross-references between related items
- Jekyll front matter for website navigation
Template Files (in /reference/binding/ for consistency):
helper_template.md- Template for new Handlebars helpersoperator_template.md- Template for new operatorsfunction_template.md- Template for new expression functions
- ARCHITECTURE.md - Deep dive into internal architecture, pipeline stages, and component system
- Scryber Documentation - Official documentation with examples
- GitHub Samples - Working examples and templates
- Learning Documentation - Tutorials and conceptual articles
- Reference Documentation - Complete technical reference:
- HTML Elements - Supported HTML elements
- HTML Attributes - HTML attribute reference
- CSS Selectors - CSS selector support and specificity
- CSS Properties - Supported CSS properties
- SVG Elements - SVG element reference
- SVG Attributes - SVG attribute reference
- Data Binding - Helpers, operators, and functions (129 files)
The ARCHITECTURE.md file provides comprehensive details about:
- Complete PDF generation pipeline (Parse → Init → Load → DataBind → Style → Layout → Render)
- Component model and lifecycle
- CSS parser architecture
- Expression engine internals
- Layout engine details
- Extension points for custom components
When creating sample projects that demonstrate Scryber capabilities:
Use a consistent directory structure for sample projects:
sample-project/
├── templates/ # HTML templates
├── styles/ # CSS stylesheets
├── data/ # Sample JSON data files
├── images/ # Images and logos (SVG recommended)
├── output/ # Generated PDFs (git-ignored)
├── Program.cs # CLI executable
├── ProjectName.csproj # Project file
└── README.md # Documentation
Sample projects should:
- Multi-target frameworks for broad compatibility:
net6.0;net8.0;net9.0 - Copy resource files to output directory automatically
- Reference NuGet packages (not project references) to simulate real-world usage
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0;net8.0;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Use NuGet package, not project reference -->
<PackageReference Include="Scryber.Core" Version="9.1.1-rc.4" />
<PackageReference Include="Scryber.Core.OpenType" Version="5.0.3" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
<!-- Copy resource files to output directory -->
<ItemGroup>
<None Update="templates/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="styles/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="data/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="images/**/*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>Important Notes:
- Scryber.Core.OpenType dependency: When using Scryber.Core from NuGet, you must also add
Scryber.Core.OpenTypepackage reference explicitly. This is required for font rendering. - RC version format: Use correct version format for pre-release packages:
9.1.1-rc.4(with hyphen and dot), not9.1.1.0-rc4 - System.Text.Json version: Use latest stable version to avoid security vulnerabilities in older versions
Sample CLI tools should follow this pattern:
using System;
using System.IO;
using System.Text.Json;
using Scryber.Components;
namespace Scryber.Samples.ProjectName
{
class Program
{
static void Main(string[] args)
{
try
{
// Parse arguments
string dataFile = args.Length > 0 ? args[0] : "data/default.json";
string outputFile = args.Length > 1 ? args[1] : "output/report.pdf";
string templateFile = "templates/template.html";
// Create output directory if needed
string? outputDir = System.IO.Path.GetDirectoryName(outputFile);
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
// Validate input files exist
if (!File.Exists(dataFile))
{
Console.WriteLine($"ERROR: Data file not found: {dataFile}");
return;
}
// Load JSON data
Console.WriteLine($"Loading data from: {dataFile}");
string jsonContent = File.ReadAllText(dataFile);
var model = JsonSerializer.Deserialize<object>(jsonContent);
// Parse template and generate PDF
Console.WriteLine($"Parsing template: {templateFile}");
using (var doc = Document.ParseDocument(templateFile))
{
doc.Params["model"] = model;
Console.WriteLine($"Generating PDF: {outputFile}");
using (var stream = new FileStream(outputFile, FileMode.Create))
{
doc.SaveAsPDF(stream);
}
}
Console.WriteLine("✓ PDF generated successfully");
}
catch (Exception ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
Environment.Exit(1);
}
}
}
}Important: Use System.IO.Path fully qualified to avoid ambiguity with Scryber.Components.Path class.
When running a project that targets multiple frameworks, specify the framework explicitly:
# Specify framework explicitly
dotnet run --framework net9.0
# Or build for specific framework
dotnet build --framework net8.0
dotnet run --framework net8.0 --no-buildSample templates should showcase Scryber capabilities:
1. Use CSS Variables for Theming:
:root {
/* Theme: Corporate (Blue) */
--color-primary: #2563EB;
--color-success: #22C55E;
--color-warning: #EAB308;
/* Typography */
--font-family: 'Helvetica', 'Arial', sans-serif;
--font-size-base: 10pt;
}2. Demonstrate Expression Capabilities:
<!-- Mathematical calculations -->
<span>{{model.budget.spent / model.budget.total * 100}}%</span>
<!-- Date formatting (convert JSON string to DateTime first) -->
<span>{{format(date(model.reportDate), 'MMMM dd, yyyy')}}</span>
<!-- Collection operations -->
<span>Average: {{averageOf(model.items, .value)}}</span>
<span>Total: {{count(model.items)}} items</span>Important: When working with JSON data, date properties are strings that must be converted using date() before formatting: {{format(date(model.dateProperty), 'format')}}
3. Use Template Variables for Reusable Calculations:
<!-- Calculate once, use multiple times -->
<var data-id="percentage" data-value="{{value / total * 100}}" />
<rect width="{{percentage * 5}}" height="40" />
<text>{{format(percentage, 'N1')}}%</text>3b. Complex Variable Chaining for Financial Reports:
For reports with interdependent calculations (like financial statements), define all variables at the document top and reuse them throughout:
<body>
<!-- ============================================
CALCULATE ALL METRICS AT THE TOP
============================================ -->
<!-- Balance Sheet Calculations -->
<var data-id="totalCurrentAssets" data-value="{{sumOf(model.balanceSheet.currentAssets, .current)}}" />
<var data-id="totalNonCurrentAssets" data-value="{{sumOf(model.balanceSheet.nonCurrentAssets, .current)}}" />
<var data-id="totalAssets" data-value="{{totalCurrentAssets + totalNonCurrentAssets}}" />
<var data-id="totalLiabilities" data-value="{{sumOf(model.balanceSheet.liabilities, .current)}}" />
<var data-id="totalEquity" data-value="{{sumOf(model.balanceSheet.equity, .current)}}" />
<!-- Income Statement Calculations (using Balance Sheet variables) -->
<var data-id="totalRevenue" data-value="{{sumOf(model.incomeStatement.revenue, .current)}}" />
<var data-id="totalCostOfRevenue" data-value="{{sumOf(model.incomeStatement.costs, .current)}}" />
<var data-id="grossProfit" data-value="{{totalRevenue - totalCostOfRevenue}}" />
<var data-id="netIncome" data-value="{{grossProfit - sumOf(model.incomeStatement.expenses, .current)}}" />
<!-- Financial Ratios (using above variables) -->
<var data-id="grossMargin" data-value="{{(grossProfit / totalRevenue) * 100}}" />
<var data-id="currentRatio" data-value="{{totalCurrentAssets / totalLiabilities}}" />
<var data-id="debtToEquity" data-value="{{totalLiabilities / totalEquity}}" />
<var data-id="returnOnEquity" data-value="{{(netIncome / totalEquity) * 100}}" />
<!-- ============================================
USE VARIABLES THROUGHOUT THE DOCUMENT
============================================ -->
<!-- Cover Page - Key Metrics -->
<div class="cover-page">
<h2>Key Financial Ratios</h2>
<div class="metric">Gross Margin: {{format(grossMargin, 'N1')}}%</div>
<div class="metric">ROE: {{format(returnOnEquity, 'N1')}}%</div>
<div class="metric">Current Ratio: {{format(currentRatio, 'N2')}}</div>
</div>
<!-- Page 2 - Balance Sheet -->
<div class="section">
<h2>Balance Sheet</h2>
<p>Total Assets: ${{format(totalAssets / 1000, 'N0')}}K</p>
<p>Total Liabilities: ${{format(totalLiabilities / 1000, 'N0')}}K</p>
<p>Total Equity: ${{format(totalEquity / 1000, 'N0')}}K</p>
</div>
<!-- Page 3 - Income Statement -->
<div class="section">
<h2>Income Statement</h2>
<p>Revenue: ${{format(totalRevenue / 1000, 'N0')}}K</p>
<p>Gross Profit: ${{format(grossProfit / 1000, 'N0')}}K</p>
<p>Net Income: ${{format(netIncome / 1000, 'N0')}}K</p>
</div>
</body>Benefits of top-level variable calculation:
- Calculate once, use everywhere (performance)
- Easy to reorganize sections without recalculating
- Clear dependency chain (variables reference earlier variables)
- Simpler templates (no duplicate calculation logic)
- Perfect for financial reports with interdependent metrics
4. Show Conditional Rendering:
{{#each model.items}}
{{#if this.value >= 100}}
<div class="high-value">{{this.name}}</div>
{{else if this.value >= 50}}
<div class="medium-value">{{this.name}}</div>
{{else}}
<div class="low-value">{{this.name}}</div>
{{/if}}
{{/each}}5. Include SVG Charts with Dynamic Data:
<svg width="600" height="200" viewBox="0 0 600 200">
{{#each model.dataPoints}}
<!-- Variables update each iteration - no @index suffix needed -->
<var data-id="barX" data-value="{{@index * 50}}" />
<var data-id="barHeight" data-value="{{this.value / model.maxValue * 180}}" />
<rect x="{{barX}}"
y="{{200 - barHeight}}"
width="40"
height="{{barHeight}}"
fill="#2563EB" />
{{/each}}
</svg>Note: Variables inside loops update automatically - don't use data-id="var_{{@index}}" patterns. Root parameters like model are always accessible directly without ../ prefix.
5b. Fitting Multiple Charts on Same Page:
To fit multiple SVG charts on a single page, reduce chart heights and adjust spacing:
<!-- First chart with reduced height -->
<div class="section">
<h2 class="section-title">Quarterly Revenue</h2>
<div class="chart-container">
<svg width="100%" height="180" viewBox="0 0 600 180">
<var data-id="maxRevenue" data-value="{{maxOf(model.quarterly, .revenue)}}" />
<!-- Grid adjusted to smaller viewBox -->
<line x1="60" y1="20" x2="60" y2="140" class="chart-axis" />
<line x1="60" y1="140" x2="580" y2="140" class="chart-axis" />
{{#each model.quarterly}}
<var data-id="barX" data-value="{{100 + (@index * 120)}}" />
<var data-id="barHeight" data-value="{{(this.revenue / maxRevenue) * 110}}" />
<var data-id="barY" data-value="{{140 - barHeight}}" />
<rect x="{{barX}}" y="{{barY}}" width="80" height="{{barHeight}}" />
{{/each}}
</svg>
</div>
<!-- Second chart on same page with subtitle -->
<h2 class="section-subtitle">Revenue by Region</h2>
<div class="chart-container">
<svg width="100%" height="180" viewBox="0 0 600 180">
<!-- Similar structure with reduced dimensions -->
</svg>
</div>
</div>Reduce chart container spacing in CSS:
.chart-container {
margin: var(--spacing-xs) 0; /* Reduced from --spacing-md */
padding: var(--spacing-sm); /* Reduced from --spacing-md */
background-color: var(--color-background-alt);
border: 1pt solid var(--color-border);
}Tips for multiple charts per page:
- Reduce
heightandviewBoxheight proportionally (e.g., 250→180, 280→180) - Adjust all Y-coordinates proportionally (e.g., if height reduced by 30%, reduce Y coords by 30%)
- Use
section-subtitlefor second chart instead ofsection-title - Remove
page-breakbetween charts - Reduce container margins and padding
- Test with
page-break-inside: avoidon chart containers to prevent splitting
XML Entity Escaping: When using HTML entities in templates that will be parsed as XML, remember to escape special characters:
❌ Wrong:
<h2>Risks & Issues</h2> <!-- Ampersand will cause parse error -->
<h2>Liabilities & Equity</h2> <!-- Will fail XML parsing -->✅ Correct:
<h2>Risks & Issues</h2> <!-- Properly escaped -->
<h2>Liabilities & Equity</h2> <!-- Properly escaped -->XML Entity Escaping in Expressions: Operators like < and > must also be escaped when used in HTML attributes:
❌ Wrong:
<span class="{{if(value < 0, 'negative', 'positive')}}"> <!-- < will cause parse error -->✅ Correct:
<span class="{{if(value < 0, 'negative', 'positive')}}"> <!-- < properly escaped -->
<span class="{{if(value > 0, 'positive', 'negative')}}"> <!-- > should be > -->Common XML entities to escape:
&→&<→<>→>"→"(in attribute values)'→'(in attribute values)
SVG Attribute Best Practices: SVG text elements support both numeric and keyword font-weight values:
✅ Numeric values (most precise):
<text x="50" y="90" font-weight="700">Label</text>
<!-- font-weight values: 100, 200, 300, 400 (normal), 500, 600, 700 (bold), 800, 900 -->✅ Keyword values (also supported):
<text x="50" y="90" font-weight="bold">Label</text>
<text x="50" y="90" font-weight="normal">Label</text>
<text x="50" y="90" font-weight="light">Label</text>
<text x="50" y="90" font-weight="bolder">Label</text>
<text x="50" y="90" font-weight="lighter">Label</text>✅ CSS classes (for complex styling):
<!-- In CSS -->
.chart-label-bold {
font-weight: bold;
font-size: 14pt;
}
<!-- In SVG -->
<text x="50" y="90" class="chart-label-bold">Label</text>Keep sample data simple and focused:
{
"reportDate": "2024-03-15T00:00:00",
"title": "Sample Report",
"summary": {
"total": 150000,
"spent": 75000
},
"items": [
{
"name": "Item 1",
"value": 100,
"status": "Active"
}
]
}Guidelines:
- Use ISO 8601 date format with time component:
"2024-03-15T00:00:00" - Use simple property names that are self-documenting
- Include variety of data types (strings, numbers, booleans, dates, arrays, nested objects)
- Keep sample data realistic but concise
Each sample should include a comprehensive README.md covering:
- What the sample demonstrates - List Scryber features showcased
- File structure - Explain organization
- Building and running - Commands to build and execute
- Customization - How to modify colors, data, layout
- JSON data structure - Document expected data format
- Troubleshooting - Common issues and solutions
- Scryber expression examples - Highlight interesting techniques used
Path Resolution Issues:
// ❌ Wrong - ambiguous reference
using Scryber.Components; // Contains Path class
var outputDir = Path.GetDirectoryName(file); // Ambiguous!
// ✅ Correct - fully qualified
var outputDir = System.IO.Path.GetDirectoryName(file);Missing Output Directory:
// ✅ Always ensure output directory exists
string? outputDir = System.IO.Path.GetDirectoryName(outputFile);
if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}Framework Selection:
# ❌ Wrong - ambiguous which framework to use
dotnet run
# ✅ Correct - explicit framework
dotnet run --framework net9.0Float: Right Element Ordering:
<!-- ❌ Wrong - float: right wraps to next line -->
<div class="header">
<span class="title">My Title</span>
<span class="date" style="float: right;">2024-03-15</span>
</div>
<!-- ✅ Correct - float: right appears first -->
<div class="header">
<span class="date" style="float: right;">2024-03-15</span>
<span class="title">My Title</span>
</div>Important: In Scryber, float: right elements must appear BEFORE non-floating inline content in HTML source order to prevent wrapping to a new line.
Float Width Issues:
<!-- ❌ Wrong - unconstrained width can cause overflow -->
<div class="title-section" style="float: right;">
<h1>Very Long Project Status Report Title</h1>
</div>
<!-- ✅ Correct - explicit width prevents overflow -->
<div class="title-section" style="float: right; width: 300pt;">
<h1>Very Long Project Status Report Title</h1>
</div>Important: Floating elements should have explicit width to prevent their inner content from using full width and causing page overflow.
Margin Collapsing:
/* ❌ Problem - margins don't collapse (40pt total space) */
.section {
margin-bottom: 20pt;
}
.section + .section {
margin-top: 20pt; /* Adds to margin-bottom, not collapsed */
}
/* ✅ Solution - use margin on one side only */
.section {
margin-bottom: 12pt; /* Reduced margin, one side only */
}Important: Unlike browsers, Scryber does NOT collapse adjacent top/bottom margins between siblings. Plan spacing with smaller margins or use margin on only one side.
# Build entire solution
dotnet build Scryber.Core.sln
# Build specific configuration
dotnet build Scryber.Core.sln -c Release
dotnet build Scryber.Core.sln -c Debug# Run all tests
dotnet test Scryber.Core.sln
# Run specific test project
dotnet test Scryber.UnitTest/Scryber.UnitTests.csproj
dotnet test Scryber.UnitSamples/Scryber.UnitSamples.csproj
dotnet test Scryber.UnitLayouts/Scryber.UnitLayouts.csproj
# Run a single test
dotnet test --filter "FullyQualifiedName~TestMethodName"# Pack the main library
dotnet pack Scryber.Components/Scryber.Components.csproj -c Release
# Pack MVC extension
dotnet pack Scryber.Components.Mvc/Scryber.Components.Mvc.csproj -c ReleaseScryber follows a multi-stage pipeline architecture with clear separation of concerns across specialized projects.
Dependency Flow: Common → Drawing/Expressive → Styles/Generation → Imaging → Components → Components.Mvc
-
Scryber.Common - Foundation layer defining core interfaces and contracts
- Lifecycle interfaces:
IComponent,IDocument,IBindableComponent,IRemoteComponent - Low-level PDF structure handling (PDF/Native, PDF/Resources, PDF/Parsing)
- HTML entity definitions, configuration, caching, and logging abstractions
- Lifecycle interfaces:
-
Scryber.Drawing - Graphics primitives and typography
- Font system with embedded standard fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats)
- TrueType/OpenType support via Scryber.Core.OpenType package
- Drawing primitives (colors, units, points, rectangles, pen, brush)
- SVG path and element rendering
-
Scryber.Expressive - Expression engine for template expressions
- Parses and evaluates handlebars syntax
{{...}} - Expression tree: variables, properties, functions, operators, indexers
- Built-in functions: concat, if, index, and more
- Used throughout templates and CSS for dynamic content
- Parses and evaluates handlebars syntax
-
Scryber.Styles - CSS parsing and style management
- CSS parser using individual typed parsers for each property (CSS*Parser classes)
- Selector matching and specificity calculation
- Cascading and inheritance rules
- Supports CSS variables
var(--name)andcalc()expressions
-
Scryber.Generation - Document parsing and data binding
- Binding expression infrastructure connecting to Expressive engine
- Parser definitions for XML/HTML attributes and templates
- XPath-like data path navigation
-
Scryber.Imaging - Image loading and processing
- Format-specific factories for JPEG, PNG, GIF, TIFF
- Data URL support for embedded base64 images
- Uses SixLabors.ImageSharp for image processing
- Optimized image data conversion for PDF inclusion
-
Scryber.Components - Main PDF generation engine
- Orchestrates all subsystems to produce PDF output
- 80+ HTML element implementations
- Full layout engine with box model, flow, positioning, tables, lists
- Complete PDF generation pipeline (see below)
-
Scryber.Components.Mvc - ASP.NET MVC integration
PDFViewResultActionResult for PDF responses- Extension methods for controllers:
PDFAsync()
Documents flow through these discrete stages:
Parse → Init → Load → DataBind → Style Resolution → Layout → Render
1. Parsing (Document.ParseDocument() or Document.ParseHtmlDocument())
- XML parser for strict XHTML (System.Xml)
- HTML parser for loose HTML (HtmlAgilityPack)
- Creates component tree from markup
2. Initialization (Init())
- Registers components with document
- Sets up resource containers
- Resolves font references
3. Loading (Load())
- Loads external resources (images, CSS, fonts)
- Async loading for WASM compatibility
- Processes remote references
4. Data Binding (DataBind())
- Evaluates
{{...}}expressions via Expressive engine - Populates templates with data
- Supports complex object models
5. Style Resolution
- Merges CSS rules from multiple sources
- Applies selector matching and specificity
- Resolves computed styles with cascading
6. Layout (RenderToPDF() → Layout phase)
- Layout engines:
LayoutEngineDocument,LayoutEnginePage,LayoutEnginePanel,LayoutEngineTable,LayoutEngineList,LayoutEngineText - Measures all components
- Calculates positions and sizes
- Handles page breaks and flowing content
- Text line breaking with hyphenation support
- Creates
PDFLayoutDocumentwithPDFLayoutPageobjects
7. Rendering (OutputToPDF())
- Generates PDF structure via
PDFWriter - Writes pages, resources (fonts, images), catalog
- Applies compression and security
All In One ( 'SaveAsPDF()')
- Does stages 2 to 7 in one go.
- All elements implement
IComponentwith lifecycle methods:Init(),Load(),DataBind(),Dispose() - Components form tree hierarchy with
Documentat root - Parent/child relationships enable path resolution and resource sharing
HTMLParserComponentFactory: Maps HTML tags to component instancesImageFactoryList: Creates image handlers based on formatFontFactory: Creates font instances
- Context objects passed through pipeline stages:
InitContext,LoadContext,DataContext,LayoutContext,RenderContext - Keeps component state immutable while threading operation context
ISharedResourceinterface for fonts and images- Resources cached at document level, referenced multiple times
- Reduces PDF file size through sharing
Handlebars syntax {{expression}} works in:
- HTML attributes:
<div style="{{model.style}}"> - CSS properties:
color: {{model.color}};, can also use thecalc(...)syntax rather than handlebars. - Text content:
<span>{{model.name}}</span>
Expression types:
- Variables:
{{model.title}} - Property paths:
{{model.user.name}} - Array indexing:
{{model.items[0]}} - Functions:
{{concat(model.first, ' ', model.last)}} - Math:
{{model.price * 1.1}} - Conditionals:
{{model.age > 18 ? 'Adult' : 'Minor'}}
Handlebars helpers are parsed and transformed into Scryber XML components:
Helper Mapping System (Scryber.Generation/Generation/Handlebars/):
HBarHelperMapping.cs- Maps helper names to handler classes- Each helper has dedicated handler:
HBarEach,HBarWith,HBarIf,HBarElse,HBarElseIf,HBarLog - Helpers compile to XML during parsing phase before component tree creation
- Transformation happens via
DocumentHBarExpression.ProcessHandleBars()
Compilation Examples:
Becomes:
<template data-bind="{{items}}">...</template>Becomes:
<choose>
<when test="{{condition}}">...</when>
<when test="{{other}}">...</when>
<otherwise>...</otherwise>
</choose>Special Variables:
@index,@first,@lastin{{#each}}are handled by ForEach component's internal iteration context- Context navigation (
this,../) managed by binding context stack during data binding phase
Correct Pattern - Simple and clean:
using (var doc = Document.ParseDocument("template.html"))
{
doc.Params["model"] = dataModel;
using (var stream = new FileStream("output.pdf", FileMode.Create))
{
doc.SaveAsPDF(stream); // This handles everything!
}
}Important: Do NOT call doc.ProcessDocument() - it doesn't exist and isn't needed. SaveAsPDF() handles all processing stages automatically (Init, Load, DataBind, Style Resolution, Layout, Render).
Scryber handles JSON data binding automatically - no complex conversion needed:
// Read and deserialize JSON
string jsonContent = File.ReadAllText("data.json");
var model = JsonSerializer.Deserialize<object>(jsonContent);
// Pass directly to template - Scryber handles all binding
using (var doc = Document.ParseDocument("template.html"))
{
doc.Params["model"] = model;
// ... generate PDF
}What Scryber handles automatically:
- Object property access:
{{model.propertyName}} - Nested object navigation:
{{model.user.address.city}} - Array iteration:
{{#each model.items}} - Type conversions (strings, numbers, booleans, dates)
- Conditional logic:
{{#if model.condition}}
Use standard mathematical notation in templates:
Correct:
<var data-id="total" data-value="{{price * quantity}}" />
<div style="width: {{baseWidth + 10}}pt;">
<rect height="{{value / maxValue * 200}}" />Incorrect: - Don't use {{calc(price, '*', quantity)}}calc() function
Supported operators: +, -, *, /, %
Variables stored with <var> are accessed directly by name:
<!-- Define variable -->
<var data-id="barHeight" data-value="{{revenue / maxValue * 200}}" />
<!-- Use variable directly by name -->
<rect height="{{barHeight}}" />
<text y="{{240 - barHeight}}">{{format(revenue, 'C0')}}</text>
<!-- Conditional with variable -->
{{#if barHeight > 30}}
<text>Show label</text>
{{/if}}Important: Access variables by name only, NOT Document.Params.varName or params.varName.
Variables Inside Loops: Variables defined inside {{#each}} loops update automatically with each iteration:
{{#each model.dataPoints}}
<var data-id="barX" data-value="{{@index * 50}}" />
<var data-id="barHeight" data-value="{{this.value / model.maxValue * 180}}" />
<!-- Variables update each iteration - no unique names needed -->
<rect x="{{barX}}" y="{{200 - barHeight}}" width="40" height="{{barHeight}}" />
{{/each}}❌ Don't try to create unique variable names with @index - nested binding doesn't work:
<!-- This won't work -->
<var data-id="bar_{{@index}}" data-value="{{calculation}}" />
<rect x="{{bar_{{@index}}}}" />Templates can link to external CSS files for clean separation:
<head>
<link rel="stylesheet" href="styles.css" type="text/css" />
</head>Benefits:
- Separates structure (HTML) from styling (CSS)
- Easier for designers to maintain styles
- Reusable across multiple templates
- Cleaner template code
For complex reports, use three-file separation:
-
Template (HTML): Structure and layout logic
<html> <head> <link rel="stylesheet" href="styles.css" /> </head> <body> {{#each model.items}} <div class="item">{{this.name}}</div> {{/each}} </body> </html>
-
Styles (CSS): Visual design and branding
@page { size: A4; margin: 20mm; } body { font-family: Arial; } .item { padding: 10pt; }
-
Data (JSON): Content and information
{ "items": [ { "name": "Item 1" }, { "name": "Item 2" } ] }
This separation enables:
- Content editors work on JSON without touching code
- Designers work on CSS independently
- Developers focus on template logic
- Easy localization by swapping JSON files
Standard CSS box model: margin → border → padding → content
- Flow Layout: Block and inline (default HTML behavior)
- Positioned Layout: Relative and absolute positioning
- Table Layout: Full table support with colspan/rowspan
- List Layout: Numbered and bulleted lists with CSS counters
- Line breaking with hyphenation (
hyphensCSS property) - White space handling:
normal,nowrap,pre - Text overflow and clipping
- Multi-font text runs (inline style changes)
- Standard Fonts: PDF standard fonts embedded as resources
- TrueType/OpenType: Full support via
Scryber.Core.OpenTypepackage - Google Fonts: Can load from external URLs
- Font Fallback: Chain of fallbacks when exact font not found
- Note: Font subsetting not implemented (embeds full fonts)
Important: Standard PDF fonts (Helvetica, Times, Courier) have very limited Unicode support. Special characters, symbols, and emoji will not render correctly.
❌ Characters that won't render with standard fonts:
- Unicode symbols: ✓ ✔ ✗ ✘ ○ ● ◯ ◉ ★ ☆
- Emoji: 😀 👍
⚠️ ✨ - Special punctuation: " " ' ' — – …
- Math symbols: ≤ ≥ ≠ ∞ ∑ ∏
- HTML entities:
✓(✓),○(○)
These will appear as:
- Question marks (??)
- Empty boxes (□)
- Missing/blank characters
✅ Reliable alternatives using ASCII:
<!-- Instead of ✓ and ○ -->
<td>{{if(this.completed, '[X]', '[ ]')}}</td>
<!-- Instead of ★ ratings -->
<td>{{if(this.rating >= 4, '****', '***')}}</td>
<!-- Instead of special quotes " " -->
<td>"Standard quotes work fine"</td>
<!-- Instead of — (em dash) -->
<td>Text - with - dashes</td>✅ Best Solution - Font Awesome Icons (Recommended):
Font Awesome v5 is fully supported and provides reliable icon rendering:
<head>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" type="text/css" />
</head>
<body>
<!-- Solid icons (fas) -->
<i class="fas fa-check-circle" style="color: #22C55E;"></i>
<i class="fas fa-times-circle" style="color: #EF4444;"></i>
<i class="fas fa-star" style="color: #EAB308;"></i>
<!-- Regular icons (far) -->
<i class="far fa-circle"></i>
<i class="far fa-square"></i>
<!-- In conditional expressions -->
{{#if this.completed}}
<i class="fas fa-check-circle" style="color: #22C55E;"></i>
{{else}}
<i class="far fa-circle" style="color: #6B7280;"></i>
{{/if}}
</body>Why Font Awesome works:
- Icon font specifically designed for symbols
- Properly embeds in PDFs
- Wide variety of icons (check marks, circles, stars, arrows, etc.)
- Colored with inline styles
- v5.15.4 is fully tested and working
Common Font Awesome icons for reports:
fas fa-check-circle- Completed/success checkmarkfas fa-times-circle- Failed/error Xfas fa-exclamation-triangle- Warningfar fa-circle- Empty/pending circlefas fa-star/far fa-star- Ratingsfas fa-arrow-up/fas fa-arrow-down- Trends
Font Awesome with CSS ::before Pseudo-elements:
Font Awesome icons can also be rendered using CSS ::before pseudo-elements with Unicode escape codes. This is particularly useful for automatically adding icons to elements without modifying HTML:
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<style>
/* Positive indicator with up arrow */
.percentage-positive::before {
content: "\f062"; /* fa-arrow-up Unicode */
font-family: "Font Awesome 6 Free";
font-weight: 900;
margin-right: 4pt;
color: #059669;
}
/* Negative indicator with down arrow */
.percentage-negative::before {
content: "\f063"; /* fa-arrow-down Unicode */
font-family: "Font Awesome 6 Free";
font-weight: 900;
margin-right: 4pt;
color: #DC2626;
}
/* Status indicators */
.status-complete::before {
content: "\f058"; /* fa-check-circle */
font-family: "Font Awesome 6 Free";
font-weight: 900;
margin-right: 4pt;
color: #22C55E;
}
</style>
</head>
<body>
<!-- Icons automatically added via CSS -->
<span class="percentage-positive">15.2%</span>
<span class="percentage-negative">-3.4%</span>
<span class="status-complete">Task completed</span>
</body>Finding Font Awesome Unicode values:
- Visit: https://fontawesome.com/icons
- Search for icon (e.g., "arrow-up")
- Click icon → Copy Unicode value (e.g.,
f062) - Use in CSS as
content: "\f062";
Common Font Awesome Unicode values:
\f062- fa-arrow-up\f063- fa-arrow-down\f058- fa-check-circle\f057- fa-times-circle\f06a- fa-exclamation-circle\f071- fa-exclamation-triangle\f005- fa-star (solid)\f006- fa-star (outline)\f0c8- fa-square (unchecked)\f14a- fa-check-square
Benefits of CSS ::before approach:
- No HTML changes needed for icon placement
- Consistent styling across all matching elements
- Easy to maintain and update icon styles
- Reduces template clutter
- Works automatically with conditional CSS classes
✅ Alternative - Custom Unicode Fonts (If Font Awesome doesn't fit):
Load a custom font with full Unicode coverage:
<head>
<style>
@font-face {
font-family: 'NotoSans';
src: url('path/to/NotoSans-Regular.ttf');
}
body {
font-family: 'NotoSans', sans-serif;
}
</style>
</head>Recommended Unicode fonts:
- Noto Sans - Google's font supporting all Unicode characters
- Arial Unicode MS - Microsoft font with broad Unicode support
- DejaVu Sans - Open source with extensive Unicode coverage
Note: Custom fonts may not render all Unicode characters correctly in PDFs. Font Awesome is more reliable.
- Formats: JPEG, PNG, GIF, TIFF
- Data URLs:
src="data:image/..."for embedded images - Remote Loading: Async HTTP requests
- Color Formats: RGB24, RGBA32, ARGB32, BGR24
- Optimization: JPEG pass-through (no re-encoding)
All code must be WASM-compatible:
- All remote resource loading is asynchronous
- No blocking I/O operations
DocumentTimerExecutionallows yielding during generation- Use
SaveAsPDFTimer()for async PDF generation in WASM
When adding new functionality:
- Custom Components: Implement
IComponentor extend existing base classes - Custom HTML Elements: Add to
HTMLParserComponentFactory.DefaultTagsdictionary - Custom CSS Properties: Create new
CSSStyleAttributeParser<T>subclass inScryber.Styles/Styles/Parsing/Typed/ - Custom Expression Functions:
- Create function in
Scryber.Generation/Binding/Functions/directory - Organize by category (e.g.,
String/,Math/,DateTime/, etc.) - Register in
BindingCalcExpressionFactory.cs - Add documentation file in
docs/reference/binding/functions/
- Create function in
- Custom Handlebars Helpers:
- Create handler class implementing helper interface in
Scryber.Generation/Generation/Handlebars/ - Pattern:
HBarYourHelper.csextending appropriate base class - Register in
HBarHelperMapping.csdictionary - Define XML output format (what component structure it compiles to)
- Add documentation file in
docs/reference/binding/helpers/
- Create handler class implementing helper interface in
- Custom Binding Operators:
- Add to
Scryber.Expressiveexpression parser - Define precedence level (lower = higher priority)
- Add documentation file in
docs/reference/binding/operators/
- Add to
- Custom Layout Engines: Implement
IPDFLayoutEngineinterface - Custom Image Formats: Extend
ImageFactoryBaseand register inImageFactoryList
Scryber.Components/Document.cs- Main public API for document creation and parsingScryber.Components/Html/Parsing/HTMLParser.cs- HTML parsing entry pointScryber.Styles/Styles/Parsing/CSSStyleParser.cs- CSS parsing entry point
Scryber.Components/PDF/Layout/PDFLayoutDocument.cs- Layout state managementScryber.Components/PDF/Layout/LayoutEngine*.cs- Layout engine implementationsScryber.Components/PDF/Layout/PDFLayout*.cs- Layout item hierarchy
Scryber.Common/PDF/Native/PDFWriter*.cs- Low-level PDF structure writingScryber.Components/PDF/Native/PDFWriter*.cs- High-level PDF writing
Scryber.Expressive/ExpressionParser.cs- Expression tokenization and parsingScryber.Generation/Binding/BindingCalcParser.cs- Template binding integrationScryber.Generation/Binding/BindingCalcExpressionFactory.cs- Function registration (90+ functions)Scryber.Generation/Binding/Functions/- All expression function implementations organized by category:String/- String manipulation functionsMath/- Mathematical functionsDateTime/- Date and time functionsCollection/- Array/collection operationsLogical/- Conditional and logical functionsStatistical/- Statistical analysis functionsConversion/- Type conversion functionsCSS/- CSS helper functions
Scryber.Generation/Generation/Handlebars/HBarHelperMapping.cs- Helper name to handler mappingScryber.Generation/Generation/Handlebars/HBarEach.cs-{{#each}}iteration helperScryber.Generation/Generation/Handlebars/HBarWith.cs-{{#with}}context switchingScryber.Generation/Generation/Handlebars/HBarIf.cs-{{#if}}conditionalScryber.Generation/Generation/Handlebars/HBarElse.cs-{{else}}fallbackScryber.Generation/Generation/Handlebars/HBarElseIf.cs-{{else if}}alternative conditionScryber.Generation/Generation/Handlebars/HBarLog.cs-{{log}}debugging helperScryber.Generation/Generation/Handlebars/DocumentHBarExpression.cs- Handlebars processing orchestration
Projects target multiple frameworks: net6.0;net8.0;net9.0;netstandard2.0
When working with framework-specific code:
- Use
#if NET6_0_OR_GREATERpreprocessor directives - Ensure WASM compatibility (no platform-specific APIs)
- Test across all target frameworks when possible
Current version: 9.1.0.7-beta (as of last update)
- Version defined in
Scryber.Components/Scryber.Components.csproj - Also in
Directory.Build.propsfor assembly versioning