Skip to content

Commit

Permalink
Add support for rendering areas in PDF templates
Browse files Browse the repository at this point in the history
Introduce the concept of `areas` that can be rendered in designated positions within a PDF template, ignoring margin rules. Implement new exceptions to handle incomplete area configurations and unsupported child controls within content controls. Update generator and template processing to measure, arrange, and render these `areas`, with new unit tests verifying the functionality.
  • Loading branch information
X39 committed Oct 31, 2024
1 parent a283b7a commit 0ee2685
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .idea/.idea.X39.Solutions.PdfTemplate/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Use [GitHub](https://github.com/X39/X39.Solutions.PdfTemplate) for best reading
* [`th`](#th)
* [`tr`](#tr)
* [`td`](#td)
* [`area`](#area)
* [Transformers](#transformers)
* [Creating your own transformer](#creating-your-own-transformer)
* [Evaluating user data](#evaluating-user-data)
Expand Down Expand Up @@ -174,6 +175,11 @@ It has four base sections:
working with the initial size.
-->
</foreground>
<areas>
<area left="10cm" right="10cm" height="10cm" top="10cm">
<!-- See About areas -->
</area>
</areas>
</template>
```

Expand All @@ -189,6 +195,28 @@ you must use a different prefix for the controls,
such as `xmlns:prefix="X39.Solutions.PdfTemplate.Controls"`.
For instance, `<text>` would then be written as `<prefix:text>`.



### About `areas`

The `areas` section is a special section, rendering content at a designated area.
The area is identified by a position provided on a separate node and ignore margin rules.

Areas are rendered above body but below foreground.

It has the following attributes:

| Attribute | Description | Values | Default |
|-----------|-------------------------------------------------------------------------------------------------------------------------------------|---------------------|---------|
| `Width` | The width of the area. | [`Length`](#length) | `0` |
| `Height` | The height of the area. | [`Length`](#length) | `0` |
| `Left` | The distance from the left side of a page for the area. If both `Left` and `Right` values are provided, `Width` will be ignored. | [`Length`](#length) | `0` |
| `Top` | The distance from the top side of a page for the area. If both `Top` and `Bottom` values are provided, `Height` will be ignored. | [`Length`](#length) | `0` |
| `Right` | The distance from the right side of a page for the area. If both `Left` and `Right` values are provided, `Width` will be ignored. | [`Length`](#length) | `null` |
| `Bottom` | The distance from the bottom side of a page for the area. If both `Top` and `Bottom` values are provided, `Height` will be ignored. | [`Length`](#length) | `null` |



## Integration

### Functions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace X39.Solutions.PdfTemplate.Exceptions;

/// <summary>
/// Represents an exception that is thrown when an area is incomplete in a PDF template.
/// </summary>
public class AreaIncompleteException : Exception
{
/// <summary>
/// Gets the line number where the exception occurred.
/// </summary>
public int Line { get; }

/// <summary>
/// Gets the column number associated with the exception.
/// </summary>
/// <remarks>
/// The Column property indicates the specific column where the incomplete area
/// was encountered during the parsing or processing operation.
/// </remarks>
public int Column { get; }

/// <summary>
/// Represents an exception that is thrown when an area is found to be incomplete.
/// </summary>
internal AreaIncompleteException(int line, int column, string message) : base(message)
{
Line = line;
Column = column;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace X39.Solutions.PdfTemplate.Exceptions;

/// <summary>
/// Exception thrown when an attempt is made to add child controls to a content control
/// that does not support children.
/// </summary>
/// <remarks>
/// This exception provides details about the location in the template where the
/// unsupported children were encountered, including the line and column numbers.
/// </remarks>
public class ContentControlDoesNotSupportChildrenException : Exception
{
/// <summary>
/// Gets the line number where the exception occurred.
/// </summary>
public int Line { get; }

/// <summary>
/// Gets the column number where the exception occurred.
/// </summary>
/// <remarks>
/// This property holds the column number which can help in identifying
/// the exact location in the document or template that caused the exception.
/// </remarks>
public int Column { get; }

/// <summary>
/// Represents an exception that is thrown when a content control
/// does not support the inclusion of child controls.
/// </summary>
/// <remarks>
/// This exception provides information about the line and column
/// where the issue occurred, aiding in debugging and error tracking.
/// </remarks>
internal ContentControlDoesNotSupportChildrenException(int line, int column, string message) : base(message)
{
Line = line;
Column = column;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace X39.Solutions.PdfTemplate.Exceptions;

/// <summary>
/// Represents an exception that is thrown when a content control
/// does not support the provided child control type.
/// </summary>
/// <remarks>
/// This exception is specifically used within the context of PDF template processing
/// where content controls have strict type constraints on the child controls they can contain.
/// </remarks>
/// <example>
/// An instance of this exception may be thrown during the creation of a control hierarchy
/// when a child control that does not match the expected type is added to a content control.
/// </example>
/// <seealso cref="X39.Solutions.PdfTemplate.Abstraction.IContentControl"/>
[PublicAPI]
public class ContentControlDoesNotSupportTheProvidedChildException : Exception
{
/// <summary>
/// Gets the line number where the exception occurred.
/// </summary>
public int Line { get; }

/// <summary>
/// Gets the column number where the exception occurred.
/// </summary>
public int Column { get; }

/// <summary>
/// Gets the type of the unsupported child control.
/// </summary>
public Type Type { get; }

/// <summary>
/// Exception thrown when a content control does not support the provided child type.
/// This exception is used to indicate that an attempt was made to add a child control to a content control,
/// but the content control does not support adding children of the specified type.
/// </summary>
/// <param name="line">The line number in the source where the error occurred.</param>
/// <param name="column">The column number in the source where the error occurred.</param>
/// <param name="type">The type of the child control that caused the exception.</param>
/// <param name="message">The message of the exception.</param>
internal ContentControlDoesNotSupportTheProvidedChildException(int line, int column, Type type, string message) : base(message)
{
Line = line;
Column = column;
Type = type;
}
}
83 changes: 81 additions & 2 deletions source/X39.Solutions.PdfTemplate/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using X39.Solutions.PdfTemplate.Functions;
using X39.Solutions.PdfTemplate.Services;
using X39.Solutions.PdfTemplate.Xml;
using X39.Util.Collections;

namespace X39.Solutions.PdfTemplate;

Expand Down Expand Up @@ -246,6 +247,15 @@ private async Task GenerateAsync(
control.Measure(options.DotsPerInch, pageSize, pageSize, pageSize, cultureInfo);
}

foreach (var area in template.AreaControls)
{
var tuple = area.CalculateClippingAndTranslationData(options.DotsPerInch, originalPageSize);
foreach (var areaControl in area.Controls)
{
areaControl.Measure(options.DotsPerInch, tuple.size, tuple.size, tuple.size, cultureInfo);
}
}

#endregion

#region Arrange
Expand Down Expand Up @@ -334,10 +344,28 @@ private async Task GenerateAsync(

#endregion

#region Areas

var areaSizes = new List<(int areaIndex, Size size)>();
foreach (var (area, areaIndex) in template.AreaControls.Indexed())
{
var tuple = area.CalculateClippingAndTranslationData(options.DotsPerInch, originalPageSize);

foreach (var control in area.Controls)
{
var size = control.Arrange(options.DotsPerInch, tuple.size, tuple.size, tuple.size, cultureInfo);
areaSizes.Add((areaIndex, size));
}
}

#endregion

#endregion

#region Render

#region Background

var backgroundCanvasAbstraction = new DeferredCanvasImpl
{
ActualPageSize = originalPageSize, PageSize = originalPageSize,
Expand All @@ -351,6 +379,10 @@ private async Task GenerateAsync(
}
}

#endregion

#region Header

var headerCanvasAbstraction = new DeferredCanvasImpl
{
ActualPageSize = originalPageSize, PageSize = pageSize,
Expand All @@ -364,6 +396,10 @@ private async Task GenerateAsync(
}
}

#endregion

#region Body

var bodyCanvasAbstraction = new DeferredCanvasImpl { ActualPageSize = originalPageSize, PageSize = pageSize, };
float desiredBodyHeight;
using (bodyCanvasAbstraction.CreateState())
Expand All @@ -382,7 +418,10 @@ private async Task GenerateAsync(
}
}

var pageCount = Math.Max((ushort) Math.Ceiling(desiredBodyHeight / bodyPageSize.Height), (ushort) 1);
#endregion


#region Footer

var footerCanvasAbstraction = new DeferredCanvasImpl
{
Expand All @@ -397,6 +436,39 @@ private async Task GenerateAsync(
}
}

#endregion

#region Area

var areaCanvasAbstraction = new DeferredCanvasImpl
{
ActualPageSize = originalPageSize, PageSize = originalPageSize,
};
using (areaCanvasAbstraction.CreateState())
{
foreach (var (area, areaIndex) in template.AreaControls.Indexed())
{
var tuple = area.CalculateClippingAndTranslationData(options.DotsPerInch, originalPageSize);
using (areaCanvasAbstraction.CreateState())
{
// Translate canvas
areaCanvasAbstraction.Translate(tuple.left, tuple.top);
// Clip Canvas
areaCanvasAbstraction.Clip(0, 0, tuple.width, tuple.height);
foreach (var (control, (_, size)) in area.Controls.Zip(areaSizes.Where(t => t.areaIndex == areaIndex)))
{
_ = control.Render(areaCanvasAbstraction, options.DotsPerInch, tuple.size, cultureInfo);
areaCanvasAbstraction.Translate(0F, size.Height);
}
}
}
}


#endregion

#region Foreground

var foregroundCanvasAbstraction = new DeferredCanvasImpl
{
ActualPageSize = originalPageSize, PageSize = originalPageSize,
Expand All @@ -410,8 +482,11 @@ private async Task GenerateAsync(
}
}

#endregion

var currentHeight = 0F;

var pageCount = Math.Max((ushort) Math.Ceiling(desiredBodyHeight / bodyPageSize.Height), (ushort) 1);
for (var i = 0; i < pageCount; i++)
{
// ReSharper disable AccessToDisposedClosure
Expand Down Expand Up @@ -452,14 +527,18 @@ private async Task GenerateAsync(
footerCanvasAbstraction.Render(immediateCanvas);
}
}
using (immediateCanvas.CreateState())
{
immediateCanvas.Translate(0, i * originalPageSize.Height);
areaCanvasAbstraction.Render(immediateCanvas);
}

using (immediateCanvas.CreateState())
{
immediateCanvas.Clip(0, 0, foregroundPageSize.Width, foregroundPageSize.Height);
foregroundCanvasAbstraction.Render(immediateCanvas);
}

canvas.Restore();
currentHeight += bodyPageSize.Height;
}
// ReSharper restore AccessToDisposedClosure
Expand Down
Loading

0 comments on commit 0ee2685

Please sign in to comment.