Papercraft Manual

User-facing documentation for Papercraft XML templates and the X39.Solutions.PdfTemplate compatibility bridge.

Developer Integration Appendix

Previous: Troubleshooting Manual home Next: Renderer backends

What Is This?

The developer integration appendix is the place for application setup and extension details. It keeps service registration, custom controls, custom transformers, functions, resource resolvers and public interfaces out of the template-author chapters.

When Should I Use This?

Use this appendix when a template change requires application code, such as adding a new function, supplying new data, loading images from a custom location, registering a custom control or selecting a render target. Use renderer backends for output-format choices such as SkiaSharp PDF/PNG, SVG, PDFsharp or ESC/POS.

How Do I Start?

Start with the Papercraft facade package and the default renderer: X39.Solutions.Papercraft, AddPapercraft(), PapercraftRenderer, PapercraftRenderOptions, DocumentOptions, IFunction, ITransformer, IControl, ITemplateData, IResourceResolver, IDrawableCanvas, IDeferredCanvas, IImmediateCanvas, IPropertyAccessCache, ITextService and IParameterConverter<T>.

Use X39.Solutions.PdfTemplate, AddPdfTemplateService() and Generator only when maintaining existing compatibility-package consumers.

Install

The library targets .NET 10.0. Install the Papercraft facade package in the application that renders documents:

dotnet add package X39.Solutions.Papercraft

On Linux, also install the SkiaSharp native Linux assets package for the default renderer:

dotnet add package SkiaSharp.NativeAssets.Linux

Existing applications can keep the compatibility package during migration. See Compatibility Package And Legacy API.

Register Services

Register the built-in parser/runtime, controls, transformers and default SkiaSharp renderer at application startup:

using Microsoft.Extensions.DependencyInjection;
using X39.Solutions.Papercraft;

services.AddPapercraft();

AddPapercraft() registers Papercraft Core services, the built-in controls, the built-in transformers, PapercraftGenerator, PapercraftRenderer and the SkiaSharp render backend.

Use the builder overload when the application needs to add or replace template features:

services.AddPapercraft(
    (builder) => builder
        .AddFunction<MyFunction>()
        .AddControl<MyControl>()
        .AddTransformer<MyTransformer>());

Use the renderer-neutral setup only when the application supplies its own render backend:

using Microsoft.Extensions.DependencyInjection;
using X39.Solutions.Papercraft;
using X39.Solutions.Papercraft.Abstraction;

services.AddPapercraftCore();
services.AddTransient<IPapercraftRenderBackend, MyRenderBackend>();

When PapercraftRenderer selects a backend by BackendId or render target, it uses that backend’s ITextService while generating the document. Custom backends must expose a text service that matches their rendering behavior. Renderer-specific package setup and output examples are documented in Renderer backends.

Generate A PDF

Resolve a PapercraftRenderer, create an XmlReader for the template, and write the PDF to a stream:

using System.Globalization;
using System.Xml;
using Microsoft.Extensions.DependencyInjection;
using X39.Solutions.Papercraft;

await using var scope = serviceProvider.CreateAsyncScope();
var renderer = scope.ServiceProvider.GetRequiredService<PapercraftRenderer>();

using var reader = XmlReader.Create(xmlTemplateStream);
await using var output = File.Create("document.pdf");

await renderer.GeneratePdfAsync(
    output,
    reader,
    CultureInfo.CurrentUICulture);

PapercraftRenderer is registered as transient and owns per-render template data through its PapercraftGenerator. Resolve a fresh renderer when concurrent renders need isolated data.

Supply Template Data

Template authors can only use data that the application supplies. Set variables before rendering:

renderer.TemplateData.SetVariable("CustomerName", customer.Name);
renderer.TemplateData.SetVariable("InvoiceTotal", invoice.Total);

The template can then read those values:

<template>
    <body>
        <text>@CustomerName</text>
        <text>Total: @InvoiceTotal</text>
    </body>
</template>

If a template author needs a new value, the application should expose it as a variable or function instead of asking the author to hard-code business logic in XML.

Configure Document Options

Pass PapercraftRenderOptions when page setup, backend selection or per-request context must differ from the default:

using System.Globalization;
using X39.Solutions.Papercraft;
using X39.Solutions.Papercraft.Data;

await renderer.GeneratePdfAsync(
    output,
    reader,
    CultureInfo.CurrentUICulture,
    new PapercraftRenderOptions
    {
        DocumentOptions = new DocumentOptions
        {
            Margin = new Thickness(new Length(1, ELengthUnit.Centimeters)),
            Producer = "Invoice service",
            Context = new PrintRequestContext(invoice.Id),
        },
    });

Useful options include:

Option Use
DocumentOptions.PageWidthInMillimeters and DocumentOptions.PageHeightInMillimeters Change the page size. Defaults match A4 dimensions.
DocumentOptions.DotsPerInch, DocumentOptions.DotsPerCentimeter and DocumentOptions.DotsPerMillimeter Change render density.
DocumentOptions.Margin Reserve document margin before header, body and footer layout.
DocumentOptions.Producer and DocumentOptions.Modified Set PDF metadata.
DocumentOptions.Context Pass request-specific information to context-aware extension points.
DocumentOptions.IgnoreErrors Instruct the generator to ignore errors where possible. Use carefully, because XML can still become invalid.
BackendId Select a registered render backend by renderer id.
TreatDegradedAsUnsupported Treat degraded validation diagnostics as render-blocking diagnostics.

Validate Before Rendering

Call ValidateAsync when an application lets users choose output formats or renderer backends:

var result = await renderer.ValidateAsync(
    reader,
    RenderTarget.Pdf,
    CultureInfo.CurrentUICulture);

if (!result.IsSupported)
{
    // Show result.Diagnostics to the user.
}

Validation reads the XML template. Create a new XmlReader for the actual render after validation. Unsupported diagnostics block rendering. Degraded diagnostics are warnings unless PapercraftRenderOptions.TreatDegradedAsUnsupported is enabled.

Trace Renderer Activity

Papercraft emits phase-level renderer activities through PapercraftInstrumentation.ActivitySource. Applications that use OpenTelemetry can install X39.Solutions.Papercraft.OpenTelemetry and register the source during host setup:

using X39.Solutions.Papercraft.OpenTelemetry;

builder.AddPapercraftOpenTelemetry(
    (tracing) =>
    {
        // Add exporters or processors here.
    });

The tracing package only wires Papercraft into OpenTelemetry. Exporters, sampling and resource configuration remain application-owned.

Add A Function

Use a function when a template needs a reusable value or calculation such as a formatted total, lookup result or application-specific label.

using System.Globalization;
using X39.Solutions.Papercraft.Abstraction;

public sealed class CustomerLabelFunction : IFunction
{
    public string Name => "customerLabel";
    public int Arguments => 1;
    public bool IsVariadic => false;

    public ValueTask<object?> ExecuteAsync(
        CultureInfo cultureInfo,
        object?[] arguments,
        CancellationToken cancellationToken = default)
    {
        var customerId = Convert.ToString(arguments[0], cultureInfo);
        return ValueTask.FromResult<object?>($"Customer {customerId}");
    }
}

Register the function:

services.AddPapercraft(
    (builder) => builder.AddFunction<CustomerLabelFunction>());

A template author can call it after the application exposes it:

<text>@customerLabel(CustomerId)</text>

Add A Custom Control

Use a custom control when the built-in XML controls cannot render a required visual element. Most custom controls should derive from Control or an existing base control instead of implementing IControl directly.

using System.Globalization;
using X39.Solutions.Papercraft;
using X39.Solutions.Papercraft.Abstraction;
using X39.Solutions.Papercraft.Attributes;
using X39.Solutions.Papercraft.Controls.Base;
using X39.Solutions.Papercraft.Data;

[Control(Constants.ControlsNamespace, "approvalStamp")]
public sealed class ApprovalStampControl : Control
{
    protected override Size DoMeasure(
        float dpi,
        in Size fullPageSize,
        in Size framedPageSize,
        in Size remainingSize,
        CultureInfo cultureInfo)
        => Size.Zero;

    protected override Size DoArrange(
        float dpi,
        in Size fullPageSize,
        in Size framedPageSize,
        in Size remainingSize,
        CultureInfo cultureInfo)
        => Size.Zero;

    protected override Size DoRender(
        IDeferredCanvas canvas,
        float dpi,
        in Size parentSize,
        CultureInfo cultureInfo)
        => Size.Zero;
}

Register the control:

services.AddPapercraft(
    (builder) => builder.AddControl<ApprovalStampControl>());

Use the registered element name in the template:

<template>
    <body>
        <approvalStamp/>
    </body>
</template>

Elements without an XML namespace are treated as if they are in the runtime’s built-in control namespace. Registering a custom control with Constants.ControlsNamespace lets template authors use it beside built-in controls without adding xmlns, as shown above.

If a template sets a custom default XML namespace, unprefixed elements move into that namespace and built-in controls are not found unless the application registers matching controls there. Current templates should not use prefixed element names such as default:text; the reader validates the XML element name itself and rejects prefixed control names.

Existing compatibility templates may still contain the legacy XML namespace X39.Solutions.PdfTemplate.Controls. Keep that namespace unchanged during a package migration unless the application team has verified the runtime namespace registrations for the release being used.

Add A Transformer

Use a transformer only when XML nodes need to be included, removed, repeated or rewritten before controls are created. For simple calculated values, prefer a function.

using System.Globalization;
using System.Runtime.CompilerServices;
using X39.Solutions.Papercraft.Abstraction;
using XmlNode = X39.Solutions.Papercraft.Xml.XmlNode;

public sealed class KeepChildrenTransformer : ITransformer
{
    public string Name => "keep";

    public async IAsyncEnumerable<XmlNode> TransformAsync(
        CultureInfo cultureInfo,
        ITemplateData templateData,
        string remainingLine,
        IReadOnlyCollection<XmlNode> nodes,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await Task.Yield();
        foreach (var node in nodes)
        {
            yield return node.DeepCopy();
        }
    }
}

Register the transformer:

services.AddPapercraft(
    (builder) => builder.AddTransformer<KeepChildrenTransformer>());

The transformer is then available by name:

@keep {
    <text>This node is returned by the custom transformer.</text>
}

Add An Image Resolver

The default image resolver accepts base64 image data and data:image/...;base64,... sources. It does not read the file system or the internet. Register a custom IResourceResolver after AddPapercraft when templates need images from application storage:

using X39.Solutions.Papercraft.Services.ResourceResolver;

services.AddPapercraft();
services.AddScoped<IResourceResolver, ApplicationImageResolver>();
public sealed class ApplicationImageResolver : IResourceResolver
{
    public async ValueTask<byte[]> ResolveImageAsync(
        string source,
        object? context,
        CancellationToken cancellationToken = default)
    {
        var request = context as PrintRequestContext;
        return await LoadImageBytesAsync(source, request, cancellationToken);
    }
}

PapercraftRenderOptions.DocumentOptions.Context is passed unchanged to IResourceResolver.ResolveImageAsync and to controls that implement IInitializeControlAsync.

Extension Point Map

Choose the smallest extension point that solves the template author’s request. Most requests fit one of these rows:

Interface or type Use when How it is wired
PapercraftRenderer The application needs to render or validate a template. Resolve it from dependency injection for each isolated render workflow.
PapercraftGenerator Advanced code needs a backend-neutral PapercraftDocument before rendering. Resolve it from dependency injection when a backend-neutral document is needed directly.
ITemplateData The application supplies variables, registers functions or evaluates expressions inside custom extensions. Use renderer.TemplateData before rendering; custom transformers and functions receive it through their APIs.
IFunction The template needs a reusable calculated value. Implement Name, argument metadata and ExecuteAsync, then register with AddFunction<TFunction>().
IControl The application must render a new XML element. Add [Control(...)], implement measure/arrange/render behavior and register with AddControl<TControl>().
IContentControl A custom control must contain child controls. Implement CanAdd and child storage, or derive from an existing content-control base.
IInitializeControlAsync A control needs async setup or request context before rendering. Implement it on the control; DocumentOptions.Context is passed to InitializeControlAsync.
ITransformer XML nodes must be conditionally rewritten, removed or repeated before controls are created. Implement Name and TransformAsync, then register with AddTransformer<TTransformer>().
IResourceResolver Image sources must be resolved from application-specific storage. Register an IResourceResolver implementation after AddPapercraft.
IParameterConverter<T> A custom control attribute needs custom parsing. Set the converter on the control property with ParameterAttribute.Converter.
IPapercraftRenderBackend The application needs a custom output backend. Expose backend capabilities, ITextService, and render methods, then register the backend with DI.
IControlFactory Advanced activation behavior is needed. Replace the DI service only for unusual activation needs; most applications should use AddControl<TControl>().

Source-checked registration notes:

Advanced Infrastructure Interfaces

These interfaces are public because controls, renderers and extension points use them. Most applications should not replace them directly.

Interface Use
IDrawableCanvas Low-level drawing surface used by controls. Custom controls may call it through the deferred canvas.
IDeferredCanvas Canvas passed to IControl.Render; it records drawing work until page-specific values are available.
IImmediateCanvas Canvas exposed inside deferred drawing for page-specific information such as the current page and total pages.
ITextService Text measurement and text display-list service used by text-based controls. PapercraftRenderer uses the selected backend’s ITextService; the unkeyed DI service is a direct-control-activation fallback.
IPropertyAccessCache Expression-evaluation infrastructure that caches property access. It is not a normal application extension point.

Compatibility Package And Legacy API

Existing applications can keep the compatibility package during the additive migration:

dotnet add package X39.Solutions.PdfTemplate

The compatibility package keeps the legacy service registration and generator entry point available:

using System.Globalization;
using System.Xml;
using Microsoft.Extensions.DependencyInjection;
using X39.Solutions.PdfTemplate;

services.AddPdfTemplateService();

await using var scope = serviceProvider.CreateAsyncScope();
using var generator = scope.ServiceProvider.GetRequiredService<Generator>();

using var reader = XmlReader.Create(xmlTemplateStream);
await using var output = File.Create("document.pdf");

await generator.GeneratePdfAsync(
    output,
    reader,
    CultureInfo.CurrentUICulture);

Generator.GenerateBitmapsAsync(...) remains the legacy SkiaSharp bitmap API. For new raster workflows, prefer the renderer-neutral PapercraftRenderer.RenderRasterPagesAsync(...).

When Template Authors Need Developer Help

Ask for application work when a manual page or template needs:

Keep those decisions in application code. The user-facing template should stay focused on XML structure, layout, data placeholders and small task examples.

Previous: Troubleshooting Manual home Next: Renderer backends