All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.layout.renderer.AbstractRenderer Maven / Gradle / Ivy

There is a newer version: 9.0.0
Show newest version
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2024 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
 */
package com.itextpdf.layout.renderer;

import com.itextpdf.commons.datastructures.Tuple2;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.util.NumberUtil;
import com.itextpdf.kernel.colors.Color;
import com.itextpdf.kernel.colors.gradients.AbstractLinearGradientBuilder;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.canvas.CanvasArtifact;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.extgstate.PdfExtGState;
import com.itextpdf.kernel.pdf.navigation.PdfStructureDestination;
import com.itextpdf.kernel.pdf.tagging.PdfStructElem;
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.kernel.pdf.xobject.PdfXObject;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.IPropertyContainer;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Div;
import com.itextpdf.layout.element.IElement;
import com.itextpdf.layout.exceptions.LayoutExceptionMessageConstant;
import com.itextpdf.layout.font.FontCharacteristics;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.layout.font.FontSelector;
import com.itextpdf.layout.font.FontSet;
import com.itextpdf.layout.layout.LayoutArea;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutPosition;
import com.itextpdf.layout.layout.PositionedLayoutContext;
import com.itextpdf.layout.minmaxwidth.MinMaxWidth;
import com.itextpdf.layout.minmaxwidth.MinMaxWidthUtils;
import com.itextpdf.layout.properties.Background;
import com.itextpdf.layout.properties.BackgroundBox;
import com.itextpdf.layout.properties.BackgroundImage;
import com.itextpdf.layout.properties.BaseDirection;
import com.itextpdf.layout.properties.BlendMode;
import com.itextpdf.layout.properties.BorderRadius;
import com.itextpdf.layout.properties.BoxSizingPropertyValue;
import com.itextpdf.layout.properties.HorizontalAlignment;
import com.itextpdf.layout.properties.OverflowPropertyValue;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.properties.Transform;
import com.itextpdf.layout.properties.TransparentColor;
import com.itextpdf.layout.properties.UnitValue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Defines the most common properties and behavior that are shared by most
 * {@link IRenderer} implementations. All default Renderers are subclasses of
 * this default implementation.
 */
public abstract class AbstractRenderer implements IRenderer {
    public static final float OVERLAP_EPSILON = 1e-4f;


    /**
     * The maximum difference between {@link Rectangle} coordinates to consider rectangles equal
     */
    protected static final float EPS = 1e-4f;

    /**
     * The infinity value which is used while layouting
     */
    protected static final float INF = 1e6f;

    /**
     * The common ordering index of top side in arrays of four elements which define top, right, bottom,
     * left sides values (e.g. margins, borders, paddings).
     */
    static final int TOP_SIDE = 0;

    /**
     * The common ordering index of right side in arrays of four elements which define top, right, bottom,
     * left sides values (e.g. margins, borders, paddings).
     */
    static final int RIGHT_SIDE = 1;

    /**
     * The common ordering index of bottom side in arrays of four elements which define top, right, bottom,
     * left sides values (e.g. margins, borders, paddings).
     */
    static final int BOTTOM_SIDE = 2;

    /**
     * The common ordering index of left side in arrays of four elements which define top, right, bottom,
     * left sides values (e.g. margins, borders, paddings).
     */
    static final int LEFT_SIDE = 3;

    private static final int ARC_RIGHT_DEGREE = 0;
    private static final int ARC_TOP_DEGREE = 90;
    private static final int ARC_LEFT_DEGREE = 180;
    private static final int ARC_BOTTOM_DEGREE = 270;

    private static final int ARC_QUARTER_CLOCKWISE_EXTENT = -90;

    // For autoport
    private static final Tuple2 CHECK_TUPLE2_TYPE =
            new Tuple2("", new PdfDictionary());

    protected List childRenderers = new ArrayList<>();
    protected List positionedRenderers = new ArrayList<>();
    protected IPropertyContainer modelElement;
    protected boolean flushed = false;
    protected LayoutArea occupiedArea;
    protected IRenderer parent;
    protected Map properties = new HashMap<>();
    protected boolean isLastRendererForModelElement = true;

    /**
     * Creates a renderer.
     */
    protected AbstractRenderer() {
    }

    /**
     * Creates a renderer for the specified layout element.
     *
     * @param modelElement the layout element that will be drawn by this renderer
     */
    protected AbstractRenderer(IElement modelElement) {
        this.modelElement = modelElement;
    }

    /**
     * Creates a new renderer based on an instance of another renderer.
     *
     * @param other renderer from which to copy essential properties
     */
    protected AbstractRenderer(AbstractRenderer other) {
        this.childRenderers = other.childRenderers;
        this.positionedRenderers = other.positionedRenderers;
        this.modelElement = other.modelElement;
        this.flushed = other.flushed;
        this.occupiedArea = other.occupiedArea != null ? other.occupiedArea.clone() : null;
        this.parent = other.parent;
        this.properties.putAll(other.properties);
        this.isLastRendererForModelElement = other.isLastRendererForModelElement;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addChild(IRenderer renderer) {
        // https://www.webkit.org/blog/116/webcore-rendering-iii-layout-basics
        // "The rules can be summarized as follows:"...
        Integer positioning = renderer.getProperty(Property.POSITION);
        if (positioning == null || positioning == LayoutPosition.RELATIVE || positioning == LayoutPosition.STATIC) {
            childRenderers.add(renderer);
        } else if (positioning == LayoutPosition.FIXED) {
            AbstractRenderer root = this;
            while (root.parent instanceof AbstractRenderer) {
                root = (AbstractRenderer) root.parent;
            }
            if (root == this) {
                positionedRenderers.add(renderer);
            } else {
                root.addChild(renderer);
            }
        } else if (positioning == LayoutPosition.ABSOLUTE) {
            // For position=absolute, if none of the top, bottom, left, right properties are provided,
            // the content should be displayed in the flow of the current content, not overlapping it.
            // The behavior is just if it would be statically positioned except it does not affect other elements
            AbstractRenderer positionedParent = this;
            boolean noPositionInfo = AbstractRenderer.noAbsolutePositionInfo(renderer);
            while (!positionedParent.isPositioned() && !noPositionInfo) {
                IRenderer parent = positionedParent.parent;
                if (parent instanceof AbstractRenderer) {
                    positionedParent = (AbstractRenderer) parent;
                } else {
                    break;
                }
            }
            if (positionedParent == this) {
                positionedRenderers.add(renderer);
            } else {
                positionedParent.addChild(renderer);
            }
        }

        // Fetch positioned renderers from non-positioned child because they might be stuck there because child's parent was null previously
        if (renderer instanceof AbstractRenderer && !((AbstractRenderer) renderer).isPositioned() && ((AbstractRenderer) renderer).positionedRenderers.size() > 0) {
            // For position=absolute, if none of the top, bottom, left, right properties are provided,
            // the content should be displayed in the flow of the current content, not overlapping it.
            // The behavior is just if it would be statically positioned except it does not affect other elements
            int pos = 0;
            List childPositionedRenderers = ((AbstractRenderer) renderer).positionedRenderers;
            while (pos < childPositionedRenderers.size()) {
                if (AbstractRenderer.noAbsolutePositionInfo(childPositionedRenderers.get(pos))) {
                    pos++;
                } else {
                    positionedRenderers.add(childPositionedRenderers.get(pos));
                    childPositionedRenderers.remove(pos);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IPropertyContainer getModelElement() {
        return modelElement;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List getChildRenderers() {
        return childRenderers;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasProperty(int property) {
        return hasOwnProperty(property)
                || (modelElement != null && modelElement.hasProperty(property))
                || (parent != null && Property.isPropertyInherited(property) && parent.hasProperty(property));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasOwnProperty(int property) {
        return properties.containsKey(property);
    }

    /**
     * Checks if this renderer or its model element have the specified property,
     * i.e. if it was set to this very element or its very model element earlier.
     *
     * @param property the property to be checked
     * @return {@code true} if this instance or its model element have given own property, {@code false} otherwise
     */
    public boolean hasOwnOrModelProperty(int property) {
        return hasOwnOrModelProperty(this, property);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteOwnProperty(int property) {
        properties.remove(property);
    }

    /**
     * Deletes property from this very renderer, or in case the property is specified on its model element, the
     * property of the model element is deleted
     *
     * @param property the property key to be deleted
     */
    public void deleteProperty(int property) {
        if (properties.containsKey(property)) {
            properties.remove(property);
        } else {
            if (modelElement != null) {
                modelElement.deleteOwnProperty(property);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public  T1 getProperty(int key) {
        Object property;
        if ((property = properties.get(key)) != null || properties.containsKey(key)) {
            return (T1) property;
        }
        if (modelElement != null && ((property = modelElement.getProperty(key)) != null || modelElement.hasProperty(key))) {
            return (T1) property;
        }
        if (parent != null && Property.isPropertyInherited(key) && (property = parent.getProperty(key)) != null) {
            return (T1) property;
        }
        property = this.getDefaultProperty(key);
        if (property != null) {
            return (T1) property;
        }
        return modelElement != null ? modelElement.getDefaultProperty(key) : (T1) (Object) null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public  T1 getOwnProperty(int property) {
        return (T1) properties.get(property);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public  T1 getProperty(int property, T1 defaultValue) {
        T1 result = this.getProperty(property);
        return result != null ? result : defaultValue;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setProperty(int property, Object value) {
        properties.put(property, value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public  T1 getDefaultProperty(int property) {
        return (T1) (Object) null;
    }

    /**
     * Returns a property with a certain key, as a font object.
     *
     * @param property an {@link Property enum value}
     * @return a {@link PdfFont}
     */
    public PdfFont getPropertyAsFont(int property) {
        return this.getProperty(property);
    }

    /**
     * Returns a property with a certain key, as a color.
     *
     * @param property an {@link Property enum value}
     * @return a {@link Color}
     */
    public Color getPropertyAsColor(int property) {
        return this.getProperty(property);
    }

    /**
     * Returns a property with a certain key, as a {@link TransparentColor}.
     *
     * @param property an {@link Property enum value}
     * @return a {@link TransparentColor}
     */
    public TransparentColor getPropertyAsTransparentColor(int property) {
        return this.getProperty(property);
    }

    /**
     * Returns a property with a certain key, as a floating point value.
     *
     * @param property an {@link Property enum value}
     * @return a {@link Float}
     */
    public Float getPropertyAsFloat(int property) {
        return NumberUtil.asFloat(this.getProperty(property));
    }

    /**
     * Returns a property with a certain key, as a floating point value.
     *
     * @param property     an {@link Property enum value}
     * @param defaultValue default value to be returned if property is not found
     * @return a {@link Float}
     */
    public Float getPropertyAsFloat(int property, Float defaultValue) {
        return NumberUtil.asFloat(this.getProperty(property, defaultValue));
    }

    /**
     * Returns a property with a certain key, as a boolean value.
     *
     * @param property an {@link Property enum value}
     * @return a {@link Boolean}
     */
    public Boolean getPropertyAsBoolean(int property) {
        return this.getProperty(property);
    }

    /**
     * Returns a property with a certain key, as a unit value.
     *
     * @param property an {@link Property enum value}
     * @return a {@link UnitValue}
     */
    public UnitValue getPropertyAsUnitValue(int property) {
        return this.getProperty(property);
    }

    /**
     * Returns a property with a certain key, as an integer value.
     *
     * @param property an {@link Property enum value}
     * @return a {@link Integer}
     */
    public Integer getPropertyAsInteger(int property) {
        return NumberUtil.asInteger(this.getProperty(property));
    }

    /**
     * Returns a string representation of the renderer.
     *
     * @return a {@link String}
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (IRenderer renderer : childRenderers) {
            sb.append(renderer.toString());
        }
        return sb.toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public LayoutArea getOccupiedArea() {
        return occupiedArea;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void draw(DrawContext drawContext) {
        applyDestinationsAndAnnotation(drawContext);

        boolean relativePosition = isRelativePosition();
        if (relativePosition) {
            applyRelativePositioningTranslation(false);
        }

        beginElementOpacityApplying(drawContext);
        drawBackground(drawContext);
        drawBorder(drawContext);
        drawChildren(drawContext);
        drawPositionedChildren(drawContext);
        endElementOpacityApplying(drawContext);

        if (relativePosition) {
            applyRelativePositioningTranslation(true);
        }

        flushed = true;
    }

    /**
     * Apply {@code Property.OPACITY} property if specified by setting corresponding values in graphic state dictionary
     * opacity will be applied to all elements drawn after calling this method and before
     * calling {@link AbstractRenderer#endElementOpacityApplying(DrawContext)} ()}.
     *
     * @param drawContext the context (canvas, document, etc) of this drawing operation.
     */
    protected void beginElementOpacityApplying(DrawContext drawContext) {
        Float opacity = this.getPropertyAsFloat(Property.OPACITY);
        if (opacity != null && opacity < 1f) {
            PdfExtGState extGState = new PdfExtGState();
            extGState
                    .setStrokeOpacity((float) opacity)
                    .setFillOpacity((float) opacity);
            drawContext.getCanvas()
                    .saveState()
                    .setExtGState(extGState);
        }
    }

    /**
     * {@link AbstractRenderer#beginElementOpacityApplying(DrawContext)}.
     *
     * @param drawContext the context (canvas, document, etc) of this drawing operation.
     */
    protected void endElementOpacityApplying(DrawContext drawContext) {
        Float opacity = this.getPropertyAsFloat(Property.OPACITY);
        if (opacity != null && opacity < 1f) {
            drawContext.getCanvas().restoreState();
        }
    }

    /**
     * Draws a background layer if it is defined by a key {@link Property#BACKGROUND}
     * in either the layout element or this {@link IRenderer} itself.
     *
     * @param drawContext the context (canvas, document, etc) of this drawing operation.
     */
    public void drawBackground(DrawContext drawContext) {
        final Background background = this.getProperty(Property.BACKGROUND);
        final List  backgroundImagesList = this.>getProperty(Property.BACKGROUND_IMAGE);

        if (background != null || backgroundImagesList != null) {
            Rectangle bBox = getOccupiedAreaBBox();
            boolean isTagged = drawContext.isTaggingEnabled();
            if (isTagged) {
                drawContext.getCanvas().openTag(new CanvasArtifact());
            }
            Rectangle backgroundArea = getBackgroundArea(applyMargins(bBox, false));
            if (backgroundArea.getWidth() <= 0 || backgroundArea.getHeight() <= 0) {
                Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
                logger.info(MessageFormatUtil.format(
                        IoLogMessageConstant.RECTANGLE_HAS_NEGATIVE_OR_ZERO_SIZES, "background"));
            } else {
                boolean backgroundAreaIsClipped = false;
                if (background != null) {
                    // TODO DEVSIX-4525 determine how background-clip affects background-radius
                    final Rectangle clippedBackgroundArea = applyBackgroundBoxProperty(backgroundArea.clone(),
                            background.getBackgroundClip());
                    backgroundAreaIsClipped = clipBackgroundArea(drawContext, clippedBackgroundArea);
                    drawColorBackground(background, drawContext, clippedBackgroundArea);
                }
                if (backgroundImagesList != null) {
                    backgroundAreaIsClipped = drawBackgroundImagesList(backgroundImagesList, backgroundAreaIsClipped,
                            drawContext, backgroundArea);
                }
                if (backgroundAreaIsClipped) {
                    drawContext.getCanvas().restoreState();
                }
            }
            if (isTagged) {
                drawContext.getCanvas().closeTag();
            }
        }
    }

    private void drawColorBackground(Background background, DrawContext drawContext, Rectangle colorBackgroundArea) {
        final TransparentColor backgroundColor = new TransparentColor(background.getColor(),
                background.getOpacity());
        drawContext.getCanvas().saveState().setFillColor(backgroundColor.getColor());
        backgroundColor.applyFillTransparency(drawContext.getCanvas());
        drawContext.getCanvas().rectangle((double) colorBackgroundArea.getX() - background.getExtraLeft(),
                (double) colorBackgroundArea.getY() - background.getExtraBottom(),
                (double) colorBackgroundArea.getWidth() +
                        background.getExtraLeft() + background.getExtraRight(),
                (double) colorBackgroundArea.getHeight() +
                        background.getExtraTop() + background.getExtraBottom()).fill().restoreState();
    }

    private Rectangle applyBackgroundBoxProperty(Rectangle rectangle, BackgroundBox clip) {
        if (BackgroundBox.PADDING_BOX == clip) {
            applyBorderBox(rectangle, false);
        } else if (BackgroundBox.CONTENT_BOX == clip) {
            applyBorderBox(rectangle, false);
            applyPaddings(rectangle, false);
        }
        return rectangle;
    }

    private boolean drawBackgroundImagesList(final List backgroundImagesList,
                                             boolean backgroundAreaIsClipped, final DrawContext drawContext,
                                             final Rectangle backgroundArea) {
        for (int i = backgroundImagesList.size() - 1; i >= 0; i--) {
            final BackgroundImage backgroundImage = backgroundImagesList.get(i);
            if (backgroundImage != null && backgroundImage.isBackgroundSpecified()) {
                // TODO DEVSIX-4525 determine how background-clip affects background-radius
                if (!backgroundAreaIsClipped) {
                    backgroundAreaIsClipped = clipBackgroundArea(drawContext, backgroundArea);
                }
                drawBackgroundImage(backgroundImage, drawContext, backgroundArea);
            }
        }
        return backgroundAreaIsClipped;
    }

    private void drawBackgroundImage(BackgroundImage backgroundImage,
            DrawContext drawContext, Rectangle backgroundArea) {
        Rectangle originBackgroundArea = applyBackgroundBoxProperty(backgroundArea.clone(),
                backgroundImage.getBackgroundOrigin());
        float[] imageWidthAndHeight = BackgroundSizeCalculationUtil.calculateBackgroundImageSize(
                backgroundImage, originBackgroundArea.getWidth(), originBackgroundArea.getHeight());
        PdfXObject backgroundXObject = backgroundImage.getImage();
        if (backgroundXObject == null) {
            backgroundXObject = backgroundImage.getForm();
        }
        Rectangle imageRectangle;
        final UnitValue xPosition = UnitValue.createPointValue(0);
        final UnitValue yPosition = UnitValue.createPointValue(0);
        if (backgroundXObject == null) {
            final AbstractLinearGradientBuilder gradientBuilder = backgroundImage.getLinearGradientBuilder();
            if (gradientBuilder == null) {
                return;
            }
            // fullWidth and fullHeight is 0 because percentage shifts are ignored for linear-gradients
            backgroundImage.getBackgroundPosition().calculatePositionValues(0, 0, xPosition, yPosition);
            backgroundXObject = createXObject(gradientBuilder, originBackgroundArea, drawContext.getDocument());
            imageRectangle = new Rectangle(originBackgroundArea.getLeft() + xPosition.getValue(),
                    originBackgroundArea.getTop() - imageWidthAndHeight[1] - yPosition.getValue(),
                    imageWidthAndHeight[0], imageWidthAndHeight[1]);
        } else {
            backgroundImage.getBackgroundPosition().calculatePositionValues(
                    originBackgroundArea.getWidth() - imageWidthAndHeight[0],
                    originBackgroundArea.getHeight() - imageWidthAndHeight[1], xPosition, yPosition);
            imageRectangle = new Rectangle(originBackgroundArea.getLeft() + xPosition.getValue(),
                    originBackgroundArea.getTop() - imageWidthAndHeight[1] - yPosition.getValue(),
                    imageWidthAndHeight[0], imageWidthAndHeight[1]);
        }
        if (imageRectangle.getWidth() <= 0 || imageRectangle.getHeight() <= 0) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.info(MessageFormatUtil.format(
                    IoLogMessageConstant.RECTANGLE_HAS_NEGATIVE_OR_ZERO_SIZES,
                    "background-image"));
        } else {
            final Rectangle clippedBackgroundArea = applyBackgroundBoxProperty(backgroundArea.clone(),
                    backgroundImage.getBackgroundClip());
            drawContext.getCanvas()
                    .saveState()
                    .rectangle(clippedBackgroundArea)
                    .clip()
                    .endPath();
            drawPdfXObject(imageRectangle, backgroundImage, drawContext, backgroundXObject, backgroundArea,
                    originBackgroundArea);
            drawContext.getCanvas().restoreState();
        }
    }

    private static void drawPdfXObject(final Rectangle imageRectangle, final BackgroundImage backgroundImage,
            final DrawContext drawContext, final PdfXObject backgroundXObject,
            final Rectangle backgroundArea, Rectangle originBackgroundArea) {
        BlendMode blendMode = backgroundImage.getBlendMode();
        if (blendMode != BlendMode.NORMAL) {
            drawContext.getCanvas().setExtGState(new PdfExtGState().setBlendMode(blendMode.getPdfRepresentation()));
        }
        final Point whitespace = backgroundImage.getRepeat()
                .prepareRectangleToDrawingAndGetWhitespace(imageRectangle, originBackgroundArea,
                        backgroundImage.getBackgroundSize());
        final float initialX = imageRectangle.getX();
        int counterY = 1;
        boolean firstDraw = true;
        boolean isCurrentOverlaps;
        boolean isNextOverlaps;
        do {
            drawPdfXObjectHorizontally(imageRectangle,
                    backgroundImage, drawContext, backgroundXObject, backgroundArea, firstDraw, (float) whitespace.getX());
            firstDraw = false;
            imageRectangle.setX(initialX);
            isCurrentOverlaps = imageRectangle.overlaps(backgroundArea, OVERLAP_EPSILON);
            if (counterY % 2 == 1) {
                isNextOverlaps =
                        imageRectangle.moveDown((imageRectangle.getHeight() + (float) whitespace.getY()) * counterY)
                                .overlaps(backgroundArea, OVERLAP_EPSILON);
            } else {
                isNextOverlaps = imageRectangle.moveUp((imageRectangle.getHeight() + (float) whitespace.getY()) * counterY)
                        .overlaps(backgroundArea, OVERLAP_EPSILON);
            }
            ++counterY;
        } while (!backgroundImage.getRepeat().isNoRepeatOnYAxis() && (isCurrentOverlaps || isNextOverlaps));
    }

    private static void drawPdfXObjectHorizontally(Rectangle imageRectangle, BackgroundImage backgroundImage,
                                                   DrawContext drawContext, PdfXObject backgroundXObject,
                                                   Rectangle backgroundArea, boolean firstDraw, final float xWhitespace) {
        boolean isItFirstDraw = firstDraw;
        int counterX = 1;
        boolean isCurrentOverlaps;
        boolean isNextOverlaps;
        do {
            if (imageRectangle.overlaps(backgroundArea, OVERLAP_EPSILON) || isItFirstDraw) {
                drawContext.getCanvas().addXObjectFittedIntoRectangle(backgroundXObject, imageRectangle);
                isItFirstDraw = false;
            }
            isCurrentOverlaps = imageRectangle.overlaps(backgroundArea, OVERLAP_EPSILON);
            if (counterX % 2 == 1) {
                isNextOverlaps =
                        imageRectangle.moveRight((imageRectangle.getWidth() + xWhitespace) * counterX)
                                .overlaps(backgroundArea, OVERLAP_EPSILON);
            } else {
                isNextOverlaps = imageRectangle.moveLeft((imageRectangle.getWidth() + xWhitespace) * counterX)
                        .overlaps(backgroundArea, OVERLAP_EPSILON);
            }
            ++counterX;
        }
        while (!backgroundImage.getRepeat().isNoRepeatOnXAxis() && (isCurrentOverlaps || isNextOverlaps));
    }

    /**
     * Create a {@link PdfFormXObject} with the given area and containing a linear gradient inside.
     *
     * @param linearGradientBuilder the linear gradient builder
     * @param xObjectArea           the result object area
     * @param document              the pdf document
     * @return the xObject with a specified area and a linear gradient
     */
    public static PdfFormXObject createXObject(AbstractLinearGradientBuilder linearGradientBuilder,
                                               Rectangle xObjectArea, PdfDocument document) {
        Rectangle formBBox = new Rectangle(0, 0, xObjectArea.getWidth(), xObjectArea.getHeight());
        PdfFormXObject xObject = new PdfFormXObject(formBBox);
        if (linearGradientBuilder != null) {
            Color gradientColor = linearGradientBuilder.buildColor(formBBox, null, document);
            if (gradientColor != null) {
                new PdfCanvas(xObject, document)
                        .setColor(gradientColor, true)
                        .rectangle(formBBox)
                        .fill();
            }
        }
        return xObject;
    }

    /**
     * Evaluate the actual background
     *
     * @param occupiedAreaWithMargins the current occupied area with applied margins
     * @return the actual background area
     */
    protected Rectangle getBackgroundArea(Rectangle occupiedAreaWithMargins) {
        return occupiedAreaWithMargins;
    }

    protected boolean clipBorderArea(DrawContext drawContext, Rectangle outerBorderBox) {
        return clipArea(drawContext, outerBorderBox, true, true, false, true);
    }

    protected boolean clipBackgroundArea(DrawContext drawContext, Rectangle outerBorderBox) {
        return clipArea(drawContext, outerBorderBox, true, false, false, false);
    }

    protected boolean clipBackgroundArea(DrawContext drawContext, Rectangle outerBorderBox, boolean considerBordersBeforeClipping) {
        return clipArea(drawContext, outerBorderBox, true, false, considerBordersBeforeClipping, false);
    }

    private boolean clipArea(DrawContext drawContext, Rectangle outerBorderBox, boolean clipOuter, boolean clipInner, boolean considerBordersBeforeOuterClipping, boolean considerBordersBeforeInnerClipping) {
        // border widths should be considered only once
        assert false == considerBordersBeforeOuterClipping || false == considerBordersBeforeInnerClipping;

        // border widths
        float[] borderWidths = {0, 0, 0, 0};
        // outer box
        float[] outerBox = {
                outerBorderBox.getTop(),
                outerBorderBox.getRight(),
                outerBorderBox.getBottom(),
                outerBorderBox.getLeft()
        };

        // radii
        boolean hasNotNullRadius = false;
        BorderRadius[] borderRadii = getBorderRadii();
        float[] verticalRadii = calculateRadii(borderRadii, outerBorderBox, false);
        float[] horizontalRadii = calculateRadii(borderRadii, outerBorderBox, true);
        for (int i = 0; i < 4; i++) {
            verticalRadii[i] = Math.min(verticalRadii[i], outerBorderBox.getHeight() / 2);
            horizontalRadii[i] = Math.min(horizontalRadii[i], outerBorderBox.getWidth() / 2);
            if (!hasNotNullRadius && (0 != verticalRadii[i] || 0 != horizontalRadii[i])) {
                hasNotNullRadius = true;
            }
        }
        if (hasNotNullRadius) {
            // coordinates of corner centers
            float[] cornersX = {outerBox[3] + horizontalRadii[0], outerBox[1] - horizontalRadii[1], outerBox[1] - horizontalRadii[2], outerBox[3] + horizontalRadii[3]};
            float[] cornersY = {outerBox[0] - verticalRadii[0], outerBox[0] - verticalRadii[1], outerBox[2] + verticalRadii[2], outerBox[2] + verticalRadii[3]};

            PdfCanvas canvas = drawContext.getCanvas();
            canvas.saveState();

            if (considerBordersBeforeOuterClipping) {
                borderWidths = decreaseBorderRadiiWithBorders(horizontalRadii, verticalRadii, outerBox, cornersX, cornersY);
            }

            // clip border area outside
            if (clipOuter) {
                clipOuterArea(canvas, horizontalRadii, verticalRadii, outerBox, cornersX, cornersY);
            }

            if (considerBordersBeforeInnerClipping) {
                borderWidths = decreaseBorderRadiiWithBorders(horizontalRadii, verticalRadii, outerBox, cornersX, cornersY);
            }

            // clip border area inside
            if (clipInner) {
                clipInnerArea(canvas, horizontalRadii, verticalRadii, outerBox, cornersX, cornersY, borderWidths);
            }
        }
        return hasNotNullRadius;
    }

    private void clipOuterArea(PdfCanvas canvas, float[] horizontalRadii, float[] verticalRadii,
            float[] outerBox, float[] cornersX, float[] cornersY) {
        final double top = outerBox[TOP_SIDE];
        final double right = outerBox[RIGHT_SIDE];
        final double bottom = outerBox[BOTTOM_SIDE];
        final double left = outerBox[LEFT_SIDE];

        // left top corner
        if (0 != horizontalRadii[0] || 0 != verticalRadii[0]) {
            double arcBottom = ((double) cornersY[TOP_SIDE]) - verticalRadii[TOP_SIDE];
            double arcRight = ((double) cornersX[TOP_SIDE]) + horizontalRadii[TOP_SIDE];
            canvas
                    .moveTo(left, bottom)
                    .arcContinuous(left, arcBottom, arcRight, top,
                            ARC_LEFT_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(right, top)
                    .lineTo(right, bottom)
                    .lineTo(left, bottom);
            canvas.clip().endPath();
        }
        // right top corner
        if (0 != horizontalRadii[1] || 0 != verticalRadii[1]) {
            double arcLeft = ((double) cornersX[RIGHT_SIDE]) - horizontalRadii[RIGHT_SIDE];
            double arcBottom = ((double) cornersY[RIGHT_SIDE]) - verticalRadii[RIGHT_SIDE];
            canvas
                    .moveTo(left, top)
                    .arcContinuous(arcLeft, top, right, arcBottom,
                            ARC_TOP_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(right, bottom)
                    .lineTo(left, bottom)
                    .lineTo(left, top);
            canvas.clip().endPath();
        }
        // right bottom corner
        if (0 != horizontalRadii[2] || 0 != verticalRadii[2]) {
            double arcTop = ((double) cornersY[BOTTOM_SIDE]) + verticalRadii[BOTTOM_SIDE];
            double arcLeft = ((double) cornersX[BOTTOM_SIDE]) - horizontalRadii[BOTTOM_SIDE];
            canvas
                    .moveTo(right, top)
                    .arcContinuous(right, arcTop, arcLeft, bottom,
                            ARC_RIGHT_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(left, bottom)
                    .lineTo(left, top)
                    .lineTo(right, top);
            canvas.clip().endPath();
        }
        // left bottom corner
        if (0 != horizontalRadii[3] || 0 != verticalRadii[3]) {
            double arcRight = ((double) cornersX[LEFT_SIDE]) + horizontalRadii[LEFT_SIDE];
            double arcTop = ((double) cornersY[LEFT_SIDE]) + verticalRadii[LEFT_SIDE];
            canvas
                    .moveTo(right, bottom)
                    .arcContinuous(arcRight, bottom, left, arcTop,
                            ARC_BOTTOM_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(left, top)
                    .lineTo(right, top)
                    .lineTo(right, bottom);
            canvas.clip().endPath();
        }
    }

    private void clipInnerArea(PdfCanvas canvas, float[] horizontalRadii, float[] verticalRadii,
            float[] outerBox, float[] cornersX, float[] cornersY, float[] borderWidths) {
        final double top = outerBox[TOP_SIDE];
        final double right = outerBox[RIGHT_SIDE];
        final double bottom = outerBox[BOTTOM_SIDE];
        final double left = outerBox[LEFT_SIDE];

        final double x1 = cornersX[TOP_SIDE];
        final double y1 = cornersY[TOP_SIDE];
        final double x2 = cornersX[RIGHT_SIDE];
        final double y2 = cornersY[RIGHT_SIDE];
        final double x3 = cornersX[BOTTOM_SIDE];
        final double y3 = cornersY[BOTTOM_SIDE];
        final double x4 = cornersX[LEFT_SIDE];
        final double y4 = cornersY[LEFT_SIDE];
        final double topBorderWidth = borderWidths[TOP_SIDE];
        final double rightBorderWidth = borderWidths[RIGHT_SIDE];
        final double bottomBorderWidth = borderWidths[BOTTOM_SIDE];
        final double leftBorderWidth = borderWidths[LEFT_SIDE];

        // left top corner
        if (0 != horizontalRadii[0] || 0 != verticalRadii[0]) {
            canvas
                    .arc(left, y1 - verticalRadii[TOP_SIDE],
                            x1 + horizontalRadii[TOP_SIDE], top,
                            ARC_LEFT_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(x2, top)
                    .lineTo(right, y2)
                    .lineTo(right, y3)
                    .lineTo(x3, bottom)
                    .lineTo(x4, bottom)
                    .lineTo(left, y4)
                    .lineTo(left, y1)
                    .lineTo(left - leftBorderWidth, y1)
                    .lineTo(left - leftBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, y1);
            canvas.clip().endPath();
        }
        // right top corner
        if (0 != horizontalRadii[1] || 0 != verticalRadii[1]) {
            canvas
                    .arc(x2 - horizontalRadii[RIGHT_SIDE], top, right,
                            y2 - verticalRadii[RIGHT_SIDE],
                            ARC_TOP_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(right, y3)
                    .lineTo(x3, bottom)
                    .lineTo(x4, bottom)
                    .lineTo(left, y4)
                    .lineTo(left, y1)
                    .lineTo(x1, top)
                    .lineTo(x2, top)
                    .lineTo(x2, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, top + topBorderWidth)
                    .lineTo(x2, top + topBorderWidth);
            canvas.clip().endPath();
        }
        // right bottom corner
        if (0 != horizontalRadii[2] || 0 != verticalRadii[2]) {
            canvas
                    .arc(right, y3 + verticalRadii[BOTTOM_SIDE],
                            x3 - horizontalRadii[BOTTOM_SIDE], bottom,
                            ARC_RIGHT_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(x4, bottom)
                    .lineTo(left, y4)
                    .lineTo(left, y1)
                    .lineTo(x1, top)
                    .lineTo(x2, top)
                    .lineTo(right, y2)
                    .lineTo(right, y3)
                    .lineTo(right + rightBorderWidth, y3)
                    .lineTo(right + rightBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, y3);
            canvas.clip().endPath();
        }
        // left bottom corner
        if (0 != horizontalRadii[3] || 0 != verticalRadii[3]) {
            canvas
                    .arc(x4 + horizontalRadii[LEFT_SIDE], bottom,
                            left, y4 + verticalRadii[LEFT_SIDE],
                            ARC_BOTTOM_DEGREE, ARC_QUARTER_CLOCKWISE_EXTENT)
                    .lineTo(left, y1)
                    .lineTo(x1, top)
                    .lineTo(x2, top)
                    .lineTo(right, y2)
                    .lineTo(right, y3)
                    .lineTo(x3, bottom)
                    .lineTo(x4, bottom)
                    .lineTo(x4, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(right + rightBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, top + topBorderWidth)
                    .lineTo(left - leftBorderWidth, bottom - bottomBorderWidth)
                    .lineTo(x4, bottom - bottomBorderWidth);
            canvas.clip().endPath();
        }
    }

    private float[] decreaseBorderRadiiWithBorders(float[] horizontalRadii, float[] verticalRadii, float[] outerBox, float[] cornersX, float[] cornersY) {
        Border[] borders = getBorders();
        float[] borderWidths = {0, 0, 0, 0};

        if (borders[0] != null) {
            borderWidths[0] = borders[0].getWidth();
            outerBox[0] -= borders[0].getWidth();
            if (cornersY[1] > outerBox[0]) {
                cornersY[1] = outerBox[0];
            }
            if (cornersY[0] > outerBox[0]) {
                cornersY[0] = outerBox[0];
            }
            verticalRadii[0] = Math.max(0, verticalRadii[0] - borders[0].getWidth());
            verticalRadii[1] = Math.max(0, verticalRadii[1] - borders[0].getWidth());
        }
        if (borders[1] != null) {
            borderWidths[1] = borders[1].getWidth();
            outerBox[1] -= borders[1].getWidth();
            if (cornersX[1] > outerBox[1]) {
                cornersX[1] = outerBox[1];
            }
            if (cornersX[2] > outerBox[1]) {
                cornersX[2] = outerBox[1];
            }
            horizontalRadii[1] = Math.max(0, horizontalRadii[1] - borders[1].getWidth());
            horizontalRadii[2] = Math.max(0, horizontalRadii[2] - borders[1].getWidth());
        }
        if (borders[2] != null) {
            borderWidths[2] = borders[2].getWidth();
            outerBox[2] += borders[2].getWidth();
            if (cornersY[2] < outerBox[2]) {
                cornersY[2] = outerBox[2];
            }
            if (cornersY[3] < outerBox[2]) {
                cornersY[3] = outerBox[2];
            }
            verticalRadii[2] = Math.max(0, verticalRadii[2] - borders[2].getWidth());
            verticalRadii[3] = Math.max(0, verticalRadii[3] - borders[2].getWidth());
        }
        if (borders[3] != null) {
            borderWidths[3] = borders[3].getWidth();
            outerBox[3] += borders[3].getWidth();
            if (cornersX[3] < outerBox[3]) {
                cornersX[3] = outerBox[3];
            }
            if (cornersX[0] < outerBox[3]) {
                cornersX[0] = outerBox[3];
            }
            horizontalRadii[3] = Math.max(0, horizontalRadii[3] - borders[3].getWidth());
            horizontalRadii[0] = Math.max(0, horizontalRadii[0] - borders[3].getWidth());
        }
        return borderWidths;
    }

    /**
     * Performs the drawing operation for all {@link IRenderer children}
     * of this renderer.
     *
     * @param drawContext the context (canvas, document, etc) of this drawing operation.
     */
    public void drawChildren(DrawContext drawContext) {
        List waitingRenderers = new ArrayList<>();
        for (IRenderer child : childRenderers) {
            Transform transformProp = child.getProperty(Property.TRANSFORM);
            RootRenderer rootRenderer = getRootRenderer();
            List waiting = (rootRenderer != null && !rootRenderer.waitingDrawingElements.contains(child)) ? rootRenderer.waitingDrawingElements : waitingRenderers;
            processWaitingDrawing(child, transformProp, waiting);
            if (!FloatingHelper.isRendererFloating(child) && transformProp == null) {
                child.draw(drawContext);
            }
        }
        for (IRenderer waitingRenderer : waitingRenderers) {
            waitingRenderer.draw(drawContext);
        }
    }

    /**
     * Performs the drawing operation for the border of this renderer, if
     * defined by any of the {@link Property#BORDER} values in either the layout
     * element or this {@link IRenderer} itself.
     *
     * @param drawContext the context (canvas, document, etc) of this drawing operation.
     */
    public void drawBorder(DrawContext drawContext) {
        Border[] borders = getBorders();
        boolean gotBorders = false;

        for (Border border : borders)
            gotBorders = gotBorders || border != null;

        if (gotBorders) {
            float topWidth = borders[0] != null ? borders[0].getWidth() : 0;
            float rightWidth = borders[1] != null ? borders[1].getWidth() : 0;
            float bottomWidth = borders[2] != null ? borders[2].getWidth() : 0;
            float leftWidth = borders[3] != null ? borders[3].getWidth() : 0;

            Rectangle bBox = getBorderAreaBBox();
            if (bBox.getWidth() < 0 || bBox.getHeight() < 0) {
                Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
                logger.error(MessageFormatUtil.format(IoLogMessageConstant.RECTANGLE_HAS_NEGATIVE_SIZE, "border"));
                return;
            }
            float x1 = bBox.getX();
            float y1 = bBox.getY();
            float x2 = bBox.getX() + bBox.getWidth();
            float y2 = bBox.getY() + bBox.getHeight();

            boolean isTagged = drawContext.isTaggingEnabled();
            PdfCanvas canvas = drawContext.getCanvas();
            if (isTagged) {
                canvas.openTag(new CanvasArtifact());
            }

            Rectangle borderRect = applyMargins(occupiedArea.getBBox().clone(), getMargins(), false);
            boolean isAreaClipped = clipBorderArea(drawContext, borderRect);
            BorderRadius[] borderRadii = getBorderRadii();
            float[] verticalRadii = calculateRadii(borderRadii, borderRect, false);
            float[] horizontalRadii = calculateRadii(borderRadii, borderRect, true);
            for (int i = 0; i < 4; i++) {
                verticalRadii[i] = Math.min(verticalRadii[i], borderRect.getHeight() / 2);
                horizontalRadii[i] = Math.min(horizontalRadii[i], borderRect.getWidth() / 2);
            }
            if (borders[0] != null) {
                if (0 != horizontalRadii[0] || 0 != verticalRadii[0] || 0 != horizontalRadii[1] || 0 != verticalRadii[1]) {
                    borders[0].draw(canvas, x1, y2, x2, y2, horizontalRadii[0], verticalRadii[0], horizontalRadii[1], verticalRadii[1], Border.Side.TOP, leftWidth, rightWidth);
                } else {
                    borders[0].draw(canvas, x1, y2, x2, y2, Border.Side.TOP, leftWidth, rightWidth);
                }
            }
            if (borders[1] != null) {
                if (0 != horizontalRadii[1] || 0 != verticalRadii[1] || 0 != horizontalRadii[2] || 0 != verticalRadii[2]) {
                    borders[1].draw(canvas, x2, y2, x2, y1, horizontalRadii[1], verticalRadii[1], horizontalRadii[2], verticalRadii[2], Border.Side.RIGHT, topWidth, bottomWidth);
                } else {
                    borders[1].draw(canvas, x2, y2, x2, y1, Border.Side.RIGHT, topWidth, bottomWidth);
                }
            }
            if (borders[2] != null) {
                if (0 != horizontalRadii[2] || 0 != verticalRadii[2] || 0 != horizontalRadii[3] || 0 != verticalRadii[3]) {
                    borders[2].draw(canvas, x2, y1, x1, y1, horizontalRadii[2], verticalRadii[2], horizontalRadii[3], verticalRadii[3], Border.Side.BOTTOM, rightWidth, leftWidth);
                } else {
                    borders[2].draw(canvas, x2, y1, x1, y1, Border.Side.BOTTOM, rightWidth, leftWidth);
                }
            }
            if (borders[3] != null) {
                if (0 != horizontalRadii[3] || 0 != verticalRadii[3] || 0 != horizontalRadii[0] || 0 != verticalRadii[0]) {
                    borders[3].draw(canvas, x1, y1, x1, y2, horizontalRadii[3], verticalRadii[3], horizontalRadii[0], verticalRadii[0], Border.Side.LEFT, bottomWidth, topWidth);
                } else {
                    borders[3].draw(canvas, x1, y1, x1, y2, Border.Side.LEFT, bottomWidth, topWidth);
                }
            }

            if (isAreaClipped) {
                drawContext.getCanvas().restoreState();
            }

            if (isTagged) {
                canvas.closeTag();
            }
        }

    }

    /**
     * Indicates whether this renderer is flushed or not, i.e. if {@link #draw(DrawContext)} has already
     * been called.
     *
     * @return whether the renderer has been flushed
     * @see #draw
     */
    @Override
    public boolean isFlushed() {
        return flushed;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IRenderer setParent(IRenderer parent) {
        this.parent = parent;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IRenderer getParent() {
        return parent;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void move(float dxRight, float dyUp) {
        Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
        if (occupiedArea == null) {
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.OCCUPIED_AREA_HAS_NOT_BEEN_INITIALIZED,
                    "Moving won't be performed."));
            return;
        }
        occupiedArea.getBBox().moveRight(dxRight);
        occupiedArea.getBBox().moveUp(dyUp);
        for (IRenderer childRenderer : childRenderers) {
            childRenderer.move(dxRight, dyUp);
        }
        for (IRenderer childRenderer : positionedRenderers) {
            childRenderer.move(dxRight, dyUp);
        }
    }

    /**
     * Gets all rectangles that this {@link IRenderer} can draw upon in the given area.
     *
     * @param area a physical area on the {@link DrawContext}
     * @return a list of {@link Rectangle rectangles}
     */
    public List initElementAreas(LayoutArea area) {
        return Collections.singletonList(area.getBBox());
    }

    /**
     * Gets the bounding box that contains all content written to the
     * {@link DrawContext} by this {@link IRenderer}.
     *
     * @return the smallest {@link Rectangle} that surrounds the content
     */
    public Rectangle getOccupiedAreaBBox() {
        return occupiedArea.getBBox().clone();
    }

    /**
     * Gets the border box of a renderer.
     * This is a box used to draw borders.
     *
     * @return border box of a renderer
     */
    public Rectangle getBorderAreaBBox() {
        Rectangle rect = getOccupiedAreaBBox();
        applyMargins(rect, false);
        applyBorderBox(rect, false);
        return rect;
    }

    public Rectangle getInnerAreaBBox() {
        Rectangle rect = getOccupiedAreaBBox();
        applyMargins(rect, false);
        applyBorderBox(rect, false);
        applyPaddings(rect, false);
        return rect;
    }

    /**
     * Applies margins, borders and paddings of the renderer on the given rectangle.
     *
     * @param rect    a rectangle margins, borders and paddings will be applied on.
     * @param reverse indicates whether margins, borders and paddings will be applied
     *                inside (in case of false) or outside (in case of true) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     */
    Rectangle applyMarginsBordersPaddings(Rectangle rect, boolean reverse) {
        applyMargins(rect, reverse);
        applyBorderBox(rect, reverse);
        applyPaddings(rect, reverse);
        return rect;
    }

    /**
     * Applies margins of the renderer on the given rectangle
     *
     * @param rect    a rectangle margins will be applied on.
     * @param reverse indicates whether margins will be applied
     *                inside (in case of false) or outside (in case of true) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     * @see #getMargins
     */
    public Rectangle applyMargins(Rectangle rect, boolean reverse) {
        return this.applyMargins(rect, getMargins(), reverse);
    }

    /**
     * Applies the border box of the renderer on the given rectangle
     * If the border of a certain side is null, the side will remain as it was.
     *
     * @param rect    a rectangle the border box will be applied on.
     * @param reverse indicates whether the border box will be applied
     *                inside (in case of false) or outside (in case of true) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     * @see #getBorders
     */
    public Rectangle applyBorderBox(Rectangle rect, boolean reverse) {
        Border[] borders = getBorders();
        return applyBorderBox(rect, borders, reverse);
    }

    /**
     * Applies paddings of the renderer on the given rectangle
     *
     * @param rect    a rectangle paddings will be applied on.
     * @param reverse indicates whether paddings will be applied
     *                inside (in case of false) or outside (in case of true) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     * @see #getPaddings
     */
    public Rectangle applyPaddings(Rectangle rect, boolean reverse) {
        return applyPaddings(rect, getPaddings(), reverse);
    }

    public boolean isFirstOnRootArea() {
        return isFirstOnRootArea(false);
    }

    protected void applyDestinationsAndAnnotation(DrawContext drawContext) {
        applyDestination(drawContext.getDocument());
        applyAction(drawContext.getDocument());
        applyLinkAnnotation(drawContext.getDocument());
    }

    protected static boolean isBorderBoxSizing(IRenderer renderer) {
        BoxSizingPropertyValue boxSizing = renderer.getProperty(Property.BOX_SIZING);
        return boxSizing != null && boxSizing.equals(BoxSizingPropertyValue.BORDER_BOX);
    }

    protected boolean isOverflowProperty(OverflowPropertyValue equalsTo, int overflowProperty) {
        return isOverflowProperty(equalsTo, this.getProperty(overflowProperty));
    }

    protected static boolean isOverflowProperty(OverflowPropertyValue equalsTo, IRenderer renderer, int overflowProperty) {
        return isOverflowProperty(equalsTo, renderer.getProperty(overflowProperty));
    }

    protected static boolean isOverflowProperty(OverflowPropertyValue equalsTo, OverflowPropertyValue rendererOverflowProperty) {
        return equalsTo.equals(rendererOverflowProperty) || equalsTo.equals(OverflowPropertyValue.FIT) && rendererOverflowProperty == null;
    }

    protected static boolean isOverflowFit(OverflowPropertyValue rendererOverflowProperty) {
        return rendererOverflowProperty == null || OverflowPropertyValue.FIT.equals(rendererOverflowProperty);
    }

    /**
     * Replaces given property own value with the given value.
     *
     * @param property the property to be replaced
     * @param replacementValue the value with which property will be replaced
     * @param  the type associated with the property
     * @return previous property value
     */
     T replaceOwnProperty(int property, T replacementValue) {
        T ownProperty = this.getOwnProperty(property);
        setProperty(property, replacementValue);
        return ownProperty;
    }

    /**
     * Returns back own value of the given property.
     *
     * @param property the property to be returned back
     * @param prevValue the value which will be returned back
     * @param  the type associated with the property
     */
     void returnBackOwnProperty(int property, T prevValue) {
        if (prevValue == null) {
            deleteOwnProperty(property);
        } else {
            setProperty(property, prevValue);
        }
    }

    /**
     * Checks if this renderer has intrinsic aspect ratio.
     *
     * @return true, if aspect ratio is defined for this renderer, false otherwise
     */
    boolean hasAspectRatio() {
        // TODO DEVSIX-5255 This method should be changed after we support aspect-ratio property
        return false;
    }

    /**
     * Gets intrinsic aspect ratio for this renderer.
     *
     * @return aspect ratio, if it is defined for this renderer, null otherwise
     */
    Float getAspectRatio() {
        // TODO DEVSIX-5255 This method should be changed after we support aspect-ratio property
        return null;
    }

    static void processWaitingDrawing(IRenderer child, Transform transformProp, List waitingDrawing) {
        if (FloatingHelper.isRendererFloating(child) || transformProp != null) {
            waitingDrawing.add(child);
        }
        Border outlineProp = child.getProperty(Property.OUTLINE);
        if (outlineProp != null && child instanceof AbstractRenderer) {
            AbstractRenderer abstractChild = (AbstractRenderer) child;
            if (abstractChild.isRelativePosition())
                abstractChild.applyRelativePositioningTranslation(false);
            Div outlines = new Div().setNeutralRole();
            if (transformProp != null)
                outlines.setProperty(Property.TRANSFORM, transformProp);
            outlines.setProperty(Property.BORDER, outlineProp);
            float offset = outlines.getProperty(Property.BORDER).getWidth();
            if (abstractChild.getPropertyAsFloat(Property.OUTLINE_OFFSET) != null)
                offset += (float) abstractChild.getPropertyAsFloat(Property.OUTLINE_OFFSET);
            DivRenderer div = new DivRenderer(outlines);
            div.setParent(abstractChild.getParent());
            Rectangle divOccupiedArea = abstractChild.applyMargins(abstractChild.occupiedArea.clone().getBBox(), false).moveLeft(offset).moveDown(offset);
            divOccupiedArea.setWidth(divOccupiedArea.getWidth() + 2 * offset).setHeight(divOccupiedArea.getHeight() + 2 * offset);
            div.occupiedArea = new LayoutArea(abstractChild.getOccupiedArea().getPageNumber(), divOccupiedArea);
            float outlineWidth = div.getProperty(Property.BORDER).getWidth();
            if (divOccupiedArea.getWidth() >= outlineWidth * 2 && divOccupiedArea.getHeight() >= outlineWidth * 2) {
                waitingDrawing.add(div);
            }
            if (abstractChild.isRelativePosition())
                abstractChild.applyRelativePositioningTranslation(true);
        }
    }

    /**
     * Retrieves element's fixed content box width, if it's set.
     * Takes into account {@link Property#BOX_SIZING}, {@link Property#MIN_WIDTH},
     * and {@link Property#MAX_WIDTH} properties.
     *
     * @param parentBoxWidth width of the parent element content box.
     *                       If element has relative width, it will be
     *                       calculated relatively to this parameter.
     * @return element's fixed content box width or null if it's not set.
     * @see AbstractRenderer#hasAbsoluteUnitValue(int)
     */
    protected Float retrieveWidth(float parentBoxWidth) {
        Float minWidth = retrieveUnitValue(parentBoxWidth, Property.MIN_WIDTH);

        Float maxWidth = retrieveUnitValue(parentBoxWidth, Property.MAX_WIDTH);
        if (maxWidth != null && minWidth != null && minWidth > maxWidth) {
            maxWidth = minWidth;
        }

        Float width = retrieveUnitValue(parentBoxWidth, Property.WIDTH);
        if (width != null) {
            if (maxWidth != null) {
                width = width > maxWidth ? maxWidth : width;
            }
            if (minWidth != null) {
                width = width < minWidth ? minWidth : width;
            }
        } else if (maxWidth != null) {
            width = maxWidth < parentBoxWidth ? maxWidth : null;
        }

        if (width != null && isBorderBoxSizing(this)) {
            width -= calculatePaddingBorderWidth(this);
        }

        return width != null ? (Float) Math.max(0, (float) width) : null;
    }

    /**
     * Retrieves element's fixed content box max width, if it's set.
     * Takes into account {@link Property#BOX_SIZING} and {@link Property#MIN_WIDTH} properties.
     *
     * @param parentBoxWidth width of the parent element content box.
     *                       If element has relative width, it will be
     *                       calculated relatively to this parameter.
     * @return element's fixed content box max width or null if it's not set.
     * @see AbstractRenderer#hasAbsoluteUnitValue(int)
     */
    protected Float retrieveMaxWidth(float parentBoxWidth) {
        Float maxWidth = retrieveUnitValue(parentBoxWidth, Property.MAX_WIDTH);
        if (maxWidth != null) {
            Float minWidth = retrieveUnitValue(parentBoxWidth, Property.MIN_WIDTH);
            if (minWidth != null && minWidth > maxWidth) {
                maxWidth = minWidth;
            }

            if (isBorderBoxSizing(this)) {
                maxWidth -= calculatePaddingBorderWidth(this);
            }
            return maxWidth > 0 ? maxWidth : 0;
        } else {
            return null;
        }
    }

    /**
     * Retrieves element's fixed content box max width, if it's set.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @param parentBoxWidth width of the parent element content box.
     *                       If element has relative width, it will be
     *                       calculated relatively to this parameter.
     * @return element's fixed content box max width or null if it's not set.
     * @see AbstractRenderer#hasAbsoluteUnitValue(int)
     */
    protected Float retrieveMinWidth(float parentBoxWidth) {
        Float minWidth = retrieveUnitValue(parentBoxWidth, Property.MIN_WIDTH);
        if (minWidth != null) {
            if (isBorderBoxSizing(this)) {
                minWidth -= calculatePaddingBorderWidth(this);
            }
            return minWidth > 0 ? minWidth : 0;
        } else {
            return null;
        }
    }

    /**
     * Updates fixed content box width value for this renderer.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @param updatedWidthValue element's new fixed content box width.
     */
    protected void updateWidth(UnitValue updatedWidthValue) {
        if (updatedWidthValue.isPointValue() && isBorderBoxSizing(this)) {
            updatedWidthValue.setValue(updatedWidthValue.getValue() + calculatePaddingBorderWidth(this));
        }
        setProperty(Property.WIDTH, updatedWidthValue);
    }

    /**
     * Retrieves the element's fixed content box height, if it's set.
     * Takes into account {@link Property#BOX_SIZING}, {@link Property#MIN_HEIGHT},
     * and {@link Property#MAX_HEIGHT} properties.
     *
     * @return element's fixed content box height or null if it's not set.
     */
    protected Float retrieveHeight() {
        Float height = null;
        UnitValue heightUV = getPropertyAsUnitValue(Property.HEIGHT);
        Float parentResolvedHeight = retrieveResolvedParentDeclaredHeight();
        Float minHeight = null;
        Float maxHeight = null;
        if (heightUV != null) {
            if (parentResolvedHeight == null) {
                if (heightUV.isPercentValue()) {
                    //If the height is a relative value and no parent with a resolved height can be found, treat it as null
                    height = null;
                } else {
                    //Since no parent height is resolved, only point-value min and max should be taken into account
                    UnitValue minHeightUV = getPropertyAsUnitValue(Property.MIN_HEIGHT);
                    if (minHeightUV != null && minHeightUV.isPointValue()) {
                        minHeight = minHeightUV.getValue();
                    }
                    UnitValue maxHeightUV = getPropertyAsUnitValue(Property.MAX_HEIGHT);
                    if (maxHeightUV != null && maxHeightUV.isPointValue()) {
                        maxHeight = maxHeightUV.getValue();
                    }
                    //If the height is stored as a point value, we do not care about the parent's resolved height
                    height = heightUV.getValue();
                }
            } else {
                minHeight = retrieveUnitValue((float) parentResolvedHeight, Property.MIN_HEIGHT);
                maxHeight = retrieveUnitValue((float) parentResolvedHeight, Property.MAX_HEIGHT);
                height = retrieveUnitValue((float) parentResolvedHeight, Property.HEIGHT);
            }
            if (maxHeight != null && minHeight != null && minHeight > maxHeight) {
                maxHeight = minHeight;
            }
            if (height != null) {
                if (maxHeight != null) {
                    height = height > maxHeight ? maxHeight : height;
                }
                if (minHeight != null) {
                    height = height < minHeight ? minHeight : height;
                }
            }
            if (height != null && isBorderBoxSizing(this)) {
                height -= calculatePaddingBorderHeight(this);
            }
        }
        return height != null ? (Float) Math.max(0, (float) height) : null;

    }

    /**
     * Calculates the element corner's border radii.
     *
     * @param radii      defines border radii of the element
     * @param area       defines the area of the element
     * @param horizontal defines whether horizontal or vertical radii should be calculated
     * @return the element corner's border radii.
     */
    private float[] calculateRadii(BorderRadius[] radii, Rectangle area, boolean horizontal) {
        float[] results = new float[4];
        UnitValue value;
        for (int i = 0; i < 4; i++) {
            if (null != radii[i]) {
                value = horizontal ? radii[i].getHorizontalRadius() : radii[i].getVerticalRadius();
                if (value != null) {
                    if (value.getUnitType() == UnitValue.PERCENT) {
                        results[i] = value.getValue() * (horizontal ? area.getWidth() : area.getHeight()) / 100;
                    } else {
                        assert value.getUnitType() == UnitValue.POINT;
                        results[i] = value.getValue();
                    }
                } else {
                    results[i] = 0;
                }
            } else {
                results[i] = 0;
            }
        }
        return results;
    }

    /**
     * Updates fixed content box height value for this renderer.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @param updatedHeight element's new fixed content box height, shall be not null.
     */
    protected void updateHeight(UnitValue updatedHeight) {
        if (isBorderBoxSizing(this) && updatedHeight.isPointValue()) {
            updatedHeight.setValue(updatedHeight.getValue() + calculatePaddingBorderHeight(this));

        }
        setProperty(Property.HEIGHT, updatedHeight);
    }

    /**
     * Retrieve element's content box max-ehight, if it's set.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @return element's content box max-height or null if it's not set.
     */
    protected Float retrieveMaxHeight() {
        Float maxHeight = null, minHeight = null;
        Float directParentDeclaredHeight = retrieveDirectParentDeclaredHeight();
        UnitValue maxHeightAsUV = getPropertyAsUnitValue(Property.MAX_HEIGHT);
        if (maxHeightAsUV != null) {
            if (directParentDeclaredHeight == null) {
                if (maxHeightAsUV.isPercentValue()) {
                    maxHeight = null;
                } else {
                    minHeight = retrieveMinHeight();
                    //Since no parent height is resolved, only point-value min should be taken into account
                    UnitValue minHeightUV = getPropertyAsUnitValue(Property.MIN_HEIGHT);
                    if (minHeightUV != null && minHeightUV.isPointValue()) {
                        minHeight = minHeightUV.getValue();
                    }
                    //We don't care about a baseline if the max-height is explicitly defined
                    maxHeight = maxHeightAsUV.getValue();
                }
            } else {
                maxHeight = retrieveUnitValue((float) directParentDeclaredHeight, Property.MAX_HEIGHT);
            }
            if (maxHeight != null) {
                if (minHeight != null && minHeight > maxHeight) {
                    maxHeight = minHeight;
                }
                if (isBorderBoxSizing(this)) {
                    maxHeight -= calculatePaddingBorderHeight(this);
                }
                return maxHeight > 0 ? maxHeight : 0;
            }
        }
        //Max height is not set, but height might be set
        return retrieveHeight();
    }


    /**
     * Updates content box max-height value for this renderer.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @param updatedMaxHeight element's new content box max-height, shall be not null.
     */
    protected void updateMaxHeight(UnitValue updatedMaxHeight) {
        if (isBorderBoxSizing(this) && updatedMaxHeight.isPointValue()) {
            updatedMaxHeight.setValue(updatedMaxHeight.getValue() + calculatePaddingBorderHeight(this));

        }
        setProperty(Property.MAX_HEIGHT, updatedMaxHeight);
    }


    /**
     * Retrieves element's content box min-height, if it's set.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @return element's content box min-height or null if it's not set.
     */
    protected Float retrieveMinHeight() {
        Float minHeight = null;
        Float directParentDeclaredHeight = retrieveDirectParentDeclaredHeight();
        UnitValue minHeightUV = getPropertyAsUnitValue(this, Property.MIN_HEIGHT);
        if (minHeightUV != null) {
            if (directParentDeclaredHeight == null) {
                if (minHeightUV.isPercentValue()) {
                    //if there is no baseline to compare against, a relative value evaluates to null
                    minHeight = null;
                } else {
                    //If the min-height is stored as a point value, we do not care about a baseline.
                    minHeight = minHeightUV.getValue();
                }
            } else {
                minHeight = retrieveUnitValue((float) directParentDeclaredHeight, Property.MIN_HEIGHT);
            }
            if (minHeight != null) {
                if (isBorderBoxSizing(this)) {
                    minHeight -= calculatePaddingBorderHeight(this);
                }
                return minHeight > 0 ? minHeight : 0;
            }
        }
        //min-height might be zero, but height might be set
        return retrieveHeight();

    }

    /**
     * Updates content box min-height value for this renderer.
     * Takes into account {@link Property#BOX_SIZING} property value.
     *
     * @param updatedMinHeight element's new content box min-height, shall be not null.
     */
    protected void updateMinHeight(UnitValue updatedMinHeight) {
        if (isBorderBoxSizing(this) && updatedMinHeight.isPointValue()) {
            updatedMinHeight.setValue(updatedMinHeight.getValue() + calculatePaddingBorderHeight(this));
        }
        setProperty(Property.MIN_HEIGHT, updatedMinHeight);
    }

    protected Float retrieveUnitValue(float baseValue, int property) {
        return retrieveUnitValue(baseValue, property, false);
    }

    protected Float retrieveUnitValue(float baseValue, int property, boolean pointOnly) {
        UnitValue value = this.getProperty(property);
        if (pointOnly && value.getUnitType() == UnitValue.POINT) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED, property));
        }
        if (value != null) {
            if (value.getUnitType() == UnitValue.PERCENT) {
                // during mathematical operations the precision can be lost, so avoiding them if possible (100 / 100 == 1) is a good practice
                return value.getValue() != 100 ? baseValue * value.getValue() / 100 : baseValue;
            } else {
                assert value.getUnitType() == UnitValue.POINT;
                return value.getValue();
            }
        } else {
            return null;
        }
    }
    
    protected Map getOwnProperties() {
        return properties;
    }

    protected void addAllProperties(Map properties) {
        this.properties.putAll(properties);
    }

    /**
     * Gets the first yLine of the nested children recursively. E.g. for a list, this will be the yLine of the
     * first item (if the first item is indeed a paragraph).
     * NOTE: this method will no go further than the first child.
     *
     * @return the first yline of the nested children, null if there is no text found
     */
    protected Float getFirstYLineRecursively() {
        if (childRenderers.size() == 0) {
            return null;
        }
        return ((AbstractRenderer) childRenderers.get(0)).getFirstYLineRecursively();
    }

    protected Float getLastYLineRecursively() {
        if (!allowLastYLineRecursiveExtraction()) {
            return null;
        }
        for (int i = childRenderers.size() - 1; i >= 0; i--) {
            IRenderer child = childRenderers.get(i);
            if (child instanceof AbstractRenderer) {
                Float lastYLine = ((AbstractRenderer) child).getLastYLineRecursively();
                if (lastYLine != null) {
                    return lastYLine;
                }
            }
        }
        return null;
    }

    protected boolean allowLastYLineRecursiveExtraction() {
        return !isOverflowProperty(OverflowPropertyValue.HIDDEN, Property.OVERFLOW_X)
                && !isOverflowProperty(OverflowPropertyValue.HIDDEN, Property.OVERFLOW_Y);
    }

    /**
     * Applies given margins on the given rectangle
     *
     * @param rect    a rectangle margins will be applied on.
     * @param margins the margins to be applied on the given rectangle
     * @param reverse indicates whether margins will be applied
     *                inside (in case of false) or outside (in case of true) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     */
    protected Rectangle applyMargins(Rectangle rect, UnitValue[] margins, boolean reverse) {
        if (!margins[TOP_SIDE].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.MARGIN_TOP));
        }
        if (!margins[RIGHT_SIDE].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.MARGIN_RIGHT));
        }
        if (!margins[BOTTOM_SIDE].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.MARGIN_BOTTOM));
        }
        if (!margins[LEFT_SIDE].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.MARGIN_LEFT));
        }
        return rect.applyMargins(
                margins[TOP_SIDE].getValue(),
                margins[RIGHT_SIDE].getValue(),
                margins[BOTTOM_SIDE].getValue(),
                margins[LEFT_SIDE].getValue(), reverse);
    }

    /**
     * Returns margins of the renderer
     * [0] - top; [1] - right; [2] - bottom; [3] - left
     *
     * @return a {@code float[]} margins of the renderer
     */
    protected UnitValue[] getMargins() {
        return getMargins(this);
    }

    /**
     * Returns paddings of the renderer
     * [0] - top; [1] - right; [2] - bottom; [3] - left
     *
     * @return a {@code float[]} paddings of the renderer
     */
    protected UnitValue[] getPaddings() {
        return getPaddings(this);
    }

    /**
     * Applies given paddings to the given rectangle.
     *
     * @param rect     a rectangle paddings will be applied on.
     * @param paddings the paddings to be applied on the given rectangle
     * @param reverse  indicates whether paddings will be applied
     *                 inside (in case of false) or outside (in case of true) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     */
    protected Rectangle applyPaddings(Rectangle rect, UnitValue[] paddings, boolean reverse) {
        if (paddings[0] != null && !paddings[0].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.PADDING_TOP));
        }
        if (paddings[1] != null && !paddings[1].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.PADDING_RIGHT));
        }
        if (paddings[2] != null && !paddings[2].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.PADDING_BOTTOM));
        }
        if (paddings[3] != null && !paddings[3].isPointValue()) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
                    Property.PADDING_LEFT));
        }
        return rect.applyMargins(paddings[0] != null ? paddings[0].getValue() : 0,
                paddings[1] != null ? paddings[1].getValue() : 0,
                paddings[2] != null ? paddings[2].getValue() : 0,
                paddings[3] != null ? paddings[3].getValue() : 0,
                reverse);
    }

    /**
     * Applies the given border box (borders) on the given rectangle
     *
     * @param rect    a rectangle paddings will be applied on.
     * @param borders the {@link Border borders} to be applied on the given rectangle
     * @param reverse indicates whether the border box will be applied
     *                inside (in case of false) or outside (in case of false) the rectangle.
     * @return a {@link Rectangle border box} of the renderer
     */
    protected Rectangle applyBorderBox(Rectangle rect, Border[] borders, boolean reverse) {
        float topWidth = borders[0] != null ? borders[0].getWidth() : 0;
        float rightWidth = borders[1] != null ? borders[1].getWidth() : 0;
        float bottomWidth = borders[2] != null ? borders[2].getWidth() : 0;
        float leftWidth = borders[3] != null ? borders[3].getWidth() : 0;
        return rect.applyMargins(topWidth, rightWidth, bottomWidth, leftWidth, reverse);
    }

    protected void applyAbsolutePosition(Rectangle parentRect) {
        Float top = this.getPropertyAsFloat(Property.TOP);
        Float bottom = this.getPropertyAsFloat(Property.BOTTOM);
        Float left = this.getPropertyAsFloat(Property.LEFT);
        Float right = this.getPropertyAsFloat(Property.RIGHT);

        if (left == null && right == null && BaseDirection.RIGHT_TO_LEFT.equals(this.getProperty(Property.BASE_DIRECTION))) {
            right = 0f;
        }

        if (top == null && bottom == null) {
            top = 0f;
        }

        try {
            if (right != null) {
                move(parentRect.getRight() - (float) right - occupiedArea.getBBox().getRight(), 0);
            }

            if (left != null) {
                move(parentRect.getLeft() + (float) left - occupiedArea.getBBox().getLeft(), 0);
            }

            if (top != null) {
                move(0, parentRect.getTop() - (float) top - occupiedArea.getBBox().getTop());
            }

            if (bottom != null) {
                move(0, parentRect.getBottom() + (float) bottom - occupiedArea.getBBox().getBottom());
            }
        } catch (Exception exc) {
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.error(MessageFormatUtil.format(IoLogMessageConstant.OCCUPIED_AREA_HAS_NOT_BEEN_INITIALIZED,
                    "Absolute positioning might be applied incorrectly."));
        }
    }

    protected void applyRelativePositioningTranslation(boolean reverse) {
        float top = (float) this.getPropertyAsFloat(Property.TOP, 0f);
        float bottom = (float) this.getPropertyAsFloat(Property.BOTTOM, 0f);
        float left = (float) this.getPropertyAsFloat(Property.LEFT, 0f);
        float right = (float) this.getPropertyAsFloat(Property.RIGHT, 0f);

        int reverseMultiplier = reverse ? -1 : 1;

        float dxRight = left != 0 ? left * reverseMultiplier : -right * reverseMultiplier;
        float dyUp = top != 0 ? -top * reverseMultiplier : bottom * reverseMultiplier;

        if (dxRight != 0 || dyUp != 0)
            move(dxRight, dyUp);
    }

    protected void applyDestination(PdfDocument document) {
        Object destination = this.getProperty(Property.DESTINATION);
        if (destination == null) {
            return;
        }
        String destinationName = null;
        PdfDictionary linkActionDict = null;
        if (destination instanceof String) {
            destinationName = (String)destination;
        } else if (CHECK_TUPLE2_TYPE.getClass().equals(destination.getClass())) {
            // 'If' above is the only autoportable way it seems
            Tuple2 destTuple = (Tuple2)destination;
            destinationName = destTuple.getFirst();
            linkActionDict = destTuple.getSecond();
        }

        if (destinationName != null) {
            int pageNumber = occupiedArea.getPageNumber();
            if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
                Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
                String logMessageArg = "Property.DESTINATION, which specifies this element location as destination, see ElementPropertyContainer.setDestination.";
                logger.warn(MessageFormatUtil.format(
                        IoLogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN,
                        logMessageArg));
                return;
            }

            PdfArray array = new PdfArray();
            array.add(document.getPage(pageNumber).getPdfObject());
            array.add(PdfName.XYZ);
            array.add(new PdfNumber(occupiedArea.getBBox().getX()));
            array.add(new PdfNumber(occupiedArea.getBBox().getY() + occupiedArea.getBBox().getHeight()));
            array.add(new PdfNumber(0));
            document.addNamedDestination(destinationName, array.makeIndirect(document));
        }

        final boolean isPdf20 = document.getPdfVersion().compareTo(PdfVersion.PDF_2_0) >= 0;
        if (linkActionDict != null && isPdf20 && document.isTagged()) {
            // Add structure destination for the action for tagged pdf 2.0
            PdfStructElem structElem = getCurrentStructElem(document);
            PdfStructureDestination dest = PdfStructureDestination.createFit(structElem);
            linkActionDict.put(PdfName.SD, dest.getPdfObject());
        }

        deleteProperty(Property.DESTINATION);
    }

    protected void applyAction(PdfDocument document) {
        PdfAction action = this.getProperty(Property.ACTION);
        if (action != null) {
            PdfLinkAnnotation link = this.getProperty(Property.LINK_ANNOTATION);
            if (link == null) {
                link = (PdfLinkAnnotation) new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0)).setFlags(PdfAnnotation.PRINT);
                Border border = this.getProperty(Property.BORDER);
                if (border != null) {
                    link.setBorder(new PdfArray(new float[]{0, 0, border.getWidth()}));
                } else {
                    link.setBorder(new PdfArray(new float[]{0, 0, 0}));
                }
                setProperty(Property.LINK_ANNOTATION, link);
            }
            link.setAction(action);
        }
    }

    protected void applyLinkAnnotation(PdfDocument document) {
        Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
        PdfLinkAnnotation linkAnnotation = this.getProperty(Property.LINK_ANNOTATION);
        if (linkAnnotation == null) {
            return;
        }

        int pageNumber = occupiedArea.getPageNumber();
        if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
            String logMessageArg = "Property.LINK_ANNOTATION, which specifies a link associated with this element content area, see com.itextpdf.layout.element.Link.";
            logger.warn(MessageFormatUtil.format(
                    IoLogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN,
                    logMessageArg));
            return;
        }

        // If an element with a link annotation occupies more than two pages,
        // then a NPE might occur, because of the annotation being partially flushed.
        // That's why we create and use an annotation's copy.
        PdfDictionary newAnnotation = (PdfDictionary) linkAnnotation.getPdfObject().clone();
        linkAnnotation = (PdfLinkAnnotation) PdfAnnotation.makeAnnotation(newAnnotation);
        Rectangle pdfBBox = calculateAbsolutePdfBBox();
        linkAnnotation.setRectangle(new PdfArray(pdfBBox));

        PdfPage page = document.getPage(pageNumber);
        // TODO DEVSIX-1655 This check is necessary because, in some cases, our renderer's hierarchy may contain
        //  a renderer from the different page that was already flushed
        if (page.isFlushed()) {
            logger.error(MessageFormatUtil.format(
                    IoLogMessageConstant.PAGE_WAS_FLUSHED_ACTION_WILL_NOT_BE_PERFORMED, "link annotation applying"));
        } else {
            page.addAnnotation(linkAnnotation);
        }
    }

    /**
     * Retrieve the parent's resolved height declaration.
     * If the parent has a relative height declaration, it will check it's parent recursively,
     *
     * @return null if no height declaration is set on the parent, or if it's own height declaration cannot be resolved
     * The float value of the resolved height otherwiser
     */
    private Float retrieveResolvedParentDeclaredHeight() {
        if (parent != null && parent.getProperty(Property.HEIGHT) != null) {
            UnitValue parentHeightUV = getPropertyAsUnitValue(parent, Property.HEIGHT);
            if (parentHeightUV.isPointValue()) {
                return parentHeightUV.getValue();
            } else {
                return ((AbstractRenderer) parent).retrieveHeight();
            }
        } else {
            return null;
        }
    }

    /**
     * Retrieve the direct parent's absolute height property
     *
     * @return the direct parent's absolute height property value if it exists, null otherwise
     */
    private Float retrieveDirectParentDeclaredHeight() {
        if (parent != null && parent.getProperty(Property.HEIGHT) != null) {
            UnitValue parentHeightUV = getPropertyAsUnitValue(parent, Property.HEIGHT);
            if (parentHeightUV.isPointValue()) {
                return parentHeightUV.getValue();
            }
        }
        return null;
    }

    protected void updateHeightsOnSplit(boolean wasHeightClipped, AbstractRenderer splitRenderer, AbstractRenderer overflowRenderer) {
        updateHeightsOnSplit(occupiedArea.getBBox().getHeight(), wasHeightClipped, splitRenderer, overflowRenderer, true);
    }

    void updateHeightsOnSplit(float usedHeight, boolean wasHeightClipped, AbstractRenderer splitRenderer, AbstractRenderer overflowRenderer, boolean enlargeOccupiedAreaOnHeightWasClipped) {
        if (wasHeightClipped) {
            // if height was clipped, max height exists and can be resolved
            Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
            logger.warn(IoLogMessageConstant.CLIP_ELEMENT);

            if (enlargeOccupiedAreaOnHeightWasClipped) {
                Float maxHeight = retrieveMaxHeight();
                splitRenderer.occupiedArea.getBBox()
                        .moveDown((float) maxHeight - usedHeight)
                        .setHeight((float) maxHeight);
                usedHeight = (float) maxHeight;
            }
        }

        if (overflowRenderer == null || isKeepTogether()) {
            return;
        }

        // Update height related properties on split or overflow
        // For relative heights, we need the parent's resolved height declaration
        Float parentResolvedHeightPropertyValue = retrieveResolvedParentDeclaredHeight();
        UnitValue maxHeightUV = getPropertyAsUnitValue(this, Property.MAX_HEIGHT);
        if (maxHeightUV != null) {
            if (maxHeightUV.isPointValue()) {
                Float maxHeight = retrieveMaxHeight();
                UnitValue updateMaxHeight = UnitValue.createPointValue((float) (maxHeight - usedHeight));
                overflowRenderer.updateMaxHeight(updateMaxHeight);
            } else if (parentResolvedHeightPropertyValue != null) {
                // Calculate occupied fraction and update overflow renderer
                float currentOccupiedFraction = usedHeight / (float) parentResolvedHeightPropertyValue * 100;
                // Fraction
                float newFraction = maxHeightUV.getValue() - currentOccupiedFraction;
                // Update
                overflowRenderer.updateMinHeight(UnitValue.createPercentValue(newFraction));
            }
            // If parent has no resolved height, relative height declarations can be ignored
        }
        UnitValue minHeightUV = getPropertyAsUnitValue(this, Property.MIN_HEIGHT);
        if (minHeightUV != null) {
            if (minHeightUV.isPointValue()) {
                Float minHeight = retrieveMinHeight();
                UnitValue updateminHeight = UnitValue.createPointValue((float) (minHeight - usedHeight));
                overflowRenderer.updateMinHeight(updateminHeight);
            } else if (parentResolvedHeightPropertyValue != null) {
                // Calculate occupied fraction and update overflow renderer
                float currentOccupiedFraction = usedHeight / (float) parentResolvedHeightPropertyValue * 100;
                // Fraction
                float newFraction = minHeightUV.getValue() - currentOccupiedFraction;
                // Update
                overflowRenderer.updateMinHeight(UnitValue.createPercentValue(newFraction));
            }
            // If parent has no resolved height, relative height declarations can be ignored
        }

        UnitValue heightUV = getPropertyAsUnitValue(this, Property.HEIGHT);
        if (heightUV != null) {
            if (heightUV.isPointValue()) {
                Float height = retrieveHeight();
                UnitValue updateHeight = UnitValue.createPointValue((float) (height - usedHeight));
                overflowRenderer.updateHeight(updateHeight);
            } else if (parentResolvedHeightPropertyValue != null) {
                // Calculate occupied fraction and update overflow renderer
                float currentOccupiedFraction = usedHeight / (float) parentResolvedHeightPropertyValue * 100;
                // Fraction
                float newFraction = heightUV.getValue() - currentOccupiedFraction;
                // Update
                overflowRenderer.updateMinHeight(UnitValue.createPercentValue(newFraction));
            }
            // If parent has no resolved height, relative height declarations can be ignored
        }
    }

    /**
     * Calculates min and max width values for current renderer.
     *
     * @return instance of {@link MinMaxWidth}
     */
    public MinMaxWidth getMinMaxWidth() {
        return MinMaxWidthUtils.countDefaultMinMaxWidth(this);
    }

    protected boolean setMinMaxWidthBasedOnFixedWidth(MinMaxWidth minMaxWidth) {
        // retrieve returns max width, if there is no width.
        if (hasAbsoluteUnitValue(Property.WIDTH)) {
            //Renderer may override retrieveWidth, double check is required.
            Float width = retrieveWidth(0);
            if (width != null) {
                minMaxWidth.setChildrenMaxWidth((float) width);
                minMaxWidth.setChildrenMinWidth((float) width);
                return true;
            }
        }
        return false;
    }

    protected boolean isNotFittingHeight(LayoutArea layoutArea) {
        return !isPositioned() && occupiedArea.getBBox().getHeight() > layoutArea.getBBox().getHeight();
    }

    protected boolean isNotFittingWidth(LayoutArea layoutArea) {
        return !isPositioned() && occupiedArea.getBBox().getWidth() > layoutArea.getBBox().getWidth();
    }

    protected boolean isNotFittingLayoutArea(LayoutArea layoutArea) {
        return isNotFittingHeight(layoutArea) || isNotFittingWidth(layoutArea);
    }

    /**
     * Indicates whether the renderer's position is fixed or not.
     *
     * @return a {@code boolean}
     */
    protected boolean isPositioned() {
        return !isStaticLayout();
    }

    /**
     * Indicates whether the renderer's position is fixed or not.
     *
     * @return a {@code boolean}
     */
    protected boolean isFixedLayout() {
        Object positioning = this.getProperty(Property.POSITION);
        return Integer.valueOf(LayoutPosition.FIXED).equals(positioning);
    }

    protected boolean isStaticLayout() {
        Object positioning = this.getProperty(Property.POSITION);
        return positioning == null || Integer.valueOf(LayoutPosition.STATIC).equals(positioning);
    }

    protected boolean isRelativePosition() {
        Integer positioning = this.getPropertyAsInteger(Property.POSITION);
        return Integer.valueOf(LayoutPosition.RELATIVE).equals(positioning);
    }

    protected boolean isAbsolutePosition() {
        Integer positioning = this.getPropertyAsInteger(Property.POSITION);
        return Integer.valueOf(LayoutPosition.ABSOLUTE).equals(positioning);
    }

    protected boolean isKeepTogether() {
        return isKeepTogether(null);
    }

    boolean isKeepTogether(IRenderer causeOfNothing) {
        return Boolean.TRUE.equals(getPropertyAsBoolean(Property.KEEP_TOGETHER))
                && !(causeOfNothing instanceof AreaBreakRenderer);
    }

    // Note! The second parameter is here on purpose. Currently occupied area is passed as a value of this parameter in
    // BlockRenderer, but actually, the block can have many areas, and occupied area will be the common area of sub-areas,
    // whereas child element alignment should be performed area-wise.
    protected void alignChildHorizontally(IRenderer childRenderer, Rectangle currentArea) {
        float availableWidth = currentArea.getWidth();
        HorizontalAlignment horizontalAlignment = childRenderer.getProperty(Property.HORIZONTAL_ALIGNMENT);
        if (horizontalAlignment != null && horizontalAlignment != HorizontalAlignment.LEFT) {
            float freeSpace = availableWidth - childRenderer.getOccupiedArea().getBBox().getWidth();
            if (freeSpace > 0) {
                try {
                    switch (horizontalAlignment) {
                        case RIGHT:
                            childRenderer.move(freeSpace, 0);
                            break;
                        case CENTER:
                            childRenderer.move(freeSpace / 2, 0);
                            break;
                    }
                } catch (NullPointerException e) {
                    Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
                    logger.error(MessageFormatUtil.format(IoLogMessageConstant.OCCUPIED_AREA_HAS_NOT_BEEN_INITIALIZED,
                            "Some of the children might not end up aligned horizontally."));
                }
            }
        }
    }

    /**
     * Gets borders of the element in the specified order: top, right, bottom, left.
     *
     * @return an array of BorderDrawer objects.
     * In case when certain border isn't set Property.BORDER is used,
     * and if Property.BORDER is also not set then null is returned
     * on position of this border
     */
    protected Border[] getBorders() {
        return getBorders(this);
    }

    /**
     * Gets border radii of the element in the specified order: top-left, top-right, bottom-right, bottom-left.
     *
     * @return an array of BorderRadius objects.
     * In case when certain border radius isn't set Property.BORDER_RADIUS is used,
     * and if Property.BORDER_RADIUS is also not set then null is returned
     * on position of this border radius
     */
    protected BorderRadius[] getBorderRadii() {
        return getBorderRadii(this);
    }

    protected AbstractRenderer setBorders(Border border, int borderNumber) {
        switch (borderNumber) {
            case 0:
                setProperty(Property.BORDER_TOP, border);
                break;
            case 1:
                setProperty(Property.BORDER_RIGHT, border);
                break;
            case 2:
                setProperty(Property.BORDER_BOTTOM, border);
                break;
            case 3:
                setProperty(Property.BORDER_LEFT, border);
                break;
        }

        return this;
    }

    /**
     * Calculates the bounding box of the content in the coordinate system of the pdf entity on which content is placed,
     * e.g. document page or form xObject. This is particularly useful for the cases when element is nested in the rotated
     * element.
     *
     * @return a {@link Rectangle} which is a bbox of the content not relative to the parent's layout area but rather to
     * the some pdf entity coordinate system.
     */
    protected Rectangle calculateAbsolutePdfBBox() {
        Rectangle contentBox = getOccupiedAreaBBox();
        List contentBoxPoints = rectangleToPointsList(contentBox);
        AbstractRenderer renderer = this;
        while (renderer.parent != null) {
            if (renderer instanceof BlockRenderer) {
                Float angle = renderer.getProperty(Property.ROTATION_ANGLE);
                if (angle != null) {
                    BlockRenderer blockRenderer = (BlockRenderer) renderer;
                    AffineTransform rotationTransform = blockRenderer.createRotationTransformInsideOccupiedArea();
                    transformPoints(contentBoxPoints, rotationTransform);
                }
            }

            if (renderer.getProperty(Property.TRANSFORM) != null) {
                if (renderer instanceof BlockRenderer || renderer instanceof ImageRenderer || renderer instanceof TableRenderer) {
                    AffineTransform rotationTransform = renderer.createTransformationInsideOccupiedArea();
                    transformPoints(contentBoxPoints, rotationTransform);
                }
            }
            renderer = (AbstractRenderer) renderer.parent;
        }

        return calculateBBox(contentBoxPoints);
    }

    /**
     * Calculates bounding box around points.
     *
     * @param points list of the points calculated bbox will enclose.
     * @return array of float values which denote left, bottom, right, top lines of bbox in this specific order
     */
    protected Rectangle calculateBBox(List points) {
        return Rectangle.calculateBBox(points);
    }

    protected List rectangleToPointsList(Rectangle rect) {
        return Arrays.asList(rect.toPointsArray());
    }

    protected List transformPoints(List points, AffineTransform transform) {
        for (Point point : points) {
            transform.transform(point, point);
        }

        return points;
    }

    /**
     * This method calculates the shift needed to be applied to the points in order to position
     * upper and left borders of their bounding box at the given lines.
     *
     * @param left   x coordinate at which points bbox left border is to be aligned
     * @param top    y coordinate at which points bbox upper border is to be aligned
     * @param points the points, which bbox will be aligned at the given position
     * @return array of two floats, where first element denotes x-coordinate shift and the second
     * element denotes y-coordinate shift which are needed to align points bbox at the given lines.
     */
    protected float[] calculateShiftToPositionBBoxOfPointsAt(float left, float top, List points) {
        double minX = Double.MAX_VALUE;
        double maxY = -Double.MAX_VALUE;
        for (Point point : points) {
            minX = Math.min(point.getX(), minX);
            maxY = Math.max(point.getY(), maxY);
        }

        float dx = (float) (left - minX);
        float dy = (float) (top - maxY);
        return new float[]{dx, dy};
    }

    /**
     * Check if corresponding property has point value.
     *
     * @param property {@link Property}
     * @return false if property value either null, or percent, otherwise true.
     */
    protected boolean hasAbsoluteUnitValue(int property) {
        UnitValue value = this.getProperty(property);
        return value != null && value.isPointValue();
    }

    /**
     * Check if corresponding property has relative value.
     *
     * @param property {@link Property}
     * @return false if property value either null, or point, otherwise true.
     */
    protected boolean hasRelativeUnitValue(int property) {
        UnitValue value = this.getProperty(property);
        return value != null && value.isPercentValue();
    }

    boolean isFirstOnRootArea(boolean checkRootAreaOnly) {
        boolean isFirstOnRootArea = true;
        IRenderer ancestor = this;
        while (isFirstOnRootArea && ancestor.getParent() != null) {
            IRenderer parent = ancestor.getParent();
            if (parent instanceof RootRenderer) {
                isFirstOnRootArea = ((RootRenderer) parent).currentArea.isEmptyArea();
            } else if (parent.getOccupiedArea() == null) {
                break;
            } else if (!checkRootAreaOnly) {
                isFirstOnRootArea = parent.getOccupiedArea().getBBox().getHeight() < EPS;
            }
            ancestor = parent;
        }
        return isFirstOnRootArea;
    }

    /**
     * Gets pdf document from root renderers.
     *
     * @return PdfDocument, or null if there are no document
     */
    PdfDocument getPdfDocument() {
        RootRenderer renderer = getRootRenderer();
        if (renderer instanceof DocumentRenderer) {
            final Document document = ((DocumentRenderer) renderer).document;
            return document.getPdfDocument();
        } else if (renderer instanceof CanvasRenderer) {
            return ((CanvasRenderer) renderer).canvas.getPdfDocument();
        } else {
            return null;
        }
    }

    RootRenderer getRootRenderer() {
        IRenderer currentRenderer = this;
        while (currentRenderer instanceof AbstractRenderer) {
            if (currentRenderer instanceof RootRenderer) {
                return (RootRenderer) currentRenderer;
            }
            currentRenderer = ((AbstractRenderer) currentRenderer).getParent();
        }
        return null;
    }

    static float calculateAdditionalWidth(AbstractRenderer renderer) {
        Rectangle dummy = new Rectangle(0, 0);
        renderer.applyMargins(dummy, true);
        renderer.applyBorderBox(dummy, true);
        renderer.applyPaddings(dummy, true);
        return dummy.getWidth();
    }

    static boolean noAbsolutePositionInfo(IRenderer renderer) {
        return !renderer.hasProperty(Property.TOP) && !renderer.hasProperty(Property.BOTTOM) && !renderer.hasProperty(Property.LEFT) && !renderer.hasProperty(Property.RIGHT);
    }

    static Float getPropertyAsFloat(IRenderer renderer, int property) {
        return NumberUtil.asFloat(renderer.getProperty(property));
    }

    /**
     * Returns the property of the renderer as a UnitValue if it exists and is a UnitValue, null otherwise
     *
     * @param renderer renderer to retrieve the property from
     * @param property key for the property to retrieve
     * @return A UnitValue if the property is present and is a UnitValue, null otherwise
     */
    static UnitValue getPropertyAsUnitValue(IRenderer renderer, int property) {
        UnitValue result = renderer.getProperty(property);
        return result;

    }

    void shrinkOccupiedAreaForAbsolutePosition() {
        // In case of absolute positioning and not specified left, right, width values, the parent box is shrunk to fit
        // the children. It does not occupy all the available width if it does not need to.
        if (isAbsolutePosition()) {
            Float left = this.getPropertyAsFloat(Property.LEFT);
            Float right = this.getPropertyAsFloat(Property.RIGHT);
            UnitValue width = this.getProperty(Property.WIDTH);
            if (left == null && right == null && width == null) {
                occupiedArea.getBBox().setWidth(0);
            }
        }
    }

    void drawPositionedChildren(DrawContext drawContext) {
        for (IRenderer positionedChild : positionedRenderers) {
            positionedChild.draw(drawContext);
        }
    }

    FontCharacteristics createFontCharacteristics() {
        FontCharacteristics fc = new FontCharacteristics();
        if (this.hasProperty(Property.FONT_WEIGHT)) {
            fc.setFontWeight((String) this.getProperty(Property.FONT_WEIGHT));
        }
        if (this.hasProperty(Property.FONT_STYLE)) {
            fc.setFontStyle((String) this.getProperty(Property.FONT_STYLE));
        }
        return fc;
    }

    /**
     * Gets any valid {@link PdfFont} for this renderer, based on {@link Property#FONT}, {@link Property#FONT_PROVIDER} and
     * {@link Property#FONT_SET} properties.
     * This method will not change font property of renderer. Also it is not guarantied that returned font will contain
     * all glyphs used in renderer or its children.
     * 

* This method is usually needed for evaluating some layout characteristics like ascender or descender. * * @return a valid {@link PdfFont} instance based on renderer {@link Property#FONT} property. */ PdfFont resolveFirstPdfFont() { Object font = this.getProperty(Property.FONT); if (font instanceof PdfFont) { return (PdfFont) font; } else if (font instanceof String[]) { FontProvider provider = this.getProperty(Property.FONT_PROVIDER); if (provider == null) { throw new IllegalStateException( LayoutExceptionMessageConstant.FONT_PROVIDER_NOT_SET_FONT_FAMILY_NOT_RESOLVED); } FontSet fontSet = this.getProperty(Property.FONT_SET); if (provider.getFontSet().isEmpty() && (fontSet == null || fontSet.isEmpty())) { throw new IllegalStateException( LayoutExceptionMessageConstant.FONT_PROVIDER_NOT_SET_FONT_FAMILY_NOT_RESOLVED); } FontCharacteristics fc = createFontCharacteristics(); return resolveFirstPdfFont((String[]) font, provider, fc, fontSet); } else { throw new IllegalStateException("String[] or PdfFont expected as value of FONT property"); } } /** * Get first valid {@link PdfFont} for this renderer, based on given font-families, font provider and font characteristics. * This method will not change font property of renderer. Also it is not guarantied that returned font will contain * all glyphs used in renderer or its children. *

* This method is usually needed for evaluating some layout characteristics like ascender or descender. * * @return a valid {@link PdfFont} instance based on renderer {@link Property#FONT} property. */ PdfFont resolveFirstPdfFont(String[] font, FontProvider provider, FontCharacteristics fc, FontSet additionalFonts) { FontSelector fontSelector = provider.getFontSelector(Arrays.asList(font), fc, additionalFonts); return provider.getPdfFont(fontSelector.bestMatch(), additionalFonts); } static Border[] getBorders(IRenderer renderer) { Border border = renderer.getProperty(Property.BORDER); Border topBorder = renderer.getProperty(Property.BORDER_TOP); Border rightBorder = renderer.getProperty(Property.BORDER_RIGHT); Border bottomBorder = renderer.getProperty(Property.BORDER_BOTTOM); Border leftBorder = renderer.getProperty(Property.BORDER_LEFT); Border[] borders = {topBorder, rightBorder, bottomBorder, leftBorder}; if (!hasOwnOrModelProperty(renderer, Property.BORDER_TOP)) { borders[0] = border; } if (!hasOwnOrModelProperty(renderer, Property.BORDER_RIGHT)) { borders[1] = border; } if (!hasOwnOrModelProperty(renderer, Property.BORDER_BOTTOM)) { borders[2] = border; } if (!hasOwnOrModelProperty(renderer, Property.BORDER_LEFT)) { borders[3] = border; } return borders; } void applyAbsolutePositionIfNeeded(LayoutContext layoutContext) { if (isAbsolutePosition()) { applyAbsolutePosition(layoutContext instanceof PositionedLayoutContext ? ((PositionedLayoutContext) layoutContext).getParentOccupiedArea().getBBox() : layoutContext.getArea().getBBox()); } } void preparePositionedRendererAndAreaForLayout(IRenderer childPositionedRenderer, Rectangle fullBbox, Rectangle parentBbox) { Float left = getPropertyAsFloat(childPositionedRenderer, Property.LEFT); Float right = getPropertyAsFloat(childPositionedRenderer, Property.RIGHT); Float top = getPropertyAsFloat(childPositionedRenderer, Property.TOP); Float bottom = getPropertyAsFloat(childPositionedRenderer, Property.BOTTOM); childPositionedRenderer.setParent(this); adjustPositionedRendererLayoutBoxWidth(childPositionedRenderer, fullBbox, left, right); if (Integer.valueOf(LayoutPosition.ABSOLUTE).equals(childPositionedRenderer.getProperty(Property.POSITION))) { updateMinHeightForAbsolutelyPositionedRenderer(childPositionedRenderer, parentBbox, top, bottom); } } private void updateMinHeightForAbsolutelyPositionedRenderer(IRenderer renderer, Rectangle parentRendererBox, Float top, Float bottom) { if (top != null && bottom != null && !renderer.hasProperty(Property.HEIGHT)) { UnitValue currentMaxHeight = getPropertyAsUnitValue(renderer, Property.MAX_HEIGHT); UnitValue currentMinHeight = getPropertyAsUnitValue(renderer, Property.MIN_HEIGHT); float resolvedMinHeight = Math.max(0, parentRendererBox.getTop() - (float) top - parentRendererBox.getBottom() - (float) bottom); Rectangle dummy = new Rectangle(0, 0); if (!isBorderBoxSizing(renderer)) { applyPaddings(dummy, getPaddings(renderer), true); applyBorderBox(dummy, getBorders(renderer), true); } applyMargins(dummy, getMargins(renderer), true); resolvedMinHeight -= dummy.getHeight(); if (currentMinHeight != null) { resolvedMinHeight = Math.max(resolvedMinHeight, currentMinHeight.getValue()); } if (currentMaxHeight != null) { resolvedMinHeight = Math.min(resolvedMinHeight, currentMaxHeight.getValue()); } renderer.setProperty(Property.MIN_HEIGHT, UnitValue.createPointValue((float) resolvedMinHeight)); renderer.setProperty(Property.HEIGHT, UnitValue.createPointValue((float) resolvedMinHeight)); } } private void adjustPositionedRendererLayoutBoxWidth(IRenderer renderer, Rectangle fullBbox, Float left, Float right) { if (left != null) { fullBbox.setWidth(fullBbox.getWidth() - (float) left).setX(fullBbox.getX() + (float) left); } if (right != null) { fullBbox.setWidth(fullBbox.getWidth() - (float) right); } if (left == null && right == null && !renderer.hasProperty(Property.WIDTH)) { // Other, non-block renderers won't occupy full width anyway MinMaxWidth minMaxWidth = renderer instanceof BlockRenderer ? ((BlockRenderer) renderer).getMinMaxWidth() : null; if (minMaxWidth != null && minMaxWidth.getMaxWidth() < fullBbox.getWidth()) { fullBbox.setWidth(minMaxWidth.getMaxWidth() + AbstractRenderer.EPS); } } } static float calculatePaddingBorderWidth(AbstractRenderer renderer) { Rectangle dummy = new Rectangle(0, 0); renderer.applyBorderBox(dummy, true); renderer.applyPaddings(dummy, true); return dummy.getWidth(); } static float calculatePaddingBorderHeight(AbstractRenderer renderer) { Rectangle dummy = new Rectangle(0, 0); renderer.applyBorderBox(dummy, true); renderer.applyPaddings(dummy, true); return dummy.getHeight(); } /** * This method creates {@link AffineTransform} instance that could be used * to transform content inside the occupied area, * considering the centre of the occupiedArea as the origin of a coordinate system for transformation. * * @return {@link AffineTransform} that transforms the content and places it inside occupied area. */ private AffineTransform createTransformationInsideOccupiedArea() { Rectangle backgroundArea = applyMargins(occupiedArea.clone().getBBox(), false); float x = backgroundArea.getX(); float y = backgroundArea.getY(); float height = backgroundArea.getHeight(); float width = backgroundArea.getWidth(); AffineTransform transform = AffineTransform.getTranslateInstance(-1 * (x + width / 2), -1 * (y + height / 2)); transform.preConcatenate(Transform.getAffineTransform(this.getProperty(Property.TRANSFORM), width, height)); transform.preConcatenate(AffineTransform.getTranslateInstance(x + width / 2, y + height / 2)); return transform; } protected void beginTransformationIfApplied(PdfCanvas canvas) { if (this.getProperty(Property.TRANSFORM) != null) { AffineTransform transform = createTransformationInsideOccupiedArea(); canvas.saveState().concatMatrix(transform); } } protected void endTransformationIfApplied(PdfCanvas canvas) { if (this.getProperty(Property.TRANSFORM) != null) { canvas.restoreState(); } } /** * Add the specified {@link IRenderer renderer} to the end of children list and update its * parent link to {@code this}. * * @param child the {@link IRenderer child renderer} to be add */ void addChildRenderer(IRenderer child) { child.setParent(this); this.childRenderers.add(child); } /** * Add the specified collection of {@link IRenderer renderers} to the end of children list and * update their parent links to {@code this}. * * @param children the collection of {@link IRenderer child renderers} to be add */ void addAllChildRenderers(List children) { if (children == null) { return; } setThisAsParent(children); this.childRenderers.addAll(children); } /** * Inserts the specified collection of {@link IRenderer renderers} at the specified space of * children list and update their parent links to {@code this}. * * @param index index at which to insert the first element from the specified collection * @param children the collection of {@link IRenderer child renderers} to be add */ void addAllChildRenderers(int index, List children) { setThisAsParent(children); this.childRenderers.addAll(index, children); } /** * Set the specified collection of {@link IRenderer renderers} as the children for {@code this} * element. That meant that the old collection would be cleaned, all parent links in old * children to {@code this} would be erased (i.e. set to {@code null}) and then the specified * list of children would be added similar to {@link AbstractRenderer#addAllChildRenderers(List)}. * * * @param children the collection of children {@link IRenderer renderers} to be set */ void setChildRenderers(List children) { removeThisFromParents(this.childRenderers); this.childRenderers.clear(); addAllChildRenderers(children); } /** * Remove the child {@link IRenderer renderer} at the specified place. If the removed renderer has * the parent link set to {@code this} and it would not present in the children list after * removal, then the parent link of the removed renderer would be erased (i.e. set to {@code null}. * * @param index the index of the renderer to be removed * @return the removed renderer */ IRenderer removeChildRenderer(int index) { final IRenderer removed = this.childRenderers.remove(index); removeThisFromParent(removed); return removed; } /** * Remove the children {@link IRenderer renderers} which are contains in the specified collection. * If some of the removed renderers has the parent link set to {@code this}, then * the parent link of the removed renderer would be erased (i.e. set to {@code null}. * * @param children the collections of renderers to be removed from children list * @return {@code true} if the children list has been changed */ boolean removeAllChildRenderers(Collection children) { removeThisFromParents(children); return this.childRenderers.removeAll(children); } /** * Update the child {@link IRenderer renderer} at the specified place with the specified one. * If the removed renderer has the parent link set to {@code this}, then it would be erased * (i.e. set to {@code null}). * * @param index the index of the renderer to be updated * @param child the renderer to be set * @return the removed renderer */ IRenderer setChildRenderer(int index, IRenderer child) { if (child != null) { child.setParent(this); } final IRenderer removedElement = this.childRenderers.set(index, child); removeThisFromParent(removedElement); return removedElement; } /** * Sets current {@link AbstractRenderer} as parent to renderers in specified collection. * @param children the collection of renderers to set the parent renderer on */ void setThisAsParent(Collection children) { for (final IRenderer child : children) { child.setParent(this); } } boolean logWarningIfGetNextRendererNotOverridden(Class baseClass, Class rendererClass) { if (baseClass != rendererClass) { final Logger logger = LoggerFactory.getLogger(baseClass); logger.warn(MessageFormatUtil.format(IoLogMessageConstant.GET_NEXT_RENDERER_SHOULD_BE_OVERRIDDEN)); return false; } else { return true; } } private void removeThisFromParent(IRenderer toRemove) { // we need to be sure that the removed element has no other entries in child renderers list if (toRemove != null && this == toRemove.getParent() && !this.childRenderers.contains(toRemove)) { toRemove.setParent(null); } } private void removeThisFromParents(Collection children) { for (final IRenderer child : children) { if (child != null && this == child.getParent()) { child.setParent(null); } } } private static UnitValue[] getMargins(IRenderer renderer) { return new UnitValue[]{renderer.getProperty(Property.MARGIN_TOP), renderer.getProperty(Property.MARGIN_RIGHT), renderer.getProperty(Property.MARGIN_BOTTOM), renderer.getProperty(Property.MARGIN_LEFT)}; } private static BorderRadius[] getBorderRadii(IRenderer renderer) { BorderRadius radius = renderer.getProperty(Property.BORDER_RADIUS); BorderRadius topLeftRadius = renderer.getProperty(Property.BORDER_TOP_LEFT_RADIUS); BorderRadius topRightRadius = renderer.getProperty(Property.BORDER_TOP_RIGHT_RADIUS); BorderRadius bottomRightRadius = renderer.getProperty(Property.BORDER_BOTTOM_RIGHT_RADIUS); BorderRadius bottomLeftRadius = renderer.getProperty(Property.BORDER_BOTTOM_LEFT_RADIUS); BorderRadius[] borderRadii = {topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius}; if (!hasOwnOrModelProperty(renderer, Property.BORDER_TOP_LEFT_RADIUS)) { borderRadii[0] = radius; } if (!hasOwnOrModelProperty(renderer, Property.BORDER_TOP_RIGHT_RADIUS)) { borderRadii[1] = radius; } if (!hasOwnOrModelProperty(renderer, Property.BORDER_BOTTOM_RIGHT_RADIUS)) { borderRadii[2] = radius; } if (!hasOwnOrModelProperty(renderer, Property.BORDER_BOTTOM_LEFT_RADIUS)) { borderRadii[3] = radius; } return borderRadii; } private static UnitValue[] getPaddings(IRenderer renderer) { return new UnitValue[]{renderer.getProperty(Property.PADDING_TOP), renderer.getProperty(Property.PADDING_RIGHT), renderer.getProperty(Property.PADDING_BOTTOM), renderer.getProperty(Property.PADDING_LEFT)}; } private static boolean hasOwnOrModelProperty(IRenderer renderer, int property) { return renderer.hasOwnProperty(property) || (null != renderer.getModelElement() && renderer.getModelElement().hasProperty(property)); } private static PdfStructElem getCurrentStructElem(PdfDocument document) { TagStructureContext context = document.getTagStructureContext(); TagTreePointer tagPointer = context.getAutoTaggingPointer(); return context.getPointerStructElem(tagPointer); } }