paint-brush
Building .NET Document API Layouts Using Flat Element Hierarchyby@mesciusinc
143 reads

Building .NET Document API Layouts Using Flat Element Hierarchy

by MESCIUS inc.June 21st, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

GrapeCity.Imaging library contains the GrapeCity.Documents.Layout namespace with a few classes implementing the flat layout model based on constraints. Instead of setting the exact position of an element, constraints define rules for how that position depends on the positions of other elements. Each parameter is configured separately and might rely on any other rectangle in the same view.
featured image - Building .NET Document API Layouts Using Flat Element Hierarchy
MESCIUS inc. HackerNoon profile picture


When we draw text, graphics, and other elements on a PDF page (or JPEG, SVG, and so on), we need to specify their coordinates. If there are multiple elements on the page and the size or position of some elements depends on the size or position of other elements, this task is not easy. If we associate a rectangle with each element, our task is to calculate the coordinates of each of these rectangles. We can then fit and draw the elements into the target rectangles.


In computer graphics and user interfaces, most frameworks are based on the container model, where elements are contained within the parent elements and have their own children. For example, in XAML, a ComboBox can be placed in a StackPanel, and that panel is added to a DockPanel, which is a child of the main Window.


An alternative approach was suggested in Android with its ConstraintLayout and the Auto Layout of iOS/macOS. They offer a layout model with a flat view hierarchy (no nested view groups). That model is more flexible and customizable than the container model.


The GrapeCity.Documents.Imaging library contains the GrapeCity.Documents.Layout namespace with a few classes implementing the flat layout model based on constraints. Instead of setting the exact position of an element, constraints define rules for how that position depends on the positions of other elements.


The main distinction from the container model is that each parameter is configured separately and might rely on any other rectangle in the same view or even multiple rectangles if there are several constraints for the same parameter.


For example:

using System.Drawing;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Layout;
using GrapeCity.Documents.Pdf;

const float pageWidth = 600;
const float pageHeight = 400;

var doc = new GcPdfDocument();
var page = doc.NewPage();
page.Size = new SizeF(pageWidth, pageHeight);
var g = page.Graphics;
var baseTransform = g.Transform;

var host = new LayoutHost();
var view = host.CreateView(pageWidth, pageHeight);

var marginRect = view.CreateRect();
// marginRect.SetLeft(null, AnchorParam.Left, 36);
// marginRect.SetTop(null, AnchorParam.Top, 36);
// marginRect.SetRight(null, AnchorParam.Right, -36);
// marginRect.SetBottom(null, AnchorParam.Bottom, -36);
marginRect.AnchorDeflate(null, 36);

var redRect = view.CreateRect();
redRect.SetLeft(marginRect, AnchorParam.Left, 60);
redRect.SetTop(marginRect, AnchorParam.Top, 40);
redRect.SetWidth(180);
redRect.SetHeight(70);

host.PerformLayout();

DrawRect(marginRect, Color.Green);
DrawRect(redRect, Color.Red);

void DrawRect(LayoutRect rect, Color color)
{
    g.Transform = rect.Transform.Multiply(baseTransform);
    g.DrawRectangle(rect.AsRectF(), new Pen(color));
}

g.Transform = baseTransform;
doc.Save("doc1.pdf");


The resulting document in doc1.pdf looks like this:

.NET API Layout


There is nothing special here, just two rectangles on a PDF page. But this code can be considered as the base for creating much more complex layouts. Let’s describe the main aspects.


Initially, we create a PDF document with a page of the given size. Then we initialize a LayoutHost:

var host = new LayoutHost();


LayoutHost is the root of the whole infrastructure of layout objects. It is used for creating views and defining the origin of the coordinate system. LayoutHost performs layout when other objects are prepared and linked. We always need a host to instantiate views and calculate the layout.

var view = host.CreateView(pageWidth, pageHeight);


LayoutView defines a root rectangle with fixed width and height. For example, in this simple case, the view dimensions are equal to the corresponding dimensions of the PDF page. Each view has the associated transformation matrix (the identity matrix by default).


A LayoutView is used for hosting LayoutRect objects, rectangles whose sides are always parallel to either the X-axis or Y-axis of the view. The root rectangle of a LayoutView is not a clipping area. LayoutRects can be placed inside and outside the bounds of the view. It just defines the base dimensions to be referenced from constraints assigned to LayoutRect parameters.

var marginRect = view.CreateRect();


We create a LayoutRect for the page margin. LayoutRect is a rectangle defined by four points:

LayoutRect


It can be rotated by a multiple of 90 degrees relative to the owner LayoutView. The main task of the layout engine is to calculate the position of the points P0, P1, and P2 for each LayoutRect within a LayoutHost. To make that possible, we need to define constraints that unambiguously determine the position and size of each rectangle, for example:

marginRect.SetLeft(null, AnchorParam.Left, 36);
marginRect.SetTop(null, AnchorParam.Top, 36);
marginRect.SetRight(null, AnchorParam.Right, -36);
marginRect.SetBottom(null, AnchorParam.Bottom, -36);


Constraints can be defined relative to the owner LayoutView (if we pass the null value in the first parameter) or relative to any other LayoutRect that belongs to the same LayoutView. In the above code, we specify a 36-point padding for each side of the margin rectangle relative to the corresponding sides of the LayoutView.


As you can see, the previous code is rather verbose. The LayoutRect class has several methods whose names start with “Anchor“, such as AnchorTopLeft, AnchorDeflate, and others. Those methods add multiple constraints at once. For example, all the above constraints can be set with a single line of code:

marginRect.AnchorDeflate(null, 36);


In the next step, we create another rectangle and specify constraints relative to the first one:

var redRect = view.CreateRect();
redRect.SetLeft(marginRect, AnchorParam.Left, 60);
redRect.SetTop(marginRect, AnchorParam.Top, 40);
redRect.SetWidth(180);
redRect.SetHeight(70);


The width and height are defined with absolute values. Please note that we don’t set the Width and Height properties directly. Instead, we add the proper constraints to be resolved together with other constraints when we execute the following command:

host.PerformLayout();


The LayoutHost calculates all rectangle coordinates based on the constraints provided. At this point, you can observe a LayoutException if some inconsistent combination of constraints was set.


An interesting part of the drawing code:

void DrawRect(LayoutRect rect, Color color)
{
    g.Transform = rect.Transform.Multiply(baseTransform);
    g.DrawRectangle(rect.AsRectF(), new Pen(color));
}


The local method accepts a LayoutRect and a color to draw the rectangle. After performing the layout, each LayoutRect has the associated transformation matrix. If we multiply that matrix by the transformation matrix of GcGraphics, the origin of the coordinate system is moved to the P0 point of the rectangle. The X-axis direction is P0P1, and the Y-axis direction is P0P2. The AsRectF method of a LayoutRect works like executing “new RectangleF(0, 0, rect.Width, rect.Height)“.

Types of Constraints

To make the layout consistent, we have to associate constraints with LayoutRects. The possible target parameters of a LayoutRect and the corresponding constraint classes are in the following table:


To create a constraint, you can execute one of the following methods of the LayoutRect class:


All constraints created with the Set… methods are unique. In other words, you must not assign two such constraints to the same parameter. Constraints created with the Append… methods don’t have to be unique. A problem might occur if you append inconsistent constraints, for example, if you add the MinWidth and MaxWidth constraints so that MaxWidth < MinWidth.


One important thing. If you create a constraint for a rotated rectangle, move yourself to that rectangle’s point of view. For example, if the AngleConstraint is set to 270 degrees, the left side of a LayoutRect will be at the bottom, and the top side is actually at the left. But if you move yourself to that rectangle’s point of view, the left and top sides will correspondingly be at the left and top.


Let’s modify the previous sample to display the red rectangle rotated:

var redRect = view.CreateRect();
redRect.SetAngle(null, 270);
redRect.SetTop(marginRect, AnchorParam.Left, 60);
redRect.SetRight(marginRect, AnchorParam.Top, -30);
redRect.SetWidth(180);
redRect.SetHeight(70);


The resulting document looks like this:

.NET API Layout


The position of the red rectangle’s top side is set relative to the marginRect’s left side. The red’s right side is set relative to the marginRect’s top side. The right side has a negative offset because, from the rectangle’s point of view, we should move to the left from the top side of the marginRect. The left and up directions are negative, and the right and down directions correspond to a positive offset.

Chains and Star Constraints

Let’s assume we want to fill the whole width of some area with multiple rectangles having different proportional widths (weights). For example, we draw a table with four columns. The first column width is 20% of the second column width. The third column has a fixed width of 70 points. The fourth column width is 35% of the second column width.


Star width constraints are handy for such cases:

.NET API Layout


A star width specifies the proportion of the current column width in the sum of all star column widths. In the image above, if the full width is 300pt and the spacing between columns is 10pt, the sum width of all star columns is W = 300 - 70 - 10 * 3 = 200pt. The overall number of stars is S = 20 + 100 + 35 = 155. Individual column widths: W1 = 200 * 20 / 155, W2 = 200 * 100 / 155, W4 = 200 * 35 / 155.


The full sample:

using System.Drawing;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Layout;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Text;

const float pageWidth = 600;
const float pageHeight = 400;

var doc = new GcPdfDocument();
var page = doc.NewPage();
page.Size = new SizeF(pageWidth, pageHeight);
var g = page.Graphics;
var baseTransform = g.Transform;
var fmt = new TextFormat
{
    FontName = "Calibri",
    FontSize = 16
};

var host = new LayoutHost();
var view = host.CreateView(pageWidth, pageHeight);

var marginRect = view.CreateRect("margin");
marginRect.AnchorDeflate(null, 36);

var redRect = view.CreateRect("red");
redRect.AnchorLeftTopBottom(marginRect, 10, 50, 150);
redRect.SetStarWidth(20);

var blueRect = view.CreateRect("blue");
blueRect.AnchorTopBottom(marginRect, 50, 180);
blueRect.SetStarWidth(100);

var orangeRect = view.CreateRect("orange");
orangeRect.AnchorTopBottom(marginRect, 50, 120);
orangeRect.SetWidth(70);

var purpleRect = view.CreateRect("purple");
purpleRect.AnchorRightTopBottom(marginRect, 10, 50, 150);
purpleRect.SetStarWidth(35);

blueRect.SetLeftAndOpposite(redRect, AnchorParam.Right, 10);
orangeRect.SetLeftAndOpposite(blueRect, AnchorParam.Right, 10);
purpleRect.SetLeftAndOpposite(orangeRect, AnchorParam.Right, 10);

host.PerformLayout();

DrawRect(marginRect, Color.Green);
DrawRect(redRect, Color.Red);
DrawRect(blueRect, Color.Blue);
DrawRect(orangeRect, Color.Orange);
DrawRect(purpleRect, Color.Purple);

void DrawRect(LayoutRect rect, Color color)
{
    g.Transform = rect.Transform.Multiply(baseTransform);
    g.DrawRectangle(rect.AsRectF(), new Pen(color));
    g.DrawString((string)rect.Tag, fmt, new PointF(3, 0));
}

g.Transform = baseTransform;
doc.Save("doc3.pdf");


It saves a PDF document like this:

.NET API Layout


This sample uses the helper methods to set multiple constraints at once. For example:

redRect.AnchorLeftTopBottom(marginRect, 10, 50, 150);


That's a short call to the following commands:

redRect.SetAngle(marginRect, 0);
redRect.SetLeft(marginRect, AnchorParam.Left, 10);
redRect.SetTop(marginRect, AnchorParam.Top, 50);
redRect.SetBottom(marginRect, AnchorParam.Bottom, -150);


All available helper methods are listed below:


Anchors a LayoutRect to the top and bottom sides and rotation angle of another LayoutRect or the owner LayoutView and sets zero width.


Another thing to note in the previous example is using chained constraints:

blueRect.SetLeftAndOpposite(redRect, AnchorParam.Right, 10);
orangeRect.SetLeftAndOpposite(blueRect, AnchorParam.Right, 10);
purpleRect.SetLeftAndOpposite(orangeRect, AnchorParam.Right, 10);


We don’t need chained constraints when we create an explicit dependency between two parameters. For example, since the marginRect fully depends on the owner LayoutView, we can add a simple constraint based on marginRect for the left side of redRect:

redRect.SetLeft(marginRect, AnchorParam.Left, 10);


We cannot do the same with the left side of blueRect because the width of redRect is set in stars; that’s not a fixed width. The left sides of blue, orange, and purple rectangles, as well as the right sides of red, blue, and orange rectangles, are floating. For example, the position of the border between redRect and blueRect depends on both rectangle widths. We have to bind the left side of blueRect to the right side of redRect without specifying who depends on whom. The following constraint:

blueRect.SetLeftAndOpposite(redRect, AnchorParam.Right, 10);


It works as a couple of simple constraints:

redRect.SetRight(blueRect, AnchorParam.Left, -10);
blueRect.SetLeft(redRect, AnchorParam.Right, 10);


Actually, we cannot use the simple constraints in that case because they imply the explicit dependencies.


The chained constraints in the above sample can be replaced with their mirrored variants:

redRect.SetRightAndOpposite(blueRect, AnchorParam.Left, -10);
blueRect.SetRightAndOpposite(orangeRect, AnchorParam.Left, -10);
orangeRect.SetRightAndOpposite(purpleRect, AnchorParam.Left, -10);


Using SetRightAndOpposite for the left rectangle is equivalent to SetLeftAndOpposite for the right rectangle (the offset parameter changes its sign). Similar SetTopAndOpposite and SetBottomAndOpposite methods exist for assigned chained constraints in the vertical direction.

Min and Max Position Constraints

In the previous sample, we drew a series of rectangles with different levels of bottom sides. Now let’s draw another series at the 20 points below the first one. To make this task more interesting, the new series will consist of 5 rectangles placed between the horizontal centers of the red and purple rectangles. Each next rectangle in the new chain will be rotated 90 degrees relative to the previous one. All have the same star width (1) in the horizontal direction and a fixed distance (40 points) from the bottom of the marginRect.


The full source code is below:

using System.Drawing;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Layout;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Text;

const float pageWidth = 600;
const float pageHeight = 400;

var doc = new GcPdfDocument();
var page = doc.NewPage();
page.Size = new SizeF(pageWidth, pageHeight);
var g = page.Graphics;
var baseTransform = g.Transform;
var fmt = new TextFormat
{
    FontName = "Calibri",
    FontSize = 16
};

var host = new LayoutHost();
var view = host.CreateView(pageWidth, pageHeight);

var marginRect = view.CreateRect("margin");
marginRect.AnchorDeflate(null, 36);

var redRect = view.CreateRect("red");
redRect.AnchorLeftTopBottom(marginRect, 10, 50, 150);
redRect.SetStarWidth(20);

var blueRect = view.CreateRect("blue");
blueRect.AnchorTopBottom(marginRect, 50, 180);
blueRect.SetStarWidth(100);

var orangeRect = view.CreateRect("orange");
orangeRect.AnchorTopBottom(marginRect, 50, 120);
orangeRect.SetWidth(70);

var purpleRect = view.CreateRect("purple");
purpleRect.AnchorRightTopBottom(marginRect, 10, 50, 150);
purpleRect.SetStarWidth(35);

blueRect.SetLeftAndOpposite(redRect, AnchorParam.Right, 10);
orangeRect.SetLeftAndOpposite(blueRect, AnchorParam.Right, 10);
purpleRect.SetLeftAndOpposite(orangeRect, AnchorParam.Right, 10);

var rect1 = view.CreateRect("1");
rect1.SetBottom(marginRect, AnchorParam.Bottom, -40);
rect1.AppendMinTop(redRect, AnchorParam.Bottom, 20);
rect1.AppendMinTop(blueRect, AnchorParam.Bottom, 20);
rect1.AppendMinTop(orangeRect, AnchorParam.Bottom, 20);
rect1.AppendMinTop(purpleRect, AnchorParam.Bottom, 20);

rect1.SetLeft(redRect, AnchorParam.HorizontalCenter);
rect1.SetStarWidth(1);

var rect2 = view.CreateRect("2");
rect2.SetAngle(rect1, 90);
rect2.SetRight(marginRect, AnchorParam.Bottom, -40);
rect2.AppendMinLeft(redRect, AnchorParam.Bottom, 20);
rect2.AppendMinLeft(blueRect, AnchorParam.Bottom, 20);
rect2.AppendMinLeft(orangeRect, AnchorParam.Bottom, 20);
rect2.AppendMinLeft(purpleRect, AnchorParam.Bottom, 20);

rect2.SetBottomAndOpposite(rect1, AnchorParam.Right, -10);
rect2.SetStarHeight(1);

var rect3 = view.CreateRect("3");
rect3.SetAngle(rect2, 90);
rect3.SetTop(marginRect, AnchorParam.Bottom, 40);
rect3.AppendMaxBottom(redRect, AnchorParam.Bottom, -20);
rect3.AppendMaxBottom(blueRect, AnchorParam.Bottom, -20);
rect3.AppendMaxBottom(orangeRect, AnchorParam.Bottom, -20);
rect3.AppendMaxBottom(purpleRect, AnchorParam.Bottom, -20);

rect3.SetRightAndOpposite(rect2, AnchorParam.Top, -10);
rect3.SetStarWidth(1);

var rect4 = view.CreateRect("4");
rect4.SetAngle(rect3, 90);
rect4.SetLeft(marginRect, AnchorParam.Bottom, 40);
rect4.AppendMaxRight(redRect, AnchorParam.Bottom, -20);
rect4.AppendMaxRight(blueRect, AnchorParam.Bottom, -20);
rect4.AppendMaxRight(orangeRect, AnchorParam.Bottom, -20);
rect4.AppendMaxRight(purpleRect, AnchorParam.Bottom, -20);

rect4.SetTopAndOpposite(rect3, AnchorParam.Left, 10);
rect4.SetStarHeight(1);

var rect5 = view.CreateRect("5");
rect5.SetAngle(rect4, 90);
rect5.SetBottom(marginRect, AnchorParam.Bottom, -40);
rect5.AppendMinTop(redRect, AnchorParam.Bottom, 20);
rect5.AppendMinTop(blueRect, AnchorParam.Bottom, 20);
rect5.AppendMinTop(orangeRect, AnchorParam.Bottom, 20);
rect5.AppendMinTop(purpleRect, AnchorParam.Bottom, 20);

rect5.SetLeftAndOpposite(rect4, AnchorParam.Bottom, 10);
rect5.SetStarWidth(1);
rect5.SetRight(purpleRect, AnchorParam.HorizontalCenter);

host.PerformLayout();

DrawRect(marginRect, Color.Green);
DrawRect(redRect, Color.Red);
DrawRect(blueRect, Color.Blue);
DrawRect(orangeRect, Color.Orange);
DrawRect(purpleRect, Color.Purple);

DrawRect(rect1, Color.BurlyWood);
DrawRect(rect2, Color.YellowGreen);
DrawRect(rect3, Color.PaleVioletRed);
DrawRect(rect4, Color.DimGray);
DrawRect(rect5, Color.CornflowerBlue);

void DrawRect(LayoutRect rect, Color color)
{
    g.Transform = rect.Transform.Multiply(baseTransform);
    g.DrawRectangle(rect.AsRectF(), new Pen(color));
    g.DrawString((string)rect.Tag, fmt, new PointF(3, 0));
}

g.Transform = baseTransform;
doc.Save("doc4.pdf");


This sample creates a PDF document like this:

.NET API Layout


The interesting part of the code begins with creating rect1:

var rect1 = view.CreateRect("1");
rect1.SetBottom(marginRect, AnchorParam.Bottom, -40);
rect1.AppendMinTop(redRect, AnchorParam.Bottom, 20);
rect1.AppendMinTop(blueRect, AnchorParam.Bottom, 20);
rect1.AppendMinTop(orangeRect, AnchorParam.Bottom, 20);
rect1.AppendMinTop(purpleRect, AnchorParam.Bottom, 20);

rect1.SetLeft(redRect, AnchorParam.HorizontalCenter);
rect1.SetStarWidth(1);


It has a fixed offset from the bottom and multiple constraints specifying the minimum Y position of the top side. We might be not aware which of the previously drawn rectangles has the maximum Y coordinate of the bottom side. So, we append a separate MinTop constraint for each of those rectangles.


The second rectangle is rotated 90 degrees:

var rect2 = view.CreateRect("2");
rect2.SetAngle(rect1, 90);
rect2.SetRight(marginRect, AnchorParam.Bottom, -40);
rect2.AppendMinLeft(redRect, AnchorParam.Bottom, 20);
rect2.AppendMinLeft(blueRect, AnchorParam.Bottom, 20);
rect2.AppendMinLeft(orangeRect, AnchorParam.Bottom, 20);
rect2.AppendMinLeft(purpleRect, AnchorParam.Bottom, 20);

rect2.SetBottomAndOpposite(rect1, AnchorParam.Right, -10);
rect2.SetStarHeight(1);


All parameters are set taking the rotated point of view into account. The right side is at the bottom and has a fixed offset. The left side is at the top and has multiple constraints specifying the minimal offset from the old rectangles. We set the bottom chained constraint and the opposite constraint for the right side of rect1. Also, we have to set the star height instead of the star width because our chain has a horizontal direction which is considered as vertical from the rotated point of view.


The third rectangle is one of the most complex ones:

var rect3 = view.CreateRect("3");
rect3.SetAngle(rect2, 90);
rect3.SetTop(marginRect, AnchorParam.Bottom, 40);
rect3.AppendMaxBottom(redRect, AnchorParam.Bottom, -20);
rect3.AppendMaxBottom(blueRect, AnchorParam.Bottom, -20);
rect3.AppendMaxBottom(orangeRect, AnchorParam.Bottom, -20);
rect3.AppendMaxBottom(purpleRect, AnchorParam.Bottom, -20);

rect3.SetRightAndOpposite(rect2, AnchorParam.Top, -10);
rect3.SetStarWidth(1);


It is rotated 90 degrees relative to the second rectangle, which is already rotated 90 degrees. The overall angle is 180 degrees. Its top is at the bottom, and the bottom is at the top. The direction of the X and Y coordinate axes is opposite to their normal direction.


So, we append multiple MaxBottom constraints instead of MinTop ones. Also, we set the right chained constraint relative to the top side of rect2. If you take a look at the image, you can see the right side of rect3 follows the top side of rect2. That’s a tricky point.


Other rectangles (rect4 and rect5) have similar settings.

Anchor Points

All previously mentioned constraints were set relative to LayoutRects, the owner LayoutView, or as absolute values. Anchor points are one more option. Such points are convenient for setting relative positions instead of absolute ones. For example, let’s modify the first sample to display a red rectangle between 25% and 75% of the margin rectangle width and height:

using System.Drawing;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Layout;
using GrapeCity.Documents.Pdf;

const float pageWidth = 600;
const float pageHeight = 400;

var doc = new GcPdfDocument();
var page = doc.NewPage();
page.Size = new SizeF(pageWidth, pageHeight);
var g = page.Graphics;
var baseTransform = g.Transform;

var host = new LayoutHost();
var view = host.CreateView(pageWidth, pageHeight);

var marginRect = view.CreateRect();
marginRect.AnchorDeflate(null, 36);

var ap1 = marginRect.CreatePoint(0.25f, 0.25f);
var ap2 = marginRect.CreatePoint(0.75f, 0.25f);
var ap3 = marginRect.CreatePoint(0.25f, 0.75f);
var ap4 = marginRect.CreatePoint(0.75f, 0.75f);

var r1 = view.CreateRect();
AnchorCenter(r1, ap1);

var r2 = view.CreateRect();
AnchorCenter(r2, ap2);

var r3 = view.CreateRect();
AnchorCenter(r3, ap3);

var r4 = view.CreateRect();
AnchorCenter(r4, ap4);

void AnchorCenter(LayoutRect r, AnchorPoint ap)
{
    r.SetHorizontalCenter(ap);
    r.SetVerticalCenter(ap);
    r.SetWidth(10);
    r.SetHeight(10);
}

var redRect = view.CreateRect();
redRect.SetLeft(ap1);
redRect.SetTop(ap1);
redRect.SetRight(ap4);
redRect.SetBottom(ap4);

host.PerformLayout();

DrawRect(marginRect, Color.Green);
DrawRect(r1, Color.CornflowerBlue);
DrawRect(r2, Color.CornflowerBlue);
DrawRect(r3, Color.CornflowerBlue);
DrawRect(r4, Color.CornflowerBlue);
DrawRect(redRect, Color.Red);

void DrawRect(LayoutRect rect, Color color)
{
    g.Transform = rect.Transform.Multiply(baseTransform);
    g.DrawRectangle(rect.AsRectF(), new Pen(color));
}

g.Transform = baseTransform;
doc.Save("doc5.pdf");


The doc5.pdf looks like this:

.NET API Layout


An anchor point can be created from a LayoutRect or LayoutView:

/// <summary>
/// Represents a rectangle with constraints.
/// </summary>
public class LayoutRect
{
    /// <summary>
    /// Creates a point to be used as an anchor for other <see cref="LayoutRect"/>s.
    /// </summary>
    /// <param name="widthFactor">The value to be multiplied by <see cref="Width"/>
    ///   before adding to the position of the left side.</param>
    /// <param name="heightFactor">The value to be multiplied by <see cref="Height"/>
    ///   before adding to the position of the top side.</param>
    /// <param name="leftOffset">The value to be added to the position of the left side.</param>
    /// <param name="topOffset">The value to be added to the position of the top side.</param>
    /// <returns>The created <see cref="AnchorPoint"/> object.</returns>
    public AnchorPoint CreatePoint(float widthFactor, float heightFactor,
        float leftOffset = 0f, float topOffset = 0f)
}

/// <summary>
/// Represents a transformed surface with a set of <see cref="LayoutRect"/> objects.
/// </summary>
public class LayoutView
{
    /// <summary>
    /// Creates a point associated with the <see cref="LayoutView"/> to be used as
    /// an anchor for <see cref="LayoutRect"/>s.
    /// </summary>
    /// <param name="widthFactor">The value to be multiplied by <see cref="Width"/>
    ///   before adding to the position of the left side.</param>
    /// <param name="heightFactor">The value to be multiplied by <see cref="Height"/>
    ///   before adding to the position of the top side.</param>
    /// <param name="leftOffset">The value to be added to the position of the left side.</param>
    /// <param name="topOffset">The value to be added to the position of the top side.</param>
    /// <returns>The created <see cref="AnchorPoint"/> object.</returns>
    public AnchorPoint CreatePoint(float widthFactor, float heightFactor,
        float leftOffset = 0f, float topOffset = 0f)
}


One of the remarkable features of anchor points is that constraints can reference the points created in other LayoutViews (for example, with a different transformation matrix). That’s not the case for regular parameters of LayoutRects. You cannot create a constraint based on a LayoutRect from another LayoutView.


However, you can do that if you create an anchor point based on that “foreign” rectangle. The point can be referenced from constraints assigned to any LayoutRect within the same LayoutHost (but you should avoid creating circular references of any kind to prevent throwing a LayoutException).


For example, let’s create an image with two LayoutViews:

using System.Drawing;
using System.Numerics;
using GrapeCity.Documents.Common;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Imaging;
using GrapeCity.Documents.Layout;

var host = new LayoutHost();

var view1 = host.CreateView(10, 10, Matrix.CreateRotation(Math.PI / 6));
var rc1 = view1.CreateRect();
rc1.AnchorTopLeft(null, -20, 220, 300, 200);

var ap1 = rc1.CreatePoint(0, 0);
var ap2 = rc1.CreatePoint(1, 0);
var ap3 = rc1.CreatePoint(1, 1);
var ap4 = rc1.CreatePoint(0, 1);

var view2 = host.CreateView(10, 10, Matrix.CreateRotation(-Math.PI / 9));
var rc2 = view2.CreateRect();

rc2.SetTop(ap1, -20);
rc2.SetRight(ap2, 20);
rc2.SetBottom(ap3, 20);
rc2.SetLeft(ap4, -20);

host.PerformLayout();

using var bmp = new GcBitmap(600, 550, true);
using var g = bmp.CreateGraphics(Color.White);
var m = Matrix3x2.CreateTranslation(20, 20);
var pen = new Pen(Color.Coral, 2);

DrawRect(rc1, Color.CornflowerBlue);
DrawRect(rc2, Color.Green);

DrawPoint(ap1);
DrawPoint(ap2);
DrawPoint(ap3);
DrawPoint(ap4);

void DrawRect(LayoutRect r, Color c)
{
    g.Transform = r.Transform.Multiply(m);
    g.DrawRectangle(r.AsRectF(), new Pen(c, 2));
}

void DrawPoint(AnchorPoint ap)
{
    g.Transform = ap.Transform.Multiply(m);
    g.DrawEllipse(new RectangleF(-5, -5, 10, 10), pen);
}

bmp.SaveAsPng("img1.png");


Now, we create a PNG image, not a PDF document. The layout engine works with any target graphics.

.NET API Layout


The blue rectangle creates four anchor points (one for each vertex). Its LayoutView is rotated 30 degrees clockwise. The green rectangle is created in a different LayoutView, which is rotated 20 degrees counterclockwise. We bind each side of the green rectangle to the anchor points created by the blur rectangle with some offset (20 pixels).

The Matrix Class

The layout engine uses the special Matrix class for all transformations. It is defined in the GrapeCity.Documents.Common namespace. That is a 3x2 matrix similar to the standard Matrix3x2 from the System.Numerics namespace. There are several distinctions from the standard Matrix3x2 however.


Matrix is a class, not a struct. So, for example, you can pass null in the transform parameter of the LayoutHost.CreateView method (the Matrix.Identity instance will be used in that case):

/// <summary>
/// Creates a new <see cref="LayoutView"/> and associates it with the <see cref="LayoutHost"/>.
/// </summary>
/// <param name="width">The width of the view rectangle.</param>
/// <param name="height">The height of the view rectangle.</param>
/// <param name="transform">The transformation matrix for the <see cref="LayoutView"/>.</param>
/// <param name="tag">The object that contains data about the <see cref="LayoutView"/>.</param>
/// <returns>The created <see cref="LayoutView"/> object.</returns>
public LayoutView CreateView(float width, float height, Matrix transform = null, object tag = null)


However, you cannot assign null, for example, to the LayoutView.Transform property. That throws an ArgumentNullException.


The elements of the Matrix have double precision vs. single precision Matrix3x2. The Matrix class is also used for processing SVG vector images. There is a suggestion for using double-precision matrices in the SVG specification. We follow the same recommendations in the layout engine.


The instances of the Matrix class are immutable, all elements are read-only. If you need to change the value of some element, create a new instance of the Matrix with modified elements.

Matrix works well with the standard Matrix3x2 struct. You can create a Matrix from Matrix3x2:

/// <summary>
/// Initializes a new instance of the <see cref="Matrix"/> class from <see cref="Matrix3x2"/>.
/// </summary>
/// <param name="m">The source <see cref="Matrix3x2"/>.</param>
public Matrix(Matrix3x2 m)


and convert the Matrix to a Matrix3x2:

/// <summary>
/// Creates a <see cref="Matrix3x2"/> from this <see cref="Matrix"/>.
/// </summary>
public Matrix3x2 ToMatrix3x2()


The most important method of the Matrix class is probably the one that makes it possible to multiply a Matrix by Matrix3x2:

/// <summary>
/// Determines the product of the current <see cref="Matrix"/> to <see cref="Matrix3x2"/>.
/// </summary>
/// <param name="rightMatrix">The second matrix to multiply.</param>
/// <returns>The product of the two matrices in a shape of <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 Multiply(Matrix3x2 rightMatrix)


For example, if we have a GcPdfGraphics (g) with some transformation (Matrix3x2), it's easy to move to some LayoutRect (rect) coordinate system with the code like this:

var baseTransform = g.Transform;
g.Transform = rect.Transform.Multiply(baseTransform);


It's not necessary to convert Matrix to Matrix3x2 and vice versa to do that.

Dependent LayoutViews

One LayoutHost can create multiple LayoutView objects. Their hierarchy is not necessarily flat (as in the case of LayoutRects). Some views can be nested in other views. When we update the parent LayoutView's transformation matrix, all child views transformations are recalculated.


The following sample creates a group of LayoutViews with relative transformations. We assign different transformation matrices to the base view (view1) and recalculate the layout.

using System.Drawing;
using System.Numerics;
using GrapeCity.Documents.Common;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Imaging;
using GrapeCity.Documents.Layout;

var host = new LayoutHost();

var view1 = host.CreateView(240, 300);
var rc1 = view1.CreateRect();
rc1.AnchorExact(null);

var view2 = host.CreateView(100, 150);
var rc2 = view2.CreateRect();
rc2.AnchorExact(null);

var view3 = host.CreateView(70, 50);
var rc3 = view3.CreateRect();
rc3.AnchorExact(null);

const double DegToRad = Math.PI / 180;

var m2 = Matrix.CreateRotation(DegToRad * 45);
view2.SetRelativeTransform(view1, m2.Translate(120, -100));

var m3 = Matrix.CreateRotation(DegToRad * -20);
view3.SetRelativeTransform(view2, m3.Translate(-23, 90));

host.PerformLayout();

using var bmp = new GcBitmap(850, 350, true);
using var g = bmp.CreateGraphics(Color.White);
var m = Matrix3x2.CreateTranslation(20, 20);

Draw();

view1.Transform = Matrix.CreateTranslation(350, 50).Scale(0.7).Rotate(DegToRad * 20);
host.PerformLayout();

Draw();

view1.Transform = Matrix.CreateTranslation(520, 200).Scale(0.8).Rotate(DegToRad * -70);
host.PerformLayout();

Draw();

void Draw()
{
    g.Transform = rc1.Transform.Multiply(m);
    g.DrawRectangle(rc1.AsRectF(), new Pen(Color.CornflowerBlue, 2));

    g.Transform = rc2.Transform.Multiply(m);
    g.DrawRectangle(rc2.AsRectF(), new Pen(Color.Orange, 2));

    g.Transform = rc3.Transform.Multiply(m);
    g.DrawRectangle(rc3.AsRectF(), new Pen(Color.Violet, 2));
}

bmp.SaveAsPng("img2.png");


As you can see, the dependent views are updated automatically:

.NET API Layout

Contour Constraints

Contour is a closed figure drawn through anchor points. It is created with the CreateContour method of a LayoutView. New points are added to the contour with either of the following methods:

/// <summary>
/// Adds one <see cref="AnchorPoint"/> to the <see cref="Contour"/>.
/// </summary>
/// <param name="anchorPoint">
///   The <see cref="AnchorPoint"/> to be added. The owner <see cref="LayoutRect"/>
///   must belong to the same <see cref="LayoutView"/>.
/// </param>
public void AddPoint(AnchorPoint anchorPoint)

/// <summary>
/// Adds all <see cref="AnchorPoint"/>s from the collection to the <see cref="Contour"/>.
/// </summary>
/// <param name="collection">
///   <see cref="AnchorPoint"/>s to be added. The owner <see cref="LayoutRect"/>s
///   must belong to the same <see cref="LayoutView"/>.
/// </param>
public void AddPoints(IEnumerable<AnchorPoint> collection)


Anchor points from other LayoutViews are not supported in contours.


One (and only one) side of a LayoutRect can be bound to one or several contours from the same or different LayoutView. The following methods of the LayoutRect class help with creating contour constraints:

/// <summary>
/// Adds a MinLeft constraint relative to <paramref name="contour"/>.
/// </summary>
/// <param name="contour">The referenced <see cref="Contour"/>.</param>
/// <param name="contourPosition">The position of anchor for the contour constraint.</param>
/// <returns>New constraint just added.</returns>
public ContourConstraint AppendMinLeft(Contour contour, ContourPosition contourPosition)

/// <summary>
/// Adds a MaxRight constraint relative to <paramref name="contour"/>.
/// </summary>
/// <param name="contour">The referenced <see cref="Contour"/>.</param>
/// <param name="contourPosition">The position of anchor for the contour constraint.</param>
/// <returns>New constraint just added.</returns>
public ContourConstraint AppendMaxRight(Contour contour, ContourPosition contourPosition)

/// <summary>
/// Adds a MinTop constraint relative to <paramref name="contour"/>.
/// </summary>
/// <param name="contour">The referenced <see cref="Contour"/>.</param>
/// <param name="contourPosition">The position of anchor for the contour constraint.</param>
/// <returns>New constraint just added.</returns>
public ContourConstraint AppendMinTop(Contour contour, ContourPosition contourPosition)

/// <summary>
/// Adds a MaxBottom constraint relative to <paramref name="contour"/>.
/// </summary>
/// <param name="contour">The referenced <see cref="Contour"/>.</param>
/// <param name="contourPosition">The position of anchor for the contour constraint.</param>
/// <returns>New constraint just added.</returns>
public ContourConstraint AppendMaxBottom(Contour contour, ContourPosition contourPosition)


The contourPosition parameter specifies the position of anchor for the contour constraint. In other words, which condition is searched for in the contour for binding the target parameter of a LayoutRect. The following values are possible:



The next position at which the target LayoutRect ends to lie within the contour.


The first three values are “outside”. They specify a position where the rectangle begins or ends to intersect with the contour. The last two values are “inside”. They allow to placement of a rectangle inside the contour, so that it does not intersect with any contour lines.


For example, in the following image, rectangles outside the contour are colored blue, rectangles that intersect any contour lines are violet, and rectangles inside the contour are orange. The contour itself is green. Please note that we don’t get these areas automatically. For example, we can detect the start and end of the contour’s inside area only if we create the appropriate constraints for that purpose exactly.

.NET API Layout


The following code creates the above image.

using System.Drawing;
using System.Numerics;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Imaging;
using GrapeCity.Documents.Layout;

var host = new LayoutHost();
LayoutView view = host.CreateView(800, 400);

var contour = view.CreateContour();
contour.AddPoints(new AnchorPoint[]
{
    view.CreatePoint(0, 0, 400, 0),
    view.CreatePoint(0, 0, 600, 400),
    view.CreatePoint(0, 0, 400, 200),
    view.CreatePoint(0, 0, 200, 400)
});

// first row

var rc11 = view.CreateRect(); // Blue (Outside)
rc11.AnchorLeftTopBottom(null, 0, 20, 310);
rc11.AppendMaxRight(contour, ContourPosition.FirstInOutside);

var rc12 = view.CreateRect(); // Blue (Outside)
rc12.AnchorRightTopBottom(null, 0, 20, 310);
rc12.AppendMinLeft(contour, ContourPosition.FirstInOutside);

// second row

var rc21 = view.CreateRect(); // Blue (Outside)
rc21.AnchorLeftTopBottom(null, 0, 120, 210);
rc21.AppendMaxRight(contour, ContourPosition.FirstInOutside);

var rc22 = view.CreateRect(); // Violet (Intersection)
rc22.AnchorTopBottom(null, 120, 210);
rc22.SetLeft(rc21, AnchorParam.Right);
rc22.AppendMaxRight(contour, ContourPosition.FirstInInside);

var rc23 = view.CreateRect(); // Orange (Inside)
rc23.AnchorTopBottom(null, 120, 210);
rc23.SetLeft(rc22, AnchorParam.Right);
rc23.AppendMaxRight(contour, ContourPosition.NextOutInside);

var rc24 = view.CreateRect(); // Violet (Intersection)
rc24.AnchorTopBottom(null, 120, 210);
rc24.SetLeft(rc23, AnchorParam.Right);
rc24.AppendMaxRight(contour, ContourPosition.NextOutOutside);

var rc25 = view.CreateRect(); // Blue (Outside)
rc25.SetLeft(rc24, AnchorParam.Right);
rc25.AnchorRightTopBottom(null, 0, 120, 210);

// third row

var rc31 = view.CreateRect(); // Blue (Outside)
rc31.AnchorRightTopBottom(null, 0, 220, 110);
rc31.AppendMinLeft(contour, ContourPosition.FirstInOutside);

var rc32 = view.CreateRect(); // Violet (Intersection)
rc32.AnchorTopBottom(null, 220, 110);
rc32.SetRight(rc31, AnchorParam.Left);
rc32.AppendMinLeft(contour, ContourPosition.LastOutOutside);

var rc33 = view.CreateRect(); // Blue (Outside)
rc33.SetRight(rc32, AnchorParam.Left);
rc33.AnchorLeftTopBottom(null, 0, 220, 110);

// fourth row

var rc41 = view.CreateRect(); // Blue (Outside)
rc41.AnchorLeftTopBottom(null, 0, 320, 10);
rc41.AppendMaxRight(contour, ContourPosition.FirstInOutside);

var rc42 = view.CreateRect(); // Violet (Intersection)
rc42.AnchorTopBottom(null, 320, 10);
rc42.SetLeft(rc41, AnchorParam.Right);
rc42.AppendMaxRight(contour, ContourPosition.NextOutOutside);

var rc43 = view.CreateRect(); // Blue (Outside)
rc43.AnchorTopBottom(null, 320, 10);
rc43.SetLeft(rc42, AnchorParam.Right);
rc43.AppendMaxRight(contour, ContourPosition.FirstInOutside);

var rc44 = view.CreateRect(); // Violet (Intersection)
rc44.AnchorTopBottom(null, 320, 10);
rc44.SetLeft(rc43, AnchorParam.Right);
rc44.AppendMaxRight(contour, ContourPosition.NextOutOutside);

var rc45 = view.CreateRect(); // Blue (Outside)
rc45.SetLeft(rc44, AnchorParam.Right);
rc45.AnchorRightTopBottom(null, 0, 320, 10);

host.PerformLayout();

using var bmp = new GcBitmap((int)(view.Width + 40), (int)(view.Height + 40), true);
using var g = bmp.CreateGraphics(Color.White);
var m = Matrix3x2.CreateTranslation(20, 20);
g.Transform = m;

DrawContour(contour);

DrawRect(rc11, Color.CornflowerBlue);
DrawRect(rc12, Color.CornflowerBlue);

DrawRect(rc21, Color.CornflowerBlue);
DrawRect(rc22, Color.Violet);
DrawRect(rc23, Color.Orange);
DrawRect(rc24, Color.Violet);
DrawRect(rc25, Color.CornflowerBlue);

DrawRect(rc31, Color.CornflowerBlue);
DrawRect(rc32, Color.Violet);
DrawRect(rc33, Color.CornflowerBlue);

DrawRect(rc41, Color.CornflowerBlue);
DrawRect(rc42, Color.Violet);
DrawRect(rc43, Color.CornflowerBlue);
DrawRect(rc44, Color.Violet);
DrawRect(rc45, Color.CornflowerBlue);

void DrawContour(Contour co)
{
    var pts = co.Points.Select(ap => ap.TransformedLocation).ToArray();
    g.DrawPolygon(pts, new Pen(Color.Green, 2));
}

void DrawRect(LayoutRect r, Color c)
{
    g.Transform = r.Transform.Multiply(m);
    g.DrawRectangle(r.AsRectF(), new Pen(c, 2));
}

bmp.SaveAsPng("img3.png");


To learn more about GrapeCity Documents for PDF (GcPdf) and GrapeCity Documents for Imaging (GcImaging), review our documentation to see the many available features, and check out our demos to see the features in action with downloadable sample projects.


Also published here.