Back to Website
Show / Hide Table of Contents

Personalized rendering

  • 17 minutes to read

Design Editor provides a Web API to personalize products and render both proof images and hi-res outputs without loading the editor. This API works based on HTTPS requests and is handled by the Preview and HiRes controllers. It has the following functionality:

Function Request type URL Description
Generate proof images POST ~/api/Preview/GeneratePreview Populates a product with personal data, applies color themes, and generates proof images of the product.
Generate hi-res outputs POST ~/api/HiRes/GenerateHiRes Populates a product with personal data, applies color themes, and generates a new state file with these changes in the user's folder. After the state file is created, this method renders high-resolution outputs.

As a result, the Preview controller generates temporary URLs that link to proof images. The HiRes controller generates state files and URLs that link to high-resolution outputs. Optionally, this controller generates URLs that link to proof images. The response contains an array of the links grouped by products, their surfaces, and mockups: Array[products][surfaces][mockups].

The Preview and HiRes controllers operate in the same way as the Editor.getProofImages and Editor.finishProductDesign methods.

Warning

Every call of the GenerateHiRes method creates a new state file in the user's folder.

Headers

In the request header, these controllers require X-CustomersCanvasAPIKey - a unique API key defined in the AppSettings.config.

Request Payload

These controllers take the same product definitions as in the IFrame API, substitute personalized data, and return URLs that link to rendered products. You can render a single product or a product group all at once. You can pass the following common data params in the POST request:

  • productDefinitions defines a collection of products based on IProductDefinition or state files. Only this parameter is mandatory in the request payload.
  • itemsData sets the content and properties of design elements.
  • dataSet defines which surfaces from the productDefinitions should be rendered and how to apply itemsData to them.
  • productTheme applies product themes by using IProductThemeConfig.
  • productThemeName applies product themes by using a theme name.
  • userId sets a user ID.
  • previewOptions specifies the configuration of proof images.
  • hiResOptions specifies the parameters of print files. This parameter is only applicable to the HiRes controller.
  • containerSettings changes the texture or preview color of the specified container.
  • storeDataInState - if true, it saves personalization data to state files. This parameter is only applicable to the HiRes controller.
  • applyDataToVariablesOnly - if false, it applies personalization data to any field with the specified name; if true, the data applies only to fields marked as variable.

PreviewOptions

The previewOptions object defines the size, resizing mode, and rendering configuration of proof images through the following properties.

Property Possible values Default values
maxWidth number 500
maxHeight number 500
resizeMode "Fit", "Fill" "Fit"
surfaces array of indexes []
proofImageRendering IProofImage are taken from clientConfig.json
generateProductProof boolean false
productProofPageIndexes array of indexes []
pregeneratePreviewImages boolean false
generateLargePreviews boolean false
largePreviewMaxWidth number actual surface width
largePreviewMaxHeight number actual surface height

The surfaces array contains zero-based indexes of pages for which you need to render a proof image. If this array is empty or undefined, then the controller generates links to proof images for all product pages.

The generateProductProof property allows you to output previews of all product pages to a multi-page PDF file. In this case, proofImageRendering.fileFormat is ignored. If only a few of the pages are required, you can specify their indexes in the productProofPageIndexes array. Only these pages will be included in the output PDF file. You can specify comma-separated numbers as well as page ranges (two numbers separated by a dash "-") in any order.

You can set pregeneratePreviewImages to true to start generating proof images in the background when running this method. By default, the controller only returns links to proof images and starts generating these images when a request to download them is received.

To render real-size proof images, set generateLargePreviews to true. This option allows you to specify the largePreviewMaxWidth and largePreviewMaxHeight parameters to render images in a different size than the actual one.

For example, you can pass these parameters as follows:

var inputData = {
    previewOptions: {
        maxWidth: 504,
        maxHeight: 360,
        surfaces: [0, 1],
        resizeMode: "Fill",
        proofImageRendering: {
            fileFormat: "jpeg",
            rgbColorProfileName: "Adobe RGB (1998)",
            showStubContent: true
        }
    },
    ...
};

HiResOptions

The hiResOptions object allows you to configure the rendering of hi-res outputs, enable the output to separate files, and define whether the controller generates URLs that link to proof images through the following properties.

You can also use the pregenerateHiRes option to start generating hi-res outputs in the background when this endpoint is called. In this case, the resulting links to print files will be returned before the image generation is complete. By default, this option is false, and this endpoint only returns links and starts generating print files when a request to download them is received.

The pdfRenderKeepOriginalColors allow you to disable color management and maintain original colors in source templates and assets. If true, this parameter does not embed any color profile into the resulting PDF file. That allows you to preserve the original colors of source templates and assets, not just across the entire instance, but also for a single rendering operation. If not defined, the same parameter from Aurigma.DesignAtoms.config will be applied for the entire instance.

Property Possible values Default values
pregenerateHiRes boolean false
renderProofImages boolean false
renderingConfig.hiResOutputToSeparateFiles boolean false
renderingConfig.defaultHiResOutputRendering IHiResOutput are taken from clientConfig.json
renderingConfig.pdfRenderKeepOriginalColors boolean false

You can define these parameters as follows:

var inputData = {
    hiResOptions: {
        pregenerateHiRes: true,
        renderProofImages: true,
        renderingConfig:{
            hiResOutputToSeparateFiles: true,
            defaultHiResOutputRendering: {
                fileFormat: "pdf",
                dpi: 400
            }
        }
    },
    ...
};

ItemsData

The itemsData object allows you to apply properties to all product pages at once and separately on a per-page basis. Also, the itemsData object personalizes in-string and interpolation placeholders through the placeholders property. This object complies with the following specification.

itemsData: {
    surfaces: {
        [surfaceName: string]: {
            [itemName: string]: itemData;
        }
    },
    [itemName: string]: itemData,
    placeholders: {
        [placeholderName: string]: string;
    }
}

Here, itemData allows you to configure the following properties.

itemData: {
  image?: string;        // The path to images.
  content?: ItemData;    // The content of placeholders.
  contentResizeMode?: string;    // The image insertion mode.
  contentTransform?: Transforms; // Applied transformations.
  ignoreExistingTransform?: boolean; // Whether to apply transforms to content.
  opacity?: number;      // The opacity of design elements.
  text?: string;         // The content of text elements.
  leading?: number;      // The text leading.
  tracking?: number;     // The text tracking.
  font?: {
    postScriptName?: string; // The postScript name.
    size?: number;           // The font size in points.
    fauxBold?: boolean;      // Whether the faux bold style is applied.
    fauxItalic?: boolean;    // Whether the faux italic style is applied.
    allCaps?: boolean;       // Whether the AllCaps style is applied
  };
  textAlignment?: TextAlignment;
  textVerticalAlignment?: TextVerticalAlignment;
  isVertical?: boolean;  // Whether the text is vertical.
  underline?: boolean;   // Whether the text is underlined.
  visible?: boolean;     // Whether the element is rendered or not.
  location?: PointF;     // The starting point of the element.
  size?: ItemSize;       // The width and height.
  color?: string;        // The color in a CSS-compatible format.
  borderColor?: string;  // The border color of design elements.
  borderWidth?: number;  // The border width of design elements.
  fillColor?: string;    // The fill color for shapes.
  altColor?: string;     // The alternate color for dashed lines.
  dashWidth?: number;    // The dash width.
  altDashWidth?: number; // The width of alternate dash lines.
  width?: number;        // The width of lines.
  sourcePoint0?: PointF; // The start point of lines.
  sourcePoint1?: PointF; // The end point of lines.
  barcodeData?: IBarcodeData; // The data encoded into barcodes.
  shadow?: ShadowSettings;    // The shadow color.
  stroke?: StrokeSettings;    // The stroke color.
}

In text, you can pass plain text or an escaped XML string if you need to render tags. Instead, you can pass formattedText, which must comply with the format of the Aurigma Graphics Mill library. For example, "<p style='first-line-indent:20pt;'>Lorem <span style='bold:true;color:red;'>ipsum</span></p>". Note that an invalid format will result in an exception.

You can define text properties by using the IFontSettings interface. TextAlignment and TextVerticalAlignment allow you to align text elements. StrokeSettings and ShadowSettings define a color of the corresponding effect. Using PointF, you can define item coordinates.

When populating image placeholders, you can specify the image insertion mode and transforms that should be applied to the placeholder's content. The contentResizeMode property can be either "fit", "fill", or "original". You can apply the following transforms:

Transforms: {
  angle?: number;      // The rotation angle in degrees.
  scaleX?: number;     // The X-coordinate scaling factor.
  scaleY?: number;     // The Y-coordinate scaling factor.
  translateX?: number; // The X-coordinate movement in points.
  translateY?: number; // The Y-coordinate movement in points.
}

When ignoreExistingTransform is true, which is the default value, then the placeholder's content is first resized according to the contentResizeMode. After that, transformations defined in the contentTransform are applied to the content. When you set ignoreExistingTransform to false, then the contentResizeMode won't be applied even if it is defined, and the content will stretch across the entire placeholder, taking into account its proportions.

You can specify the content of barcode placeholders by using the IBarcodeData interface.

To set up the size of design elements, use ItemSize. For text elements, this object defines the bounding rectangle.

ItemSize: {
  width: number;
  height: number;
}

All properties of itemsData are optional. Through different variations, you can define any design element supported in the Design Editor. The following table details the design elements and their properties that you can pass to the Preview and HiRes controllers.

Images location, size, image, opacity, borderWidth, borderColor, visible
Image placeholders location, size, image, opacity, borderWidth, borderColor, content, contentResizeMode, contentTransform, ignoreExistingTransform, visible
Barcode placeholders color, location, size, barcodeData, opacity, borderWidth, borderColor, visible
Shapes, ellipses, rectangles location, size, fillColor, opacity, borderWidth, borderColor, visible
Lines color, width, opacity, sourcePoint0, sourcePoint1, visible
Plain text location, text, font, underline, color, isVertical, textAlignment, leading, tracking, stroke, shadow, visible
Curved text location, size, text, font, underline, color, textAlignment, leading, tracking, stroke, shadow, visible
Bounded text location, size, text, font, underline, color, isVertical, textAlignment, textVerticalAlignment, leading, tracking, stroke, shadow, visible
Path-bounded text, auto-scaled text location, size, text, font, underline, color, isVertical, textAlignment, leading, tracking, stroke, shadow, visible

All the numeric values are measured in points. If you do not pass any property that is appropriate to a design element, like text for bounded text, then the corresponding value from the design template remains unchanged.

DataSet

The dataSet object allows you to create a new product from the initial productDefinitions. When you pass a dataSet in the request payload, the Preview and HiRes controllers only render surfaces listed in the surfacesData collection.

dataSet: {
  surfacesData: SurfaceData[];
}

SurfaceData contains a list of required surfaces and data to personalize these surfaces.

SurfaceData: {
    surfaceBinding: SurfaceBinding;
    data: ItemsData[];
    iterateOverSurfacesFirst?: boolean;
}

Here, data defines one or more ItemsData sets for the personalization. In surfaceBinding, you can refer to the required surfaces by either indexes or names.

SurfaceBinding: {
    surfaceNames: string[];
    surfaceIndexes: number[];
}

For example, to personalize the Name field on the Front page and render only this page, you can provide the following input data.

var inputData = {
    "productDefinitions": [{
        "surfaces": { "designFolder": "presentation" }
    }],
    "dataSet": {
        "surfacesData": [{
            "surfaceBinding": {
                "surfaceNames": ["Front"]
            },
            "data": [{
                "Name": {
                    "text": "John Wood",
                    "color": "rgb(255, 0, 0)"
                }
            }]
        }]
    },
    ...
}

When you provide a number of data sets to render a number of surfaces, you can use the optional property iterateOverSurfacesFirst to specify the personalization order. If this property is true, then copies of the surfaces listed in the surfaceBinding are rendered in the following order:

  1. Render the first surface with the first data set.
  2. Render the first surface with the second data set.
  3. ... and so on for every data set.
  4. Render the second surface with the first data set.
  5. Render the second surface with the second data set.
  6. ... and so on for every surface and data set.

If iterateOverSurfacesFirst is false (this is the default value), then the personalization order goes over data sets:

  1. Render the first surface with the first data set.
  2. Render the second surface with the first data set.
  3. ... and so on for every surface.
  4. Render the first surface with the second data set.
  5. Render the second surface with the second data set.
  6. ... and so on for every surface and data set.

The Personalization Sample

Important

In this sample, the API security key is defined in JavaScript code. It could be highly insecure if it runs on a public site. However, you can use it this way in your admin panel, or just for demonstration purposes.

The following sample illustrates how you can generate personalized proof images without loading the Design Editor. Replace example.com in baseURL with the domain name of your site.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>The Proof Image Personalization Sample</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.2.0.js">
    </script>
    <script language="javascript">
    // Set a link to the Preview controller.
    var baseURL = "https://example.com/api/Preview/GeneratePreview";
    // Set a unique key for using the Web API.
    var apiKey = "UniqueSecurityKey";

    // Define a function for obtaining links to proof images.
    var getProofImages = function() {
        // Set up the inputData to personalize a layer containing the Name.
        var inputData = {
            itemsData: {
                "Name": {
                    text: "John Wood",
                    location: { x: 30, y: 30 },
                    font: {
                        postScriptName: "ArialMT",
                        size: 40.5,
                        fauxItalic: true
                    },
                    color: "rgb(255, 0, 100)",
                    textAlignment: "center"
                }
            },
            productDefinitions: [{
                surfaces: ["businesscard_front", "businesscard_back"]
            }],
            userId: "default"
        };

        // Make the request.
        $.ajax({
            url: baseURL,
            type: "POST",
            headers: { "X-CustomersCanvasAPIKey": apiKey },
            dataType: "json",
            contentType: "application/json; charset=utf-8",
            data: JSON.stringify(inputData)
        }).
        fail(function (d) { console.log(d.statusText); }).
        done(function (d) {
            // List the URLs of the proof images.
            d.forEach(function (products) {
                products.forEach(function (surfaces) {
                    surfaces.forEach(function (links) {
                        console.log(links);
                    })
                })
            });
        });
    }
    </script>
</head>

<body>
    <h3>Get Proof Images</h3>
    <input type="button" value="Get" onclick="getProofImages()" />
</body>
</html>

Examples of Input Data

productDefinitions allows you to specify one or more products through IProductDefinition, which, in turn, allows setting up the design location, mockups, spines, folding lines, crop marks, and safety lines. Here are several snippets illustrating possible use-cases and input data for these requests. You can copy and paste them into the previous personalization example.

A proof image of an 8.5x11-inch flyer

var inputData = {
    previewOptions: {
        maxWidth: 504,
        maxHeight: 360
    },
    productDefinitions: [{
        surfaces: [{
            designFile: "flyer",
            safetyLines: [{
                margin: 10,
                color: "green",
                altColor: "rgba(255,255,255,0)",
                stepPx: 5,
                widthPx: 1
            }],
            cropMarks: [{
                margin: 10,
                color: "red"
            }]
        }]
    }],
    userId: "default"
};

Personalization of in-string placeholders

The following example personalizes the [#Name] and [#Phone] in-string placeholders - the specially formatted strings.

var inputData = {
    itemsData: {
        placeholders: {
            "Name": "John Wood",
            "Phone": "555-123-1234"
        }
    },
    productDefinitions: [{
        surfaces: ["businesscard_front", "businesscard_back"]
    }],
    userId: "default"
};

Personalization of color themes and images

For images, you can specify URLs starting with http:// or https:// and links to your public and user images through the public: and user: prefixes, correspondingly. For example, public:company_logos/aurigma.png applies the aurigma.png logo from the \company\logos\ subfolder of the public gallery folder.

In the following example, we define a product by using state files, change the content of two image layers, and apply a color set to this product.

var inputData = {
    itemsData: {
        "photo": {
            image: "https://example.com/centralview.jpg",
            opacity: 0.8,
            borderWidth: 1
        },
        "cornerImage": {
            image: "user:tower.jpg",
            size: {
                width: 100,
                height: 100
            }
        }
    },
    productTheme: {
        "mainColor": "rgb(32, 83, 69)",
        "altColor": "rgb(26, 47, 41)",
        "texts": "rgb(176, 143, 37)"
    },
    productDefinitions: [
        "044abd5e-da18-46ff-9053-b019b62baa77",
        "d6c67467-6a3a-466a-9945-288c94b698a3"
    ],
    userId: "default"
};

Personalization of image placeholders with transforms

For image placeholders, you can specify how their content should be resized. Also, you can scale, translate, and rotate the content.

var inputData = {
    itemsData: {
        "logo": {
            image: "public:brand.svg",
            contentResizeMode: "original",
            contentTransform: {
                angle: -90
            }
        },
        "photo": {
            image: "https://example.com/castle.jpg",
            contentResizeMode: "fill",
            contentTransform: {
                angle: 0,
                scaleX: 0.5,
                scaleY: 0.5,
                translateX: 100,
                translateY: 100
            }
        }
    },
    productDefinitions: [{
        surfaces: ["postcard"]
    }],
    userId: "default"
};

Proof images of an envelope with preview mockups

In this example, we personalize only the front page of a two-page product.

var inputData = {
    itemsData: {
        surfaces: {
            "Front": {
                "name": { text: "John Wood" },
                "address": { text: "4414 Hill Road, Perth, IL" },
                "zip": { text: "61701" }
            }
        }
    },
    productDefinitions: [{
        surfaces: [{
            name: "Front",
            previewMockups: [{ up: "envelope" }],
            printAreas: [{
                designFile: "envelopeDesign",
                designLocation: { X: 4.1, Y: 4.1 } 
            }]
        },
        {
            name: "Back",
            printAreas: [{
                designFile: "envelopeBack"
            }]
        }]
    }],
    userId: "default"
};

Personalization using product-based templates

In this example, we create and personalize a product based on a multi-page IDML template. The first three pages of the created product are rendered as is, whereas the other pages are personalized based on the surfaces with indexes four and five. First, surface #4 is rendered with the big sketch, and then with the small sketch and contacts. The last two pages represent surface #5 rendered with the big sketch and then with the small one with contacts. As a result, you have a product with seven pages.

var inputData = {
    productDefinitions: [{
        surfaces: { file: "Booklet" }
    }],
    dataSet: {
        surfacesData: [
            {
                surfaceBinding: {
                    surfaceIndexes: [0,1,2]
                },
                data: [{}]
            },
            {
                surfaceBinding: {
                    surfaceIndexes: [4,5]
                },
                data: [
                    {
                        "Sketch": {
                            image: "https://example.com/big_sketch.png"
                        }
                    },
                    {
                        "Sketch": {
                            image: "https://example.com/small_sketch.png"
                        },
                        placeholders: {
                            "Phone": "1-555-123-4567",
                            "Email": "sales@example.com"
                        }
                    }
                ],
                iterateOverSurfacesFirst: true
            }
        ]
    },
    userId: "default"
}

Personalization of barcode placeholders with validation

This example illustrates how you can set up barcodes in the EAN-8 and EAN-13 formats and QR codes for URLs and phone numbers.

var inputData = {
    itemsData: {
        "Ean 8": {
            "barcodeData": {
                "BarcodeFormat": "EAN_8",
                "BarcodeValue": "1234567"
            }
        },
        "Ean 13": {
            "barcodeData": {
                "BarcodeFormat": "EAN_13",
                "BarcodeValue": "123456789012"
            }
        },
        "URL": {
            "barcodeData": {
                "BarcodeFormat": "QR_CODE",
                "BarcodeSubType": "Url",
                "Url": "https://example.com"
            }
        },
        "Phone": {
            "barcodeData": {
                "BarcodeFormat": "QR_CODE",
                "BarcodeSubType": "Phone",
                "Phone": "5551234567"
            }
        }
    },
    productDefinitions: [{
        surfaces: ["sticker"]
    }],
    userId: "default"
};

Before the personalization, you can validate the data to be pasted into barcode placeholders by using the following method.

$.ajax({
    url: "https://example.com/api/Barcode/ValidateBarcodeContent",
    type: "POST",
    headers: { "X-CustomersCanvasAPIKey": apiKey },
    dataType: "json",
    contentType: "application/json; charset=utf-8",
    data: JSON.stringify(
        {
            barcodeFormat: "EAN_13",
            barcodeValue: "1234567890123"
        }
    ).
    done(function (e) { console.log(e); });
});

This method returns an object implementing IValidationResult.

Personalization of interpolation placeholders at runtime

This example illustrates how you can get a list of variable items, including in-string and interpolation placeholders, and prepare a payload for these controllers at runtime. To make this example work, add the following text to the canvas:

Dear {{Name}}, your order #{{Order}} is ready.

Here, we define two interpolation placeholders: Name and Order. In this example, they take values from the content array.

var baseURL = "https://example.com/api/Preview/GeneratePreview";
var content = [
    { "Name": "John Wood", "Order": "555" },
    { "Name": "Cristopher Bennett", "Order": "123" }
];
// Save the product to get stateId at runtime. 
editor.saveProduct().then(function (result) {
    var stateId = result.stateId;
    editor.getProduct()
        .then(function (product) {
            // Get a list of variable items.
            return product.getVariableItems();
        }).then(function (items) {
            for (var i = 0; i < content.length; i++) {
                var placeholders = {};
                var itemsData = {};
                // Fill in the placeholders.
                items.forEach(function (item) {
                    placeholders[item.name] = (item.name in content[i]) ? content[i][item.name] : "";
                });

                itemsData.placeholders = placeholders;

                // Define inputData to personalize a copy of your product.
                var inputData = {
                    itemsData: itemsData,
                    productDefinitions: [stateId]
                };
                $.ajax({
                    url: baseURL,
                    type: "POST",
                    headers: { "X-CustomersCanvasAPIKey": "UniqueSecurityKey" },
                    dataType: "json",
                    contentType: "application/json; charset=utf-8",
                    data: JSON.stringify(inputData)
                }).
                    fail(function (d) { console.log(d.statusText); }).
                    done(function (d) {
                        d.forEach(function (surfaces) {
                            surfaces.forEach(function (surface) {
                                console.log(surface);
                            });
                        });
                    });
            }
        });
});

Responses

Success Response

The following list shows examples of URLs generated by the Preview and HiRes controllers. As a successful result, this API can return the following URLs:

  • The preview URL of a one-page product.

    Status Code: 200 OK
    Content: [[["https://example.com/api/rendering/GetProofImage/default/preview\_5603922333D0F6CB246F6AC20DC45552/0_0%5b504x360%5d.png"]]]
    
  • The hi-res URLs of two products.

    Status Code: 200 OK
    Content: [{"stateId":"408693E1872EC277EA28E90E481B2F72",
               "hiResUrls":["https://example.com/api/rendering/GetHiResOutput/default/408693E1872EC277EA28E90E481B2F72/-1_-1.pdf"]},
              {"stateId":"4C042925C4A39E45F886D67B0DE5B4E5",
               "hiResUrls":["https://example.com/api/rendering/GetHiResOutput/default/4C042925C4A39E45F886D67B0DE5B4E5/-1_-1.pdf"]}]
    
  • The URLs of proof images and hi-res outputs for the previous two products.

    Status Code: 200 OK
    Content: [{"proofImageUrls":[["https://example.com/api/rendering/GetProofImage/default/408693E1872EC277EA28E90E481B2F72/0_0.png"],
                                 ["https://example.com/api/rendering/GetProofImage/default/408693E1872EC277EA28E90E481B2F72/1_0.png"]],
               "stateId":"408693E1872EC277EA28E90E481B2F72",
               "hiResUrls":["https://example.com/api/rendering/GetHiResOutput/default/408693E1872EC277EA28E90E481B2F72/-1_-1.pdf"]},
              {"proofImageUrls":[["https://example.com/api/rendering/GetProofImage/default/4C042925C4A39E45F886D67B0DE5B4E5/0_0.png"],
                                 ["https://example.com/api/rendering/GetProofImage/default/4C042925C4A39E45F886D67B0DE5B4E5/1_0.png"],
                                 ["https://example.com/api/rendering/GetProofImage/default/4C042925C4A39E45F886D67B0DE5B4E5/2_0.png"]],
               "stateId":"4C042925C4A39E45F886D67B0DE5B4E5",
               "hiResUrls":["https://example.com/api/rendering/GetHiResOutput/default/4C042925C4A39E45F886D67B0DE5B4E5/-1_-1.pdf"]}]
    

Error Response

You can obtain the following error responses:

  • Status Code: 400 Bad Request
    Content: ProductDefinitions is required.
    
  • Status Code: 400 Bad Request
    Content: Image not found: https://example.com/centralview.jpg
    
  • Status Code: 403 Forbidden
    Content: Invalid Security Key
    
Was this page helpful?
Thanks for your feedback!
Back to top Copyright © 2001–2024 Aurigma, Inc. All rights reserved.
Loading...
    Thank for your vote
    Your opinion is important to us. To provide details, send feedback.
    Send feedback