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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions src/FlexRender.Cli/Commands/DebugLayoutCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public static Command Create()
/// <param name="verbose">Whether to enable verbose output.</param>
/// <param name="fontsDir">Optional fonts directory.</param>
/// <returns>Exit code (0 for success, non-zero for failure).</returns>
private static Task<int> Execute(
private static async Task<int> Execute(
FileInfo templateFile,
FileInfo? dataFile,
FileInfo? outputFile,
Expand All @@ -74,21 +74,21 @@ private static Task<int> Execute(
if (!templateFile.Exists)
{
Console.Error.WriteLine($"Error: Template file not found: {templateFile.FullName}");
return Task.FromResult(1);
return 1;
}

// Validate data file if specified
if (dataFile is not null && !dataFile.Exists)
{
Console.Error.WriteLine($"Error: Data file not found: {dataFile.FullName}");
return Task.FromResult(1);
return 1;
}

// Validate fonts directory if specified
if (fontsDir is not null && !fontsDir.Exists)
{
Console.Error.WriteLine($"Error: Fonts directory not found: {fontsDir.FullName}");
return Task.FromResult(1);
return 1;
}

try
Expand Down Expand Up @@ -137,7 +137,7 @@ private static Task<int> Execute(
}

// Compute layout using the same renderer as actual rendering
var root = skiaRender.ComputeLayout(template, templateData);
var root = await skiaRender.ComputeLayout(template, templateData);

// Print registered fonts
Console.WriteLine("Fonts:");
Expand All @@ -158,17 +158,17 @@ private static Task<int> Execute(
// Optionally render debug image
if (outputFile is not null)
{
RenderDebugImage(template, root, templateData, outputFile.FullName, skiaRender);
await RenderDebugImage(template, root, templateData, outputFile.FullName, skiaRender);
Console.WriteLine();
Console.WriteLine($"Debug image: {outputFile.FullName}");
}

return Task.FromResult(0);
return 0;
}
catch (TemplateParseException ex)
{
Console.Error.WriteLine($"Template error: {ex.Message}");
return Task.FromResult(1);
return 1;
}
catch (Exception ex)
{
Expand All @@ -177,7 +177,7 @@ private static Task<int> Execute(
{
Console.Error.WriteLine(ex.StackTrace);
}
return Task.FromResult(1);
return 1;
}
}

Expand Down Expand Up @@ -281,20 +281,22 @@ private static string GetComputedExtra(LayoutNode node)
/// <param name="data">The template data.</param>
/// <param name="outputPath">The output file path.</param>
/// <param name="skiaRender">The SkiaRender instance with fonts already registered.</param>
private static void RenderDebugImage(
private static async Task RenderDebugImage(
Template template,
LayoutNode root,
ObjectValue data,
string outputPath,
FlexRender.Skia.SkiaRender skiaRender)
{
var size = skiaRender.Measure(template, data);
var size = await skiaRender.Measure(template, data);

using var bitmap = new SKBitmap((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height));
using var canvas = new SKCanvas(bitmap);

// Render template normally
skiaRender.Render(canvas, template, data);
// Render template to PNG and decode back to draw onto debug canvas
var pngBytes = await skiaRender.RenderToPng(template, data);
using var rendered = SKBitmap.Decode(pngBytes);
canvas.DrawBitmap(rendered, 0, 0);

// Draw debug overlay
DrawDebugOverlay(canvas, root, 0, 0, skiaRender.FontManager);
Expand Down
4 changes: 2 additions & 2 deletions src/FlexRender.Core/Parsing/Ast/ExprValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ public override string ToString()
if (IsExpression)
return $"Expr({RawValue})";
if (RawValue is not null)
return $"Raw({RawValue})={Value}";
return $"{Value}";
return string.Create(CultureInfo.InvariantCulture, $"Raw({RawValue})={Value}");
return string.Create(CultureInfo.InvariantCulture, $"{Value}");
}
}
34 changes: 0 additions & 34 deletions src/FlexRender.Core/TemplateEngine/TemplateExpander.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,40 +77,6 @@ public TemplateExpander(ResourceLimits limits, FilterRegistry filterRegistry, Cu
_resourceLoaders = resourceLoaders;
}

/// <summary>
/// Expands EachElement and IfElement instances into concrete elements based on data.
/// Returns a new Template with all control flow elements resolved.
/// </summary>
/// <param name="template">The template containing control flow elements.</param>
/// <param name="data">The data for evaluating conditions and iterating arrays.</param>
/// <returns>A new Template with expanded elements.</returns>
/// <exception cref="ArgumentNullException">Thrown when template or data is null.</exception>
/// <exception cref="TemplateEngineException">Thrown when maximum expansion depth is exceeded.</exception>
public Template Expand(Template template, ObjectValue data)
{
ArgumentNullException.ThrowIfNull(template);
ArgumentNullException.ThrowIfNull(data);

var context = new TemplateContext(data);
var expandedElements = ExpandElements(template.Elements, context, 0, template);

var result = new Template
{
Name = template.Name,
Version = template.Version,
Canvas = template.Canvas,
Elements = expandedElements
};

// Copy fonts
foreach (var font in template.Fonts)
{
result.Fonts[font.Key] = font.Value;
}

return result;
}

/// <summary>
/// Asynchronously expands EachElement and IfElement instances into concrete elements based on data.
/// Returns a new Template with all control flow elements resolved.
Expand Down
24 changes: 0 additions & 24 deletions src/FlexRender.Core/TemplateEngine/TemplatePipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,6 @@ public TemplatePipeline(TemplateExpander expander, TemplateProcessor templatePro
_templateProcessor = templateProcessor;
}

/// <summary>
/// Processes a template through the full pipeline: Expand, Resolve, Materialize.
/// </summary>
/// <param name="template">The parsed template to process.</param>
/// <param name="data">The data context for expression evaluation.</param>
/// <returns>The expanded and resolved template with all expressions materialized.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="template"/> or <paramref name="data"/> is null.</exception>
public Template Process(Template template, ObjectValue data)
{
ArgumentNullException.ThrowIfNull(template);
ArgumentNullException.ThrowIfNull(data);

// Phase 1: Expand control flow (#if, #each, table)
var expanded = _expander.Expand(template, data);

// Phase 2: Resolve expressions in all ExprValue properties
ResolveAll(expanded, data);

// Phase 3: Materialize resolved strings into typed values
MaterializeAll(expanded);

return expanded;
}

/// <summary>
/// Asynchronously processes a template through the full pipeline: Expand, Resolve, Materialize.
/// Uses <c>await</c> for the expansion phase to support async content source resolution.
Expand Down
32 changes: 14 additions & 18 deletions src/FlexRender.ImageSharp.Render/ImageSharpRender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ public async Task RenderToPng(

try
{
using var image = _engine.RenderToImage(
layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry);
using var image = _engine.RenderToImage(processedTemplate, imageCache);

var encoder = new PngEncoder();
await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -241,8 +240,7 @@ public async Task RenderToJpeg(

try
{
using var image = _engine.RenderToImage(
layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry);
using var image = _engine.RenderToImage(processedTemplate, imageCache);

var encoder = new JpegEncoder { Quality = effectiveOptions.Quality };
await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -291,8 +289,7 @@ public async Task RenderToBmp(

try
{
using var image = _engine.RenderToImage(
layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry);
using var image = _engine.RenderToImage(processedTemplate, imageCache);

var encoder = new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32 };
await image.SaveAsync(output, encoder, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -339,8 +336,7 @@ public async Task RenderToRaw(

try
{
using var image = _engine.RenderToImage(
layoutTemplate, effectiveData, _filterRegistry, imageCache, processedTemplate, _contentParserRegistry);
using var image = _engine.RenderToImage(processedTemplate, imageCache);

// Write raw RGBA pixel data
var pixelCount = checked(image.Width * image.Height);
Expand All @@ -360,26 +356,23 @@ public async Task RenderToRaw(
// ========================================================================

/// <summary>
/// Pre-loads all images referenced in the template using the configured resource loaders.
/// Also returns the processed template so callers can skip redundant expand+preprocess steps.
/// Expands and processes the template asynchronously, then pre-loads all images
/// referenced in it using the configured resource loaders.
/// </summary>
/// <param name="template">The template to scan for image references.</param>
/// <param name="template">The template to process and scan for image references.</param>
/// <param name="data">The data context for expression evaluation.</param>
/// <param name="cancellationToken">Cancellation token for async operations.</param>
/// <returns>
/// A tuple of the processed template and an image cache. The processed template is non-null
/// when resource loaders are configured (since expand+preprocess was already performed).
/// The image cache maps URIs to pre-loaded images, or is <c>null</c> when no images were found.
/// A tuple of the fully processed template and an image cache. The processed template
/// is always non-null. The image cache maps URIs to pre-loaded images, or is <c>null</c>
/// when no images were found or no resource loaders are configured.
/// Caller is responsible for disposing images via <see cref="DisposeImageCache"/>.
/// </returns>
private async Task<(Template? processedTemplate, Dictionary<string, Image<Rgba32>>? imageCache)> PreloadImages(
private async Task<(Template processedTemplate, Dictionary<string, Image<Rgba32>>? imageCache)> PreloadImages(
Template template,
ObjectValue data,
CancellationToken cancellationToken)
{
if (_resourceLoaders.Count == 0)
return (null, null);

// Expand, resolve, and materialize template to resolve expressions in image src attributes
var expander = _filterRegistry is not null
? new TemplateExpander(_limits, _filterRegistry, _contentParserRegistry, _resourceLoaders)
Expand All @@ -391,6 +384,9 @@ public async Task RenderToRaw(
var pipeline = new TemplatePipeline(expander, templateProcessor);
var processedTemplate = await pipeline.ProcessAsync(template, data).ConfigureAwait(false);

if (_resourceLoaders.Count == 0)
return (processedTemplate, null);

var uris = ImageSharpRenderingEngine.CollectImageUris(processedTemplate);
if (uris.Count == 0)
return (processedTemplate, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using FlexRender.Parsing.Ast;
using FlexRender.Providers;
using FlexRender.Rendering;
using FlexRender.TemplateEngine;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
Expand Down Expand Up @@ -67,52 +66,25 @@ internal ImageSharpRenderingEngine(
}

/// <summary>
/// Renders a template to a new Image&lt;Rgba32&gt;.
/// Renders a pre-processed template to a new Image&lt;Rgba32&gt;.
/// The caller must run the template through <c>TemplatePipeline.ProcessAsync</c>
/// before invoking this method.
/// </summary>
/// <param name="template">The template to render.</param>
/// <param name="data">The data context for expression evaluation.</param>
/// <param name="filterRegistry">Optional filter registry.</param>
/// <param name="processedTemplate">
/// A fully expanded and processed template. Must have all expressions resolved
/// and content materialised via <c>TemplatePipeline.ProcessAsync</c>.
/// </param>
/// <param name="imageCache">
/// Optional pre-loaded image cache for HTTP and other async sources.
/// When provided, images are resolved from the cache before falling back to
/// inline loading (file and base64 only).
/// </param>
/// <param name="preprocessedTemplate">
/// Optional pre-processed template from image preloading. When provided, the
/// expand+preprocess steps are skipped to avoid redundant work.
/// </param>
/// <param name="contentParserRegistry">Optional content parser registry for custom content type parsing.</param>
/// <returns>A new image containing the rendered template. Caller owns disposal.</returns>
internal Image<Rgba32> RenderToImage(
Template template,
ObjectValue data,
FilterRegistry? filterRegistry = null,
IReadOnlyDictionary<string, Image<Rgba32>>? imageCache = null,
Template? preprocessedTemplate = null,
ContentParserRegistry? contentParserRegistry = null)
Template processedTemplate,
IReadOnlyDictionary<string, Image<Rgba32>>? imageCache = null)
{
ArgumentNullException.ThrowIfNull(template);
ArgumentNullException.ThrowIfNull(data);

Template processedTemplate;

if (preprocessedTemplate is not null)
{
processedTemplate = preprocessedTemplate;
}
else
{
// Expand, resolve, and materialize template via the Core pipeline
var expander = filterRegistry is not null
? new TemplateExpander(_limits, filterRegistry, contentParserRegistry, _resourceLoaders)
: new TemplateExpander(_limits, contentParserRegistry, _resourceLoaders);
var templateProcessor = filterRegistry is not null
? new TemplateProcessor(_limits, filterRegistry)
: new TemplateProcessor(_limits);

var pipeline = new TemplatePipeline(expander, templateProcessor);
processedTemplate = pipeline.Process(template, data);
}
ArgumentNullException.ThrowIfNull(processedTemplate);

// Register fonts from the processed template (backend-specific)
_preprocessor.RegisterFonts(processedTemplate);
Expand Down
Loading