ABC Positioning Catalog builder

To wrap up 2011 we will look at another advanced example of custom development for PageBuilder: ABC Positioning.

What is ABC Positioning? For each resolved product that's being added to a page or spread there will be added some extra information on another place of the page. The extra information needs to be visually linked to the resolved product. How to achieve this?

Introduction

In this sample we want to create a catalog where the image of each product is seperated from the product (metadata) itself. In other words, each page is divided in two: the textual information of the product and the image corresponding to each product. Maybe a screenshot will make it clear. Following image is a page taken from the resulting catalog.

This is a left page of the catalog. As you can see it contains 6 products (A-F). Each product contains a title, description and a table. This information was added using the out of the box PageBuilder functionality: the design template with basic textboxes and corresponding tags. The left column (images) are added using custom code. The letters (A-F) are there to link each image to a product. This is done by placing each letter in the upperleft corner of the resolved template and above each image. The numbering restarts for each page.

Templates

The image below is the layout of the Master Template. The left and the right page contains 3 columns, but only 2 of these columns are used to paste the products (the purple area, 'margins' in the master template). The green rectangle is preservered for the images we will add using custom code. With the out of the box PageBuilder, these green columns would stay empty.

The last screenshot is the design template. Again you can see three columns, this is only to make it easier to determine the width for this design. For this example the width of a product does not exceed the width of the columns in the master template. In this way each products' width fits perfectly in 1 column.
The three textboxes (CT_name, Ct_description, table) will be filled with data from the ADAM Database. The last textbox (upper left corner) only contains an X and is not tagged. Again, without the custom code, each product in the resulting catalog would have this box with an 'X' in it.

Creating the CatalogBuilder

To accomplish this we need a custom CatalogBuilder (we are creating a catalog), with a custom RecordPaginateAction (to update the letter 'X' and to add the image) and a custom PagePaginateAction (to be able to restart each page with the letter 'A').

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections.Generic;
using System.Linq;
using Adam.Core;
using Adam.Core.Records;
using Adam.DocMaker.Core;
using Adam.DocMaker.Core.Geometry;
using Adam.PageBuilder.Core.Build;
using Adam.PageBuilder.Core.Paginate;

namespace ABCPositioning
{
    internal class AbcCatalogBuilder : CatalogBuilder
    {
        public AbcCatalogBuilder(Application application) : base(application)
        {
            List<char> charactersUsedOnPage = new List<char>();
            PagePaginateAction = new AbcPagePaginateAction(application, charactersUsedOnPage);
            RecordPaginateAction = new AbcRecordPaginateAction(application, charactersUsedOnPage);
        }

        private class AbcRecordPaginateAction : RecordPaginateAction {...}

        private class AbcPagePaginateAction : PagePaginateAction {...}
    }
}

As you can see a charactersUsedOnPage is used in both Action constructors. In here, every character that will be used in a page will be added. In this way, both actions can access the same data.

Creating the PagePaginateAction

We'll start with the PagePaginateAction. The only thing that needs to be done here is to clear the characters list every time the page is full. This can be accomplished by overriding the OnPaginatedPage method.
Below you can find the code of the custom PagePaginateAction class.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private class AbcPagePaginateAction : PagePaginateAction
{
    private readonly IList <char> _charactersUsedOnPage;

    public AbcPagePaginateAction(Application application, IList<char> charactersUsedOnPage) 
        : base(application)
    {
        _charactersUsedOnPage = charactersUsedOnPage;
    }

    // Clear the characters list.
    protected override void OnPaginatedPage(PageTarget pageTarget)
    {
        _charactersUsedOnPage.Clear();
    }
}

Pretty simple no? Lets continue with the real stuff.

Creating the RecordPaginateAction

Let's take a look to the code in the custom RecordPaginateAction class.
As you can see, the constructor is equal to the one of our PagePaginateAction. _boxPadding is the amount of padding that is used when placing a new image into the green column. _characters is the available list of characters to be used for the items that are going to be placed on the page.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class AbcRecordPaginateAction : RecordPaginateAction
{
    private const int _boxPadding = 10;
    private const string _characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  
    private readonly IList<char> _charactersUsedOnPage;

    public AbcRecordPaginateAction(Application application, IList<char> charactersUsedOnPage)
     : base(application)
    {
        _charactersUsedOnPage = charactersUsedOnPage;
    }

    protected override void OnPlaced(PlaceTarget placeTarget) {...}

    private static void AddItemGroup(
        Document document, 
        TextElement abcElement, 
        FileVersion fileVersion)
    {...}
}

We are overriding the OnPlaced method, cause after an ItemGroup was added to the page, we want to update the letter 'X' and add the corresponding image. The following code snippet shows the OnPlaced method. The inline comments should be clear enough to see what's happening.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected override void OnPlaced(PlaceTarget placeTarget)
{
    // Find the first available character for current page.
    char currentCharacter = _characters.ToCharArray().First(c => !_charactersUsedOnPage.Contains(c));
    _charactersUsedOnPage.Add(currentCharacter);

    // Add the current character to the original placed item group.
    // Find the textElement: we can search on contents being equal to 'X'.
    // Then update the text to the current character.
    TextElement abcElement = 
        placeTarget.Elements.OfType<TextElement>().Single(e => e.Story.ToText() == "X");
    abcElement.Story.Paragraphs.Single().Items.OfType<Run>().Single().Value = currentCharacter.ToString();

    // Load the image record and add the image to our page.
    // The resolved record can be found using our placeTarget.
    Record imageRecord = new Record(abcElement.App);
    imageRecord.Load(((RecordPlaceTarget)placeTarget).ResolvedRecordId.Value);
    FileVersion fileVersion = imageRecord.Files.LatestMaster;
    if (fileVersion != null)
    {
        AddItemGroup(placeTarget.Document, abcElement, fileVersion);
    }
}

The only thing that remains now is to add our image (and a duplicate of the character element) to the correct place in our green column. Following snippet contains that code.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static void AddItemGroup(Document document, TextElement abcElement, FileVersion fileVersion)
{
    // Find the rectangle in the masterpage to place the image in.
    Page page = document.Pages.Last();
    Page masterPage = document.MasterSpreads.Last().Pages.Single(p => p.Side == page.Side);

    // Select the bounds of the green rectangle. 
    // The offset is used to let this work for both left and right pages.
    RectangleD bounds = masterPage.Elements
        .Select(element => element.Shape.Bounds)
        .Single(b => b.Height > 500)
        .Offset(page.Bounds.Left - masterPage.Bounds.Left, 0);

    // Find the elements already in the column, and get the bottom of the bounding box.
    RectangleD columnBounds = bounds;
    double offset = page.Elements
        .Select(e => e.Shape.Bounds)
        .Where(b => b.IntersectsWith(columnBounds))
        .Select(b => b.Bottom - columnBounds.Top)
        .OrderByDescending(b => b)
        .FirstOrDefault();

    // Add the boxPadding offset.
    bounds = columnBounds.Offset(_boxPadding, _boxPadding + offset);

    // Clone the text element containing the letter and move it to correct location.
    TextElement clonedAbcElement = document.CreateTextElement(abcElement);
    clonedAbcElement.MoveTo(bounds.Location);
    abcElement.OwnerSpread.Elements.Add(clonedAbcElement);

    // Calculate the size and location for the image.
    bounds = bounds.Offset(0, 20);
    double imageWidth = bounds.Width - (2 * _boxPadding);
    IReadOnlyImage image = fileVersion.GetPreview();
    SizeD imageSize = new SizeD(imageWidth, imageWidth * image.Height / image.Width);
    RectangleD imageBounds = new RectangleD(bounds.Location, imageSize);

    // Create the imageElement and add it to the document.
    ImageElement imageElement = document.CreateImageElement(imageBounds);
    imageElement.ReplaceImage(ReplaceImageSource.FileVersion, fileVersion.Id);
    abcElement.OwnerSpread.Elements.Add(imageElement);
}

This concludes our last PageBuilder blog post of the year. Hopefully we shared some cool custom development cases the last few months. We are eager to see some mind blowing custom creations from you guys out there!

Comments

Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed