Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.itextpdf.layout.renderer.LineRenderer Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2023 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.actions.contexts.IMetaInfo;
import com.itextpdf.commons.actions.sequence.SequenceId;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.font.otf.Glyph;
import com.itextpdf.io.font.otf.GlyphLine;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.util.ArrayUtil;
import com.itextpdf.io.util.TextUtil;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.layout.element.TabStop;
import com.itextpdf.layout.layout.LayoutArea;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.layout.LineLayoutContext;
import com.itextpdf.layout.layout.LineLayoutResult;
import com.itextpdf.layout.layout.MinMaxWidthLayoutResult;
import com.itextpdf.layout.layout.TextLayoutResult;
import com.itextpdf.layout.minmaxwidth.MinMaxWidth;
import com.itextpdf.layout.minmaxwidth.MinMaxWidthUtils;
import com.itextpdf.layout.properties.BaseDirection;
import com.itextpdf.layout.properties.FloatPropertyValue;
import com.itextpdf.layout.properties.InlineVerticalAlignment;
import com.itextpdf.layout.properties.InlineVerticalAlignmentType;
import com.itextpdf.layout.properties.Leading;
import com.itextpdf.layout.properties.OverflowPropertyValue;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.properties.RenderingMode;
import com.itextpdf.layout.properties.TabAlignment;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.renderer.TextSequenceWordWrapping.LastFittingChildRendererData;
import com.itextpdf.layout.renderer.TextSequenceWordWrapping.MinMaxWidthOfTextRendererSequenceHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LineRenderer extends AbstractRenderer {
// AbstractRenderer.EPS is not enough here
private static final float MIN_MAX_WIDTH_CORRECTION_EPS = 0.001f;
private static final Logger logger = LoggerFactory.getLogger(LineRenderer.class);
protected float maxAscent;
protected float maxDescent;
// bidi levels
protected byte[] levels;
float maxTextAscent;
float maxTextDescent;
private float maxBlockAscent;
private float maxBlockDescent;
@Override
public LayoutResult layout(LayoutContext layoutContext) {
boolean textSequenceOverflowXProcessing = false;
int firstChildToRelayout = -1;
Rectangle layoutBox = layoutContext.getArea().getBBox().clone();
boolean wasParentsHeightClipped = layoutContext.isClippedHeight();
List floatRendererAreas = layoutContext.getFloatRendererAreas();
OverflowPropertyValue oldXOverflow = null;
boolean wasXOverflowChanged = false;
boolean floatsPlacedBeforeLine = false;
if (floatRendererAreas != null) {
float layoutWidth = layoutBox.getWidth();
float layoutHeight = layoutBox.getHeight();
// consider returning some value to check if layoutBox has been changed due to floats,
// than reuse on non-float layout: kind of not first piece of content on the line
FloatingHelper.adjustLineAreaAccordingToFloats(floatRendererAreas, layoutBox);
if (layoutWidth > layoutBox.getWidth() || layoutHeight > layoutBox.getHeight()) {
floatsPlacedBeforeLine = true;
oldXOverflow = this.getProperty(Property.OVERFLOW_X);
wasXOverflowChanged = true;
setProperty(Property.OVERFLOW_X, OverflowPropertyValue.FIT);
}
}
boolean noSoftWrap = Boolean.TRUE.equals(this.getOwnProperty(Property.NO_SOFT_WRAP_INLINE));
LineLayoutContext lineLayoutContext =
layoutContext instanceof LineLayoutContext ? (LineLayoutContext) layoutContext
: new LineLayoutContext(layoutContext);
if (lineLayoutContext.getTextIndent() != 0) {
layoutBox
.moveRight(lineLayoutContext.getTextIndent())
.setWidth(layoutBox.getWidth() - lineLayoutContext.getTextIndent());
}
occupiedArea = new LayoutArea(layoutContext.getArea().getPageNumber(),
layoutBox.clone().moveUp(layoutBox.getHeight()).setHeight(0).setWidth(0));
updateChildrenParent();
TargetCounterHandler.addPageByID(this);
float curWidth = 0;
if (RenderingMode.HTML_MODE.equals(this.getProperty(Property.RENDERING_MODE))
&& hasChildRendererInHtmlMode()) {
float[] ascenderDescender = LineHeightHelper.getActualAscenderDescender(this);
maxAscent = ascenderDescender[0];
maxDescent = ascenderDescender[1];
} else {
maxAscent = 0;
maxDescent = 0;
}
maxTextAscent = 0;
maxTextDescent = 0;
maxBlockAscent = -1e20f;
maxBlockDescent = 1e20f;
int childPos = 0;
MinMaxWidth minMaxWidth = new MinMaxWidth();
AbstractWidthHandler widthHandler;
if (noSoftWrap) {
widthHandler = new SumSumWidthHandler(minMaxWidth);
} else {
widthHandler = new MaxSumWidthHandler(minMaxWidth);
}
resolveChildrenFonts();
int totalNumberOfTrimmedGlyphs = trimFirst();
BaseDirection baseDirection = applyOtf();
updateBidiLevels(totalNumberOfTrimmedGlyphs, baseDirection);
boolean anythingPlaced = false;
TabStop hangingTabStop = null;
LineLayoutResult result = null;
boolean floatsPlacedInLine = false;
Map floatsToNextPageSplitRenderers = new LinkedHashMap<>();
List floatsToNextPageOverflowRenderers = new ArrayList<>();
List floatsOverflowedToNextLine = new ArrayList<>();
int lastTabIndex = 0;
Map specialScriptLayoutResults = new HashMap<>();
Map textRendererLayoutResults = new HashMap<>();
Map textRendererSequenceAscentDescent = new HashMap<>();
LineAscentDescentState lineAscentDescentStateBeforeTextRendererSequence = null;
MinMaxWidthOfTextRendererSequenceHelper minMaxWidthOfTextRendererSequenceHelper = null;
while (childPos < getChildRenderers().size()) {
IRenderer childRenderer = getChildRenderers().get(childPos);
LayoutResult childResult = null;
Rectangle bbox = new Rectangle(layoutBox.getX() + curWidth, layoutBox.getY(),
layoutBox.getWidth() - curWidth, layoutBox.getHeight());
RenderingMode childRenderingMode = childRenderer.getProperty(Property.RENDERING_MODE);
if (TextSequenceWordWrapping.isTextRendererAndRequiresSpecialScriptPreLayoutProcessing(childRenderer)
&& TypographyUtils.isPdfCalligraphAvailable()) {
TextSequenceWordWrapping.processSpecialScriptPreLayout(this, childPos);
}
TextSequenceWordWrapping.resetTextSequenceIfItEnded(
specialScriptLayoutResults, true, childRenderer, childPos,
minMaxWidthOfTextRendererSequenceHelper, noSoftWrap, widthHandler);
TextSequenceWordWrapping.resetTextSequenceIfItEnded(
textRendererLayoutResults, false, childRenderer, childPos,
minMaxWidthOfTextRendererSequenceHelper, noSoftWrap, widthHandler);
if (childRenderer instanceof TextRenderer) {
// Delete these properties in case of relayout. We might have applied them during justify().
childRenderer.deleteOwnProperty(Property.CHARACTER_SPACING);
childRenderer.deleteOwnProperty(Property.WORD_SPACING);
} else if (childRenderer instanceof TabRenderer) {
if (hangingTabStop != null) {
IRenderer tabRenderer = getChildRenderers().get(childPos - 1);
tabRenderer.layout(new LayoutContext(new LayoutArea(layoutContext.getArea().getPageNumber(), bbox),
wasParentsHeightClipped));
curWidth += tabRenderer.getOccupiedArea().getBBox().getWidth();
widthHandler.updateMaxChildWidth(tabRenderer.getOccupiedArea().getBBox().getWidth());
}
hangingTabStop = calculateTab(childRenderer, curWidth, layoutBox.getWidth());
if (childPos == getChildRenderers().size() - 1) {
hangingTabStop = null;
}
if (hangingTabStop != null) {
lastTabIndex = childPos;
++childPos;
continue;
}
}
if (hangingTabStop != null && hangingTabStop.getTabAlignment() == TabAlignment.ANCHOR
&& childRenderer instanceof TextRenderer) {
childRenderer.setProperty(Property.TAB_ANCHOR, hangingTabStop.getTabAnchor());
}
// Normalize child width
Object childWidth = childRenderer.getProperty(Property.WIDTH);
boolean childWidthWasReplaced = false;
boolean childRendererHasOwnWidthProperty = childRenderer.hasOwnProperty(Property.WIDTH);
if (childWidth instanceof UnitValue && ((UnitValue) childWidth).isPercentValue()) {
float normalizedChildWidth =
((UnitValue) childWidth).getValue() / 100 * layoutContext.getArea().getBBox().getWidth();
normalizedChildWidth = decreaseRelativeWidthByChildAdditionalWidth(childRenderer, normalizedChildWidth);
if (normalizedChildWidth > 0) {
childRenderer.setProperty(Property.WIDTH, UnitValue.createPointValue(normalizedChildWidth));
childWidthWasReplaced = true;
}
}
FloatPropertyValue kidFloatPropertyVal = childRenderer.getProperty(Property.FLOAT);
boolean isChildFloating =
childRenderer instanceof AbstractRenderer && FloatingHelper.isRendererFloating(childRenderer,
kidFloatPropertyVal);
if (isChildFloating) {
childResult = null;
MinMaxWidth kidMinMaxWidth = FloatingHelper.calculateMinMaxWidthForFloat(
(AbstractRenderer) childRenderer, kidFloatPropertyVal);
float floatingBoxFullWidth = kidMinMaxWidth.getMaxWidth();
// Width will be recalculated on float layout;
// also not taking it into account (i.e. not setting it on child renderer) results in differences with html
// when floating span is split on other line;
// TODO DEVSIX-1730: may be process floating spans as inline blocks always?
if (!wasXOverflowChanged && childPos > 0) {
oldXOverflow = this.getProperty(Property.OVERFLOW_X);
wasXOverflowChanged = true;
setProperty(Property.OVERFLOW_X, OverflowPropertyValue.FIT);
}
if (!lineLayoutContext.isFloatOverflowedToNextPageWithNothing() && floatsOverflowedToNextLine.isEmpty()
&& (!anythingPlaced || floatingBoxFullWidth <= bbox.getWidth())) {
childResult = childRenderer.layout(new LayoutContext(
new LayoutArea(layoutContext.getArea().getPageNumber(),
layoutContext.getArea().getBBox().clone()), null, floatRendererAreas,
wasParentsHeightClipped));
}
// Get back child width so that it's not lost
if (childWidthWasReplaced) {
if (childRendererHasOwnWidthProperty) {
childRenderer.setProperty(Property.WIDTH, childWidth);
} else {
childRenderer.deleteOwnProperty(Property.WIDTH);
}
}
float minChildWidth = 0;
float maxChildWidth = 0;
if (childResult instanceof MinMaxWidthLayoutResult) {
if (!childWidthWasReplaced) {
minChildWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth().getMinWidth();
}
maxChildWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth().getMaxWidth();
widthHandler.updateMinChildWidth(minChildWidth + AbstractRenderer.EPS);
widthHandler.updateMaxChildWidth(maxChildWidth + AbstractRenderer.EPS);
} else {
widthHandler.updateMinChildWidth(kidMinMaxWidth.getMinWidth() + AbstractRenderer.EPS);
widthHandler.updateMaxChildWidth(kidMinMaxWidth.getMaxWidth() + AbstractRenderer.EPS);
}
if (childResult == null && !lineLayoutContext.isFloatOverflowedToNextPageWithNothing()) {
floatsOverflowedToNextLine.add(childRenderer);
} else if (lineLayoutContext.isFloatOverflowedToNextPageWithNothing()
|| childResult.getStatus() == LayoutResult.NOTHING) {
floatsToNextPageSplitRenderers.put(childPos, null);
floatsToNextPageOverflowRenderers.add(childRenderer);
lineLayoutContext.setFloatOverflowedToNextPageWithNothing(true);
} else if (childResult.getStatus() == LayoutResult.PARTIAL) {
floatsPlacedInLine = true;
if (childRenderer instanceof TextRenderer) {
// This code is specifically for floating inline text elements:
// inline elements cannot have fixed width, also they progress horizontally, which means
// that if they don't fit in one line, they will definitely be moved onto the new line (and also
// under all floats). Specifying the whole width of layout area is required to avoid possible normal
// content wrapping around floating text in case floating text gets wrapped onto the next line
// not evenly.
LineRenderer[] split = splitNotFittingFloat(childPos, childResult);
IRenderer splitRenderer = childResult.getSplitRenderer();
if (splitRenderer instanceof TextRenderer) {
((TextRenderer) splitRenderer).trimFirst();
((TextRenderer) splitRenderer).trimLast();
}
// ensure no other thing (like text wrapping the float) will occupy the line
splitRenderer.getOccupiedArea().getBBox()
.setWidth(layoutContext.getArea().getBBox().getWidth());
result = new LineLayoutResult(LayoutResult.PARTIAL, occupiedArea, split[0], split[1], null);
break;
} else {
floatsToNextPageSplitRenderers.put(childPos, childResult.getSplitRenderer());
floatsToNextPageOverflowRenderers.add(childResult.getOverflowRenderer());
adjustLineOnFloatPlaced(layoutBox, childPos, kidFloatPropertyVal,
childResult.getSplitRenderer().getOccupiedArea().getBBox());
}
} else {
floatsPlacedInLine = true;
if (childRenderer instanceof TextRenderer) {
((TextRenderer) childRenderer).trimFirst();
((TextRenderer) childRenderer).trimLast();
}
adjustLineOnFloatPlaced(layoutBox, childPos, kidFloatPropertyVal,
childRenderer.getOccupiedArea().getBBox());
}
childPos++;
if (!anythingPlaced && childResult != null && childResult.getStatus() == LayoutResult.NOTHING
&& floatRendererAreas.isEmpty()) {
if (isFirstOnRootArea()) {
// Current line is empty, kid returns nothing and neither floats nor content
// were met on root area (e.g. page area) - return NOTHING, don't layout other line content,
// expect FORCED_PLACEMENT to be set.
break;
}
}
continue;
}
MinMaxWidth childBlockMinMaxWidth = null;
boolean isInlineBlockChild = isInlineBlockChild(childRenderer);
if (isInlineBlockChild && childRenderer instanceof AbstractRenderer) {
final MinMaxWidth childBlockMinMaxWidthLocal = ((AbstractRenderer) childRenderer).getMinMaxWidth();
// Don't calculate childBlockMinMaxWidth in case of relative width here
// and further (childBlockMinMaxWidth != null)
if (!childWidthWasReplaced) {
childBlockMinMaxWidth = childBlockMinMaxWidthLocal;
}
float childMaxWidth = childBlockMinMaxWidthLocal.getMaxWidth();
float lineFullAvailableWidth = layoutContext.getArea().getBBox().getWidth() - lineLayoutContext.getTextIndent();
if (!noSoftWrap && childMaxWidth > bbox.getWidth() + MIN_MAX_WIDTH_CORRECTION_EPS && bbox.getWidth() != lineFullAvailableWidth) {
childResult = new LineLayoutResult(LayoutResult.NOTHING, null, null, childRenderer, childRenderer);
} else {
if (childBlockMinMaxWidth != null) {
childMaxWidth += MIN_MAX_WIDTH_CORRECTION_EPS;
float inlineBlockWidth = Math.min(childMaxWidth, lineFullAvailableWidth);
if (!isOverflowFit(this.getProperty(Property.OVERFLOW_X))) {
float childMinWidth = childBlockMinMaxWidth.getMinWidth() + MIN_MAX_WIDTH_CORRECTION_EPS;
inlineBlockWidth = Math.max(childMinWidth, inlineBlockWidth);
}
bbox.setWidth(inlineBlockWidth);
if (childBlockMinMaxWidth.getMinWidth() > bbox.getWidth()) {
if (logger.isWarnEnabled()) {
logger.warn(IoLogMessageConstant.INLINE_BLOCK_ELEMENT_WILL_BE_CLIPPED);
}
childRenderer.setProperty(Property.FORCED_PLACEMENT, true);
}
}
}
if (childBlockMinMaxWidth != null) {
childBlockMinMaxWidth.setChildrenMaxWidth(
childBlockMinMaxWidth.getChildrenMaxWidth() + MIN_MAX_WIDTH_CORRECTION_EPS);
childBlockMinMaxWidth.setChildrenMinWidth(
childBlockMinMaxWidth.getChildrenMinWidth() + MIN_MAX_WIDTH_CORRECTION_EPS);
}
}
boolean shouldBreakLayouting = false;
if (childResult == null) {
boolean setOverflowFitCausedBySpecialScripts = childRenderer instanceof TextRenderer
&& ((TextRenderer) childRenderer).textContainsSpecialScriptGlyphs(true);
boolean setOverflowFitCausedByTextRendererInHtmlMode = RenderingMode.HTML_MODE == childRenderingMode
&& childRenderer instanceof TextRenderer
&& !((TextRenderer) childRenderer).textContainsSpecialScriptGlyphs(true);
if (!wasXOverflowChanged
&& (childPos > 0 || setOverflowFitCausedBySpecialScripts
|| setOverflowFitCausedByTextRendererInHtmlMode)
&& !textSequenceOverflowXProcessing) {
oldXOverflow = this.getProperty(Property.OVERFLOW_X);
wasXOverflowChanged = true;
setProperty(Property.OVERFLOW_X, OverflowPropertyValue.FIT);
}
TextSequenceWordWrapping.preprocessTextSequenceOverflowX(this, textSequenceOverflowXProcessing,
childRenderer, wasXOverflowChanged, oldXOverflow);
childResult = childRenderer.layout(
new LayoutContext(new LayoutArea(layoutContext.getArea().getPageNumber(), bbox),
wasParentsHeightClipped));
shouldBreakLayouting = TextSequenceWordWrapping.postprocessTextSequenceOverflowX(
this, textSequenceOverflowXProcessing,
childPos, childRenderer, childResult, wasXOverflowChanged);
TextSequenceWordWrapping.updateTextSequenceLayoutResults(
textRendererLayoutResults, false, childRenderer, childPos, childResult);
TextSequenceWordWrapping.updateTextSequenceLayoutResults(
specialScriptLayoutResults, true, childRenderer, childPos, childResult);
// it means that we've already increased layout area by MIN_MAX_WIDTH_CORRECTION_EPS
if (childResult instanceof MinMaxWidthLayoutResult && null != childBlockMinMaxWidth) {
MinMaxWidth childResultMinMaxWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth();
childResultMinMaxWidth.setChildrenMaxWidth(
childResultMinMaxWidth.getChildrenMaxWidth() + MIN_MAX_WIDTH_CORRECTION_EPS);
childResultMinMaxWidth.setChildrenMinWidth(
childResultMinMaxWidth.getChildrenMinWidth() + MIN_MAX_WIDTH_CORRECTION_EPS);
}
}
// Get back child width so that it's not lost
if (childWidthWasReplaced) {
if (childRendererHasOwnWidthProperty) {
childRenderer.setProperty(Property.WIDTH, childWidth);
} else {
childRenderer.deleteOwnProperty(Property.WIDTH);
}
}
float minChildWidth = 0;
float maxChildWidth = 0;
if (childResult instanceof MinMaxWidthLayoutResult) {
if (!childWidthWasReplaced) {
minChildWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth().getMinWidth();
}
maxChildWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth().getMaxWidth();
} else if (childBlockMinMaxWidth != null) {
minChildWidth = childBlockMinMaxWidth.getMinWidth();
maxChildWidth = childBlockMinMaxWidth.getMaxWidth();
}
float[] childAscentDescent = getAscentDescentOfLayoutedChildRenderer(childRenderer, childResult,
childRenderingMode, isInlineBlockChild);
lineAscentDescentStateBeforeTextRendererSequence =
TextSequenceWordWrapping.updateTextRendererSequenceAscentDescent(
this, textRendererSequenceAscentDescent, childPos, childAscentDescent,
lineAscentDescentStateBeforeTextRendererSequence);
minMaxWidthOfTextRendererSequenceHelper =
TextSequenceWordWrapping.updateTextRendererSequenceMinMaxWidth(
this, widthHandler, childPos,
minMaxWidthOfTextRendererSequenceHelper, anythingPlaced, textRendererLayoutResults,
specialScriptLayoutResults, lineLayoutContext.getTextIndent());
boolean newLineOccurred = (childResult instanceof TextLayoutResult
&& ((TextLayoutResult) childResult).isSplitForcedByNewline());
if (!shouldBreakLayouting) {
shouldBreakLayouting = childResult.getStatus() != LayoutResult.FULL || newLineOccurred;
}
boolean shouldBreakLayoutingOnTextRenderer = shouldBreakLayouting
&& childResult instanceof TextLayoutResult;
boolean forceOverflowForTextRendererPartialResult = false;
if (shouldBreakLayoutingOnTextRenderer) {
boolean isWordHasBeenSplitLayoutRenderingMode = ((TextLayoutResult) childResult).isWordHasBeenSplit()
&& RenderingMode.HTML_MODE != childRenderingMode
&& !((TextRenderer) childRenderer).textContainsSpecialScriptGlyphs(true);
boolean enableSpecialScriptsWrapping = ((TextRenderer) getChildRenderers().get(childPos))
.textContainsSpecialScriptGlyphs(true)
&& !textSequenceOverflowXProcessing && !newLineOccurred;
boolean enableTextSequenceWrapping = RenderingMode.HTML_MODE == childRenderingMode && !newLineOccurred
&& !textSequenceOverflowXProcessing;
if (isWordHasBeenSplitLayoutRenderingMode) {
forceOverflowForTextRendererPartialResult = isForceOverflowForTextRendererPartialResult(
childRenderer, wasXOverflowChanged, oldXOverflow, layoutContext, layoutBox,
wasParentsHeightClipped);
} else if (enableSpecialScriptsWrapping) {
boolean isOverflowFit = wasXOverflowChanged
? (oldXOverflow == OverflowPropertyValue.FIT)
: isOverflowFit(this.getProperty(Property.OVERFLOW_X));
LastFittingChildRendererData lastFittingChildRendererData =
TextSequenceWordWrapping.getIndexAndLayoutResultOfTheLastTextRendererContainingSpecialScripts(
this, childPos,
specialScriptLayoutResults, wasParentsHeightClipped,
isOverflowFit);
if (lastFittingChildRendererData == null) {
textSequenceOverflowXProcessing = true;
shouldBreakLayouting = false;
firstChildToRelayout = childPos;
} else {
curWidth -= TextSequenceWordWrapping.getCurWidthRelayoutedTextSequenceDecrement(childPos,
lastFittingChildRendererData.childIndex, specialScriptLayoutResults);
childPos = lastFittingChildRendererData.childIndex;
childResult = lastFittingChildRendererData.childLayoutResult;
specialScriptLayoutResults.put(childPos, childResult);
MinMaxWidth textSequenceElemminMaxWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth();
minChildWidth = textSequenceElemminMaxWidth.getMinWidth();
maxChildWidth = textSequenceElemminMaxWidth.getMaxWidth();
}
} else if (enableTextSequenceWrapping) {
boolean isOverflowFit = wasXOverflowChanged
? (oldXOverflow == OverflowPropertyValue.FIT)
: isOverflowFit(this.getProperty(Property.OVERFLOW_X));
LastFittingChildRendererData lastFittingChildRendererData =
TextSequenceWordWrapping.getIndexAndLayoutResultOfTheLastTextRendererWithNoSpecialScripts(
this, childPos,
textRendererLayoutResults, wasParentsHeightClipped,
isOverflowFit, floatsPlacedInLine || floatsPlacedBeforeLine);
if (lastFittingChildRendererData == null) {
textSequenceOverflowXProcessing = true;
shouldBreakLayouting = false;
firstChildToRelayout = childPos;
} else {
curWidth -= TextSequenceWordWrapping.getCurWidthRelayoutedTextSequenceDecrement(childPos,
lastFittingChildRendererData.childIndex, textRendererLayoutResults);
childAscentDescent =
updateAscentDescentAfterTextRendererSequenceProcessing(
(lastFittingChildRendererData.childLayoutResult.getStatus()
== LayoutResult.NOTHING)
? (lastFittingChildRendererData.childIndex - 1)
: lastFittingChildRendererData.childIndex,
lineAscentDescentStateBeforeTextRendererSequence,
textRendererSequenceAscentDescent);
childPos = lastFittingChildRendererData.childIndex;
childResult = lastFittingChildRendererData.childLayoutResult;
if (0 == childPos && LayoutResult.NOTHING == childResult.getStatus()) {
anythingPlaced = false;
}
textRendererLayoutResults.put(childPos, childResult);
MinMaxWidth textSequenceElemminMaxWidth = ((MinMaxWidthLayoutResult) childResult).getMinMaxWidth();
minChildWidth = textSequenceElemminMaxWidth.getMinWidth();
maxChildWidth = textSequenceElemminMaxWidth.getMaxWidth();
}
}
}
if (childPos != firstChildToRelayout) {
if (!forceOverflowForTextRendererPartialResult) {
updateAscentDescentAfterChildLayout(childAscentDescent, childRenderer, isChildFloating);
}
float maxHeight = maxAscent - maxDescent;
float currChildTextIndent = anythingPlaced ? 0 : lineLayoutContext.getTextIndent();
if (hangingTabStop != null && (
TabAlignment.LEFT == hangingTabStop.getTabAlignment()
|| shouldBreakLayouting
|| getChildRenderers().size() - 1 == childPos
|| getChildRenderers().get(childPos + 1) instanceof TabRenderer)) {
IRenderer tabRenderer = getChildRenderers().get(lastTabIndex);
List affectedRenderers = new ArrayList<>();
affectedRenderers.addAll(getChildRenderers().subList(lastTabIndex + 1, childPos + 1));
float tabWidth = calculateTab(layoutBox, curWidth, hangingTabStop, affectedRenderers, tabRenderer);
tabRenderer.layout(new LayoutContext(new LayoutArea(layoutContext.getArea().getPageNumber(), bbox),
wasParentsHeightClipped));
float sumOfAffectedRendererWidths = 0;
for (IRenderer renderer : affectedRenderers) {
renderer.move(tabWidth + sumOfAffectedRendererWidths, 0);
sumOfAffectedRendererWidths += renderer.getOccupiedArea().getBBox().getWidth();
}
if (childResult.getSplitRenderer() != null) {
childResult.getSplitRenderer()
.move(tabWidth + sumOfAffectedRendererWidths - childResult.getSplitRenderer()
.getOccupiedArea().getBBox().getWidth(), 0);
}
float tabAndNextElemWidth = tabWidth + childResult.getOccupiedArea().getBBox().getWidth();
if (hangingTabStop.getTabAlignment() == TabAlignment.RIGHT
&& curWidth + tabAndNextElemWidth < hangingTabStop.getTabPosition()) {
curWidth = hangingTabStop.getTabPosition();
} else {
curWidth += tabAndNextElemWidth;
}
widthHandler.updateMinChildWidth(minChildWidth + currChildTextIndent);
widthHandler.updateMaxChildWidth(tabWidth + maxChildWidth + currChildTextIndent);
hangingTabStop = null;
} else if (null == hangingTabStop) {
if (childResult.getOccupiedArea() != null && childResult.getOccupiedArea().getBBox() != null) {
curWidth += childResult.getOccupiedArea().getBBox().getWidth();
}
widthHandler.updateMinChildWidth(minChildWidth + currChildTextIndent);
widthHandler.updateMaxChildWidth(maxChildWidth + currChildTextIndent);
}
if (!forceOverflowForTextRendererPartialResult) {
occupiedArea.setBBox(
new Rectangle(layoutBox.getX(), layoutBox.getY() + layoutBox.getHeight() - maxHeight,
curWidth, maxHeight));
}
}
if (shouldBreakLayouting) {
LineRenderer[] split = split();
split[0].setChildRenderers(getChildRenderers().subList(0, childPos));
if (forceOverflowForTextRendererPartialResult) {
split[1].addChildRenderer(childRenderer);
} else {
boolean forcePlacement = Boolean.TRUE.equals(getPropertyAsBoolean(Property.FORCED_PLACEMENT));
boolean isInlineBlockAndFirstOnRootArea = isInlineBlockChild && isFirstOnRootArea();
if ((childResult.getStatus() == LayoutResult.PARTIAL
&& (!isInlineBlockChild || forcePlacement || isInlineBlockAndFirstOnRootArea))
|| childResult.getStatus() == LayoutResult.FULL) {
final IRenderer splitRenderer = childResult.getSplitRenderer();
split[0].addChild(splitRenderer);
// TODO: DEVSIX-4717 this code should be removed if/when the AbstractRenderer
// would start using the newly added methods
if (splitRenderer.getParent() != split[0] && split[0].childRenderers.contains(splitRenderer)) {
splitRenderer.setParent(split[0]);
}
anythingPlaced = true;
}
if (null != childResult.getOverflowRenderer()) {
if (isInlineBlockChild && !forcePlacement && !isInlineBlockAndFirstOnRootArea) {
split[1].addChildRenderer(childRenderer);
} else if (isInlineBlockChild
&& childResult.getOverflowRenderer().getChildRenderers().isEmpty()
&& childResult.getStatus() == LayoutResult.PARTIAL) {
if (logger.isWarnEnabled()) {
logger.warn(IoLogMessageConstant.INLINE_BLOCK_ELEMENT_WILL_BE_CLIPPED);
}
} else {
split[1].addChildRenderer(childResult.getOverflowRenderer());
}
}
}
split[1].addAllChildRenderers(getChildRenderers().subList(childPos + 1, getChildRenderers().size()));
replaceSplitRendererKidFloats(floatsToNextPageSplitRenderers, split[0]);
split[0].removeAllChildRenderers(floatsOverflowedToNextLine);
split[1].addAllChildRenderers(0, floatsOverflowedToNextLine);
// no sense to process empty renderer
if (split[1].getChildRenderers().isEmpty() && floatsToNextPageOverflowRenderers.isEmpty()) {
split[1] = null;
}
final IRenderer causeOfNothing = childResult.getStatus() == LayoutResult.NOTHING
? childResult.getCauseOfNothing() : getChildRenderers().get(childPos);
if (split[1] == null) {
result = new LineLayoutResult(LayoutResult.FULL, occupiedArea, split[0], split[1], causeOfNothing);
} else if (anythingPlaced || floatsPlacedInLine) {
result = new LineLayoutResult(LayoutResult.PARTIAL, occupiedArea, split[0],
split[1], causeOfNothing);
} else {
result = new LineLayoutResult(LayoutResult.NOTHING, null, null, split[1], null);
}
result.setFloatsOverflowedToNextPage(floatsToNextPageOverflowRenderers);
if (newLineOccurred) {
result.setSplitForcedByNewline(true);
}
break;
} else {
if (childPos == firstChildToRelayout) {
firstChildToRelayout = -1;
} else {
anythingPlaced = true;
childPos++;
}
}
}
TextSequenceWordWrapping.resetTextSequenceIfItEnded(specialScriptLayoutResults, true, null, childPos,
minMaxWidthOfTextRendererSequenceHelper, noSoftWrap, widthHandler);
TextSequenceWordWrapping.resetTextSequenceIfItEnded(textRendererLayoutResults, false, null, childPos,
minMaxWidthOfTextRendererSequenceHelper, noSoftWrap, widthHandler);
if (result == null) {
boolean noOverflowedFloats =
floatsOverflowedToNextLine.isEmpty() && floatsToNextPageOverflowRenderers.isEmpty();
if (((anythingPlaced || floatsPlacedInLine) && noOverflowedFloats) || getChildRenderers().isEmpty()) {
result = new LineLayoutResult(LayoutResult.FULL, occupiedArea, null, null);
} else {
if (noOverflowedFloats) {
// all kids were some non-image and non-text kids (tab-stops?),
// but in this case, it should be okay to return FULL, as there is nothing to be placed
result = new LineLayoutResult(LayoutResult.FULL, occupiedArea, null, null);
} else if (anythingPlaced || floatsPlacedInLine) {
LineRenderer[] split = split();
split[0].addAllChildRenderers(getChildRenderers().subList(0, childPos));
replaceSplitRendererKidFloats(floatsToNextPageSplitRenderers, split[0]);
split[0].removeAllChildRenderers(floatsOverflowedToNextLine);
// If `result` variable is null up until now but not everything was placed - there is no
// content overflow, only floats are overflowing.
// The floatsOverflowedToNextLine might be empty, while the only overflowing floats are
// in floatsToNextPageOverflowRenderers. This situation is handled in ParagraphRenderer separately.
split[1].addAllChildRenderers(floatsOverflowedToNextLine);
result = new LineLayoutResult(LayoutResult.PARTIAL, occupiedArea, split[0], split[1], null);
result.setFloatsOverflowedToNextPage(floatsToNextPageOverflowRenderers);
} else {
IRenderer causeOfNothing =
floatsOverflowedToNextLine.isEmpty() ? floatsToNextPageOverflowRenderers.get(0)
: floatsOverflowedToNextLine.get(0);
result = new LineLayoutResult(LayoutResult.NOTHING, null, null, this, causeOfNothing);
}
}
}
LineRenderer toProcess = (LineRenderer) result.getSplitRenderer();
if (toProcess == null && result.getStatus() == LayoutResult.FULL) {
toProcess = this;
}
if (baseDirection != null && baseDirection != BaseDirection.NO_BIDI && toProcess != null) {
final LineSplitIntoGlyphsData splitIntoGlyphsData = splitLineIntoGlyphs(toProcess);
final byte[] lineLevels = new byte[splitIntoGlyphsData.getLineGlyphs().size()];
if (levels != null) {
System.arraycopy(levels, 0, lineLevels, 0, splitIntoGlyphsData.getLineGlyphs().size());
}
final int[] newOrder = TypographyUtils.reorderLine(splitIntoGlyphsData.getLineGlyphs(), lineLevels, levels);
if (newOrder != null) {
reorder(toProcess, splitIntoGlyphsData, newOrder);
adjustChildPositionsAfterReordering(toProcess.getChildRenderers(), occupiedArea.getBBox().getLeft());
}
if (result.getStatus() == LayoutResult.PARTIAL && levels != null) {
LineRenderer overflow = (LineRenderer) result.getOverflowRenderer();
overflow.levels = new byte[levels.length - lineLevels.length];
System.arraycopy(levels, lineLevels.length, overflow.levels, 0, overflow.levels.length);
if (overflow.levels.length == 0) {
overflow.levels = null;
}
}
}
if (anythingPlaced || floatsPlacedInLine) {
toProcess.adjustChildrenYLine().trimLast();
result.setMinMaxWidth(minMaxWidth);
}
if (wasXOverflowChanged) {
setProperty(Property.OVERFLOW_X, oldXOverflow);
if (null != result.getSplitRenderer()) {
result.getSplitRenderer().setProperty(Property.OVERFLOW_X, oldXOverflow);
}
if (null != result.getOverflowRenderer()) {
result.getOverflowRenderer().setProperty(Property.OVERFLOW_X, oldXOverflow);
}
}
return result;
}
public float getMaxAscent() {
return maxAscent;
}
public float getMaxDescent() {
return maxDescent;
}
public float getYLine() {
return occupiedArea.getBBox().getY() - maxDescent;
}
public float getLeadingValue(Leading leading) {
switch (leading.getType()) {
case Leading.FIXED:
return Math.max(leading.getValue(), maxBlockAscent - maxBlockDescent);
case Leading.MULTIPLIED:
return getTopLeadingIndent(leading) + getBottomLeadingIndent(leading);
default:
throw new IllegalStateException();
}
}
@Override
public IRenderer getNextRenderer() {
return new LineRenderer();
}
@Override
protected Float getFirstYLineRecursively() {
return getYLine();
}
@Override
protected Float getLastYLineRecursively() {
return getYLine();
}
public void justify(float width) {
float ratio = (float) this.getPropertyAsFloat(Property.SPACING_RATIO);
IRenderer lastChildRenderer = getLastNonFloatChildRenderer();
if (lastChildRenderer == null) {
return;
}
float freeWidth = occupiedArea.getBBox().getX() + width - lastChildRenderer.getOccupiedArea().getBBox().getX() -
lastChildRenderer.getOccupiedArea().getBBox().getWidth();
int numberOfSpaces = getNumberOfSpaces();
int baseCharsCount = baseCharactersCount();
float baseFactor = freeWidth / (ratio * numberOfSpaces + (1 - ratio) * (baseCharsCount - 1));
//Prevent a NaN when trying to justify a single word with spacing_ratio == 1.0
if (Float.isInfinite(baseFactor)) {
baseFactor = 0;
}
float wordSpacing = ratio * baseFactor;
float characterSpacing = (1 - ratio) * baseFactor;
float lastRightPos = occupiedArea.getBBox().getX();
for (final IRenderer child : getChildRenderers()) {
if (FloatingHelper.isRendererFloating(child)) {
continue;
}
float childX = child.getOccupiedArea().getBBox().getX();
child.move(lastRightPos - childX, 0);
childX = lastRightPos;
if (child instanceof TextRenderer) {
float childHSCale = (float) ((TextRenderer) child).getPropertyAsFloat(Property.HORIZONTAL_SCALING, 1f);
Float oldCharacterSpacing = ((TextRenderer) child).getPropertyAsFloat(Property.CHARACTER_SPACING);
Float oldWordSpacing = ((TextRenderer) child).getPropertyAsFloat(Property.WORD_SPACING);
child.setProperty(Property.CHARACTER_SPACING,
(null == oldCharacterSpacing ? 0 : (float) oldCharacterSpacing)
+ characterSpacing / childHSCale);
child.setProperty(Property.WORD_SPACING,
(null == oldWordSpacing ? 0 : (float) oldWordSpacing) + wordSpacing / childHSCale);
boolean isLastTextRenderer = child == lastChildRenderer;
float widthAddition = (isLastTextRenderer ? (((TextRenderer) child).lineLength() - 1)
: ((TextRenderer) child).lineLength()) * characterSpacing +
wordSpacing * ((TextRenderer) child).getNumberOfSpaces();
child.getOccupiedArea().getBBox()
.setWidth(child.getOccupiedArea().getBBox().getWidth() + widthAddition);
}
lastRightPos = childX + child.getOccupiedArea().getBBox().getWidth();
}
getOccupiedArea().getBBox().setWidth(width);
}
protected int getNumberOfSpaces() {
int spaces = 0;
for (final IRenderer child : getChildRenderers()) {
if (child instanceof TextRenderer && !FloatingHelper.isRendererFloating(child)) {
spaces += ((TextRenderer) child).getNumberOfSpaces();
}
}
return spaces;
}
/**
* Gets the total lengths of characters in this line. Other elements (images, tables) are not taken
* into account.
*
* @return the total lengths of characters in this line.
*/
protected int length() {
int length = 0;
for (final IRenderer child : getChildRenderers()) {
if (child instanceof TextRenderer && !FloatingHelper.isRendererFloating(child)) {
length += ((TextRenderer) child).lineLength();
}
}
return length;
}
/**
* Returns the number of base characters, i.e. non-mark characters
*
* @return the number of base non-mark characters
*/
protected int baseCharactersCount() {
int count = 0;
for (final IRenderer child : getChildRenderers()) {
if (child instanceof TextRenderer && !FloatingHelper.isRendererFloating(child)) {
count += ((TextRenderer) child).baseCharactersCount();
}
}
return count;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (final IRenderer renderer : getChildRenderers()) {
sb.append(renderer.toString());
}
return sb.toString();
}
protected LineRenderer createSplitRenderer() {
return (LineRenderer) getNextRenderer();
}
protected LineRenderer createOverflowRenderer() {
return (LineRenderer) getNextRenderer();
}
protected LineRenderer[] split() {
LineRenderer splitRenderer = createSplitRenderer();
splitRenderer.occupiedArea = occupiedArea.clone();
splitRenderer.parent = parent;
splitRenderer.maxAscent = maxAscent;
splitRenderer.maxDescent = maxDescent;
splitRenderer.maxTextAscent = maxTextAscent;
splitRenderer.maxTextDescent = maxTextDescent;
splitRenderer.maxBlockAscent = maxBlockAscent;
splitRenderer.maxBlockDescent = maxBlockDescent;
splitRenderer.levels = levels;
splitRenderer.addAllProperties(getOwnProperties());
LineRenderer overflowRenderer = createOverflowRenderer();
overflowRenderer.parent = parent;
overflowRenderer.addAllProperties(getOwnProperties());
return new LineRenderer[] {splitRenderer, overflowRenderer};
}
protected LineRenderer adjustChildrenYLine() {
if (RenderingMode.HTML_MODE == this.getProperty(Property.RENDERING_MODE) &&
hasInlineBlocksWithVerticalAlignment()) {
InlineVerticalAlignmentHelper.adjustChildrenYLineHtmlMode(this);
} else {
adjustChildrenYLineDefaultMode();
}
return this;
}
protected void applyLeading(float deltaY) {
occupiedArea.getBBox().moveUp(deltaY);
occupiedArea.getBBox().decreaseHeight(deltaY);
for (final IRenderer child : getChildRenderers()) {
if (!FloatingHelper.isRendererFloating(child)) {
child.move(0, deltaY);
}
}
}
protected LineRenderer trimLast() {
int lastIndex = getChildRenderers().size();
IRenderer lastRenderer = null;
while (--lastIndex >= 0) {
lastRenderer = getChildRenderers().get(lastIndex);
if (!FloatingHelper.isRendererFloating(lastRenderer)) {
break;
}
}
if (lastRenderer instanceof TextRenderer && lastIndex >= 0) {
float trimmedSpace = ((TextRenderer) lastRenderer).trimLast();
occupiedArea.getBBox().setWidth(occupiedArea.getBBox().getWidth() - trimmedSpace);
}
return this;
}
public boolean containsImage() {
for (final IRenderer renderer : getChildRenderers()) {
if (renderer instanceof ImageRenderer) {
return true;
}
}
return false;
}
@Override
public MinMaxWidth getMinMaxWidth() {
LineLayoutResult result = (LineLayoutResult) layout(new LayoutContext(
new LayoutArea(1, new Rectangle(MinMaxWidthUtils.getInfWidth(), AbstractRenderer.INF))));
return result.getMinMaxWidth();
}
boolean hasChildRendererInHtmlMode() {
for (final IRenderer childRenderer : getChildRenderers()) {
if (RenderingMode.HTML_MODE.equals(childRenderer.getProperty(Property.RENDERING_MODE))) {
return true;
}
}
return false;
}
float getTopLeadingIndent(Leading leading) {
switch (leading.getType()) {
case Leading.FIXED:
return (Math.max(leading.getValue(), maxBlockAscent - maxBlockDescent) -
occupiedArea.getBBox().getHeight()) / 2;
case Leading.MULTIPLIED:
UnitValue fontSize = this.getProperty(Property.FONT_SIZE, UnitValue.createPointValue(0f));
if (!fontSize.isPointValue()) {
logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
Property.FONT_SIZE));
}
// In HTML, depending on whether is present or not, and if present then depending
// on the version, the behavior is different. In one case, bottom leading indent is added for images,
// in the other it is not added.
// This is why !containsImage() is present below. Depending on the presence of
// this !containsImage() condition, the behavior changes between the two possible scenarios in HTML.
float textAscent = maxTextAscent == 0 && maxTextDescent == 0 && Math.abs(maxAscent)
+ Math.abs(maxDescent) != 0 && !containsImage() ? fontSize.getValue() * 0.8f : maxTextAscent;
float textDescent = maxTextAscent == 0 && maxTextDescent == 0 && Math.abs(maxAscent)
+ Math.abs(maxDescent) != 0 && !containsImage() ? -fontSize.getValue() * 0.2f : maxTextDescent;
return Math.max(textAscent + ((textAscent - textDescent) * (leading.getValue() - 1)) / 2,
maxBlockAscent) - maxAscent;
default:
throw new IllegalStateException();
}
}
float getBottomLeadingIndent(Leading leading) {
switch (leading.getType()) {
case Leading.FIXED:
return (Math.max(leading.getValue(), maxBlockAscent - maxBlockDescent) -
occupiedArea.getBBox().getHeight()) / 2;
case Leading.MULTIPLIED:
UnitValue fontSize = this.getProperty(Property.FONT_SIZE, UnitValue.createPointValue(0f));
if (!fontSize.isPointValue()) {
logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
Property.FONT_SIZE));
}
// In HTML, depending on whether is present or not, and if present then depending
// on the version, the behavior is different. In one case, bottom leading indent is added for images,
// in the other it is not added.
// This is why !containsImage() is present below. Depending on the presence of
// this !containsImage() condition, the behavior changes between the two possible scenarios in HTML.
float textAscent = maxTextAscent == 0 && maxTextDescent == 0 && !containsImage() ?
fontSize.getValue() * 0.8f : maxTextAscent;
float textDescent = maxTextAscent == 0 && maxTextDescent == 0 && !containsImage() ?
-fontSize.getValue() * 0.2f : maxTextDescent;
return Math.max(-textDescent + ((textAscent - textDescent) * (leading.getValue() - 1)) / 2,
-maxBlockDescent) + maxDescent;
default:
throw new IllegalStateException();
}
}
static LineSplitIntoGlyphsData splitLineIntoGlyphs(LineRenderer toSplit) {
final LineSplitIntoGlyphsData result = new LineSplitIntoGlyphsData();
boolean newLineFound = false;
TextRenderer lastTextRenderer = null;
for (final IRenderer child : toSplit.getChildRenderers()) {
if (newLineFound) {
break;
}
if (child instanceof TextRenderer) {
GlyphLine childLine = ((TextRenderer) child).line;
for (int i = childLine.start; i < childLine.end; i++) {
if (TextUtil.isNewLine(childLine.get(i))) {
newLineFound = true;
break;
}
result.addLineGlyph(new RendererGlyph(childLine.get(i), (TextRenderer) child));
}
lastTextRenderer = (TextRenderer) child;
} else {
result.addInsertAfter(lastTextRenderer, child);
}
}
return result;
}
static void reorder(LineRenderer toProcess, LineSplitIntoGlyphsData splitLineIntoGlyphsResult, int[] newOrder) {
// Insert non-text renderers
toProcess.setChildRenderers(splitLineIntoGlyphsResult.getStarterNonTextRenderers());
final List lineGlyphs = splitLineIntoGlyphsResult.getLineGlyphs();
int initialPos = 0;
for (int offset = initialPos; offset < lineGlyphs.size(); offset = initialPos) {
final TextRenderer renderer = lineGlyphs.get(offset).renderer;
final TextRenderer newRenderer = new TextRenderer(renderer).removeReversedRanges();
toProcess.addChildRenderer(newRenderer);
// Insert non-text renderers
toProcess.addAllChildRenderers(splitLineIntoGlyphsResult.getInsertAfterAndRemove(renderer));
newRenderer.line = new GlyphLine(newRenderer.line);
List replacementGlyphs = new ArrayList<>();
boolean reversed = false;
for (int pos = offset; pos < lineGlyphs.size() && lineGlyphs.get(pos).renderer == renderer; ++pos) {
replacementGlyphs.add(lineGlyphs.get(pos).glyph);
if (pos + 1 < lineGlyphs.size()
&& lineGlyphs.get(pos + 1).renderer == renderer
&& newOrder[pos] == newOrder[pos + 1] + 1
&& !TextUtil.isSpaceOrWhitespace(lineGlyphs.get(pos + 1).glyph)
&& !TextUtil.isSpaceOrWhitespace(lineGlyphs.get(pos).glyph)) {
reversed = true;
continue;
}
if (reversed) {
newRenderer.initReversedRanges().add(new int[] {initialPos - offset, pos - offset});
reversed = false;
}
initialPos = pos + 1;
}
newRenderer.line.setGlyphs(replacementGlyphs);
}
}
static void adjustChildPositionsAfterReordering(List children, float initialXPos) {
float currentXPos = initialXPos;
for (IRenderer child : children) {
if (!FloatingHelper.isRendererFloating(child)) {
float currentWidth;
if (child instanceof TextRenderer) {
currentWidth = ((TextRenderer) child).calculateLineWidth();
UnitValue[] margins = ((TextRenderer) child).getMargins();
if (!margins[1].isPointValue() && logger.isErrorEnabled()) {
logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
"right margin"));
}
if (!margins[3].isPointValue() && logger.isErrorEnabled()) {
logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
"left margin"));
}
UnitValue[] paddings = ((TextRenderer) child).getPaddings();
if (!paddings[1].isPointValue() && logger.isErrorEnabled()) {
logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
"right padding"));
}
if (!paddings[3].isPointValue() && logger.isErrorEnabled()) {
logger.error(MessageFormatUtil.format(IoLogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED,
"left padding"));
}
currentWidth += margins[1].getValue() + margins[3].getValue() +
paddings[1].getValue() + paddings[3].getValue();
((TextRenderer) child).occupiedArea.getBBox().setX(currentXPos).setWidth(currentWidth);
} else {
currentWidth = child.getOccupiedArea().getBBox().getWidth();
child.move(currentXPos - child.getOccupiedArea().getBBox().getX(), 0);
}
currentXPos += currentWidth;
}
}
}
private LineRenderer[] splitNotFittingFloat(int childPos, LayoutResult childResult) {
LineRenderer[] split = split();
split[0].addAllChildRenderers(getChildRenderers().subList(0, childPos));
split[0].addChildRenderer(childResult.getSplitRenderer());
split[1].addChildRenderer(childResult.getOverflowRenderer());
split[1].addAllChildRenderers(getChildRenderers().subList(childPos + 1, getChildRenderers().size()));
return split;
}
private void adjustLineOnFloatPlaced(Rectangle layoutBox, int childPos, FloatPropertyValue kidFloatPropertyVal,
Rectangle justPlacedFloatBox) {
if (justPlacedFloatBox.getBottom() >= layoutBox.getTop() || justPlacedFloatBox.getTop() < layoutBox.getTop()) {
return;
}
float floatWidth = justPlacedFloatBox.getWidth();
if (kidFloatPropertyVal.equals(FloatPropertyValue.LEFT)) {
layoutBox.setWidth(layoutBox.getWidth() - floatWidth).moveRight(floatWidth);
occupiedArea.getBBox().moveRight(floatWidth);
for (int i = 0; i < childPos; ++i) {
final IRenderer prevChild = getChildRenderers().get(i);
if (!FloatingHelper.isRendererFloating(prevChild)) {
prevChild.move(floatWidth, 0);
}
}
} else {
layoutBox.setWidth(layoutBox.getWidth() - floatWidth);
}
}
private void replaceSplitRendererKidFloats(Map floatsToNextPageSplitRenderers,
LineRenderer splitRenderer) {
for (Map.Entry splitFloat : floatsToNextPageSplitRenderers.entrySet()) {
if (splitFloat.getValue() != null) {
splitRenderer.setChildRenderer(splitFloat.getKey(), splitFloat.getValue());
} else {
splitRenderer.setChildRenderer(splitFloat.getKey(), null);
}
}
for (int i = splitRenderer.getChildRenderers().size() - 1; i >= 0; --i) {
if (splitRenderer.getChildRenderers().get(i) == null) {
splitRenderer.removeChildRenderer(i);
}
}
}
private IRenderer getLastNonFloatChildRenderer() {
IRenderer result = null;
for (int i = getChildRenderers().size() - 1; i >= 0; --i) {
IRenderer current = getChildRenderers().get(i);
if (!FloatingHelper.isRendererFloating(current)) {
result = current;
break;
}
}
return result;
}
private TabStop getNextTabStop(float curWidth) {
NavigableMap tabStops = this.>getProperty(Property.TAB_STOPS);
Map.Entry nextTabStopEntry = null;
TabStop nextTabStop = null;
if (tabStops != null) {
nextTabStopEntry = tabStops.higherEntry(curWidth);
}
if (nextTabStopEntry != null) {
nextTabStop = ((Map.Entry) nextTabStopEntry).getValue();
}
return nextTabStop;
}
/**
* Calculates and sets encountered tab size.
* Returns null, if processing is finished and layout can be performed for the tab renderer;
* otherwise, in case when the tab should be processed after the next element in the line,
* this method returns corresponding tab stop.
*/
private TabStop calculateTab(IRenderer childRenderer, float curWidth, float lineWidth) {
TabStop nextTabStop = getNextTabStop(curWidth);
if (nextTabStop == null) {
processDefaultTab(childRenderer, curWidth, lineWidth);
return null;
}
childRenderer.setProperty(Property.TAB_LEADER, nextTabStop.getTabLeader());
childRenderer.setProperty(Property.WIDTH, UnitValue.createPointValue(nextTabStop.getTabPosition() - curWidth));
childRenderer.setProperty(Property.MIN_HEIGHT, UnitValue.createPointValue(maxAscent - maxDescent));
if (nextTabStop.getTabAlignment() == TabAlignment.LEFT) {
return null;
}
return nextTabStop;
}
/**
* Calculates and sets tab size with the account of the element that is next in the line after the tab.
* Returns resulting width of the tab.
*/
private float calculateTab(Rectangle layoutBox, float curWidth, TabStop tabStop, List affectedRenderers,
IRenderer tabRenderer) {
float sumOfAffectedRendererWidths = 0;
for (IRenderer renderer : affectedRenderers) {
sumOfAffectedRendererWidths += renderer.getOccupiedArea().getBBox().getWidth();
}
float tabWidth = 0;
switch (tabStop.getTabAlignment()) {
case RIGHT:
tabWidth = tabStop.getTabPosition() - curWidth - sumOfAffectedRendererWidths;
break;
case CENTER:
tabWidth = tabStop.getTabPosition() - curWidth - sumOfAffectedRendererWidths / 2;
break;
case ANCHOR:
float anchorPosition = -1;
float processedRenderersWidth = 0;
for (IRenderer renderer : affectedRenderers) {
anchorPosition = ((TextRenderer) renderer).getTabAnchorCharacterPosition();
if (-1 != anchorPosition) {
break;
} else {
processedRenderersWidth += renderer.getOccupiedArea().getBBox().getWidth();
}
}
if (anchorPosition == -1) {
anchorPosition = 0;
}
tabWidth = tabStop.getTabPosition() - curWidth - anchorPosition - processedRenderersWidth;
break;
}
if (tabWidth < 0) {
tabWidth = 0;
}
if (curWidth + tabWidth + sumOfAffectedRendererWidths > layoutBox.getWidth()) {
tabWidth -= (curWidth + sumOfAffectedRendererWidths + tabWidth) - layoutBox.getWidth();
}
tabRenderer.setProperty(Property.WIDTH, UnitValue.createPointValue(tabWidth));
tabRenderer.setProperty(Property.MIN_HEIGHT, UnitValue.createPointValue(maxAscent - maxDescent));
return tabWidth;
}
private void processDefaultTab(IRenderer tabRenderer, float curWidth, float lineWidth) {
Float tabDefault = this.getPropertyAsFloat(Property.TAB_DEFAULT);
Float tabWidth = tabDefault - curWidth % tabDefault;
if (curWidth + tabWidth > lineWidth) {
tabWidth = lineWidth - curWidth;
}
tabRenderer.setProperty(Property.WIDTH, UnitValue.createPointValue((float) tabWidth));
tabRenderer.setProperty(Property.MIN_HEIGHT, UnitValue.createPointValue(maxAscent - maxDescent));
}
private void updateChildrenParent() {
for (final IRenderer renderer : getChildRenderers()) {
renderer.setParent(this);
}
}
/**
* Trim first child text renderers.
*
* @return total number of trimmed glyphs.
*/
int trimFirst() {
int totalNumberOfTrimmedGlyphs = 0;
for (final IRenderer renderer : getChildRenderers()) {
if (FloatingHelper.isRendererFloating(renderer)) {
continue;
}
boolean trimFinished;
if (renderer instanceof TextRenderer) {
TextRenderer textRenderer = (TextRenderer) renderer;
GlyphLine currentText = textRenderer.getText();
if (currentText != null) {
int prevTextStart = currentText.start;
textRenderer.trimFirst();
int numOfTrimmedGlyphs = textRenderer.getText().start - prevTextStart;
totalNumberOfTrimmedGlyphs += numOfTrimmedGlyphs;
}
trimFinished = textRenderer.length() > 0;
} else {
trimFinished = true;
}
if (trimFinished) {
break;
}
}
return totalNumberOfTrimmedGlyphs;
}
/**
* Apply OTF features and return the last(!) base direction of child renderer
*
* @return the last(!) base direction of child renderer.
*/
private BaseDirection applyOtf() {
BaseDirection baseDirection = this.getProperty(Property.BASE_DIRECTION);
for (final IRenderer renderer : getChildRenderers()) {
if (renderer instanceof TextRenderer) {
((TextRenderer) renderer).applyOtf();
if (baseDirection == null || baseDirection == BaseDirection.NO_BIDI) {
baseDirection = renderer.getOwnProperty(Property.BASE_DIRECTION);
}
}
}
return baseDirection;
}
static boolean isChildFloating(IRenderer childRenderer) {
FloatPropertyValue kidFloatPropertyVal = childRenderer.getProperty(Property.FLOAT);
return childRenderer instanceof AbstractRenderer
&& FloatingHelper.isRendererFloating(childRenderer, kidFloatPropertyVal);
}
static boolean isInlineBlockChild(IRenderer child) {
return child instanceof BlockRenderer || child instanceof TableRenderer;
}
/**
* Checks if the word that's been split when has been layouted on this line can fit the next line without splitting.
*
* @param childRenderer the childRenderer containing the split word
* @param wasXOverflowChanged true if {@link Property#OVERFLOW_X} has been changed
* during layouting of {@link LineRenderer}
* @param oldXOverflow the value of {@link Property#OVERFLOW_X} before it's been changed
* during layouting of {@link LineRenderer}
* or null if {@link Property#OVERFLOW_X} hasn't been changed
* @param layoutContext {@link LayoutContext}
* @param layoutBox current layoutBox
* @param wasParentsHeightClipped true if layoutBox's height has been clipped
* @return true if the split word can fit the next line without splitting
*/
boolean isForceOverflowForTextRendererPartialResult(IRenderer childRenderer, boolean wasXOverflowChanged,
OverflowPropertyValue oldXOverflow, LayoutContext layoutContext,
Rectangle layoutBox, boolean wasParentsHeightClipped) {
if (wasXOverflowChanged) {
setProperty(Property.OVERFLOW_X, oldXOverflow);
}
LayoutResult newLayoutResult = childRenderer.layout(
new LayoutContext(new LayoutArea(layoutContext.getArea().getPageNumber(), layoutBox),
wasParentsHeightClipped));
if (wasXOverflowChanged) {
setProperty(Property.OVERFLOW_X, OverflowPropertyValue.FIT);
}
return newLayoutResult instanceof TextLayoutResult
&& !((TextLayoutResult) newLayoutResult).isWordHasBeenSplit();
}
/**
* Extracts ascender and descender of an already layouted {@link IRenderer childRenderer}.
*
* @param childRenderer an already layouted child who's ascender and descender are to be extracted
* @param childResult {@link LayoutResult} of the childRenderer based on which ascender and descender are defined
* @param childRenderingMode {@link RenderingMode rendering mode}
* @param isInlineBlockChild true if childRenderer {@link #isInlineBlockChild(IRenderer)}
* @return a two-element float array where first element is ascender value and second element is descender value
*/
float[] getAscentDescentOfLayoutedChildRenderer(IRenderer childRenderer, LayoutResult childResult,
RenderingMode childRenderingMode, boolean isInlineBlockChild) {
float childAscent = 0;
float childDescent = 0;
if (childRenderer instanceof ILeafElementRenderer
&& childResult.getStatus() != LayoutResult.NOTHING) {
if (RenderingMode.HTML_MODE == childRenderingMode && childRenderer instanceof TextRenderer) {
return LineHeightHelper.getActualAscenderDescender((TextRenderer) childRenderer);
} else {
childAscent = ((ILeafElementRenderer) childRenderer).getAscent();
childDescent = ((ILeafElementRenderer) childRenderer).getDescent();
}
} else if (isInlineBlockChild && childResult.getStatus() != LayoutResult.NOTHING) {
if (childRenderer instanceof AbstractRenderer) {
Float yLine = ((AbstractRenderer) childRenderer).getLastYLineRecursively();
if (yLine == null) {
childAscent = childRenderer.getOccupiedArea().getBBox().getHeight();
} else {
childAscent = childRenderer.getOccupiedArea().getBBox().getTop() - (float) yLine;
childDescent = -((float) yLine - childRenderer.getOccupiedArea().getBBox().getBottom());
}
} else {
childAscent = childRenderer.getOccupiedArea().getBBox().getHeight();
}
}
return new float[] {childAscent, childDescent};
}
/**
* Updates {@link LineRenderer#maxAscent}, {@link LineRenderer#maxDescent}, {@link LineRenderer#maxTextAscent} and
* {@link LineRenderer#maxTextDescent} after a {@link TextRenderer} sequence has been fully processed.
*
* @param newChildPos position of the last {@link TextRenderer} child of the
* sequence to remain on the line
* @param lineAscentDescentStateBeforeTextRendererSequence a {@link LineAscentDescentState} containing
* {@link LineRenderer}'s maxAscent, maxDescent,
* maxTextAscent, maxTextDescent before
* {@link TextRenderer} sequence start
* @param textRendererSequenceAscentDescent a {@link Map} with {@link TextRenderer} children's
* positions as keys
* and float arrays consisting of maxAscent, maxDescent,
* maxTextAscent,
* maxTextDescent of the corresponding {@link TextRenderer}
* children.
* @return a two-element float array where first element is a new {@link LineRenderer}'s ascender
* and second element is a new {@link LineRenderer}'s descender
*/
float[] updateAscentDescentAfterTextRendererSequenceProcessing(
int newChildPos, LineAscentDescentState lineAscentDescentStateBeforeTextRendererSequence,
Map textRendererSequenceAscentDescent) {
float maxAscentUpdated = lineAscentDescentStateBeforeTextRendererSequence.maxAscent;
float maxDescentUpdated = lineAscentDescentStateBeforeTextRendererSequence.maxDescent;
float maxTextAscentUpdated = lineAscentDescentStateBeforeTextRendererSequence.maxTextAscent;
float maxTextDescentUpdated = lineAscentDescentStateBeforeTextRendererSequence.maxTextDescent;
for (Map.Entry childAscentDescent : textRendererSequenceAscentDescent.entrySet()) {
if (childAscentDescent.getKey() <= newChildPos) {
maxAscentUpdated = Math.max(maxAscentUpdated, childAscentDescent.getValue()[0]);
maxDescentUpdated = Math.min(maxDescentUpdated, childAscentDescent.getValue()[1]);
maxTextAscentUpdated = Math.max(maxTextAscentUpdated, childAscentDescent.getValue()[0]);
maxTextDescentUpdated = Math.min(maxTextDescentUpdated, childAscentDescent.getValue()[1]);
}
}
this.maxAscent = maxAscentUpdated;
this.maxDescent = maxDescentUpdated;
this.maxTextAscent = maxTextAscentUpdated;
this.maxTextDescent = maxTextDescentUpdated;
return new float[] {this.maxAscent, this.maxDescent};
}
/**
* Update {@link LineRenderer#maxAscent}, {@link LineRenderer#maxDescent}, {@link LineRenderer#maxTextAscent},
* {@link LineRenderer#maxTextDescent}, {@link LineRenderer#maxBlockAscent} and {@link LineRenderer#maxBlockDescent}
* after child's layout.
*
* @param childAscentDescent a two-element float array where first element is ascender of a layouted child
* and second element is descender of a layouted child
* @param childRenderer the layouted {@link IRenderer childRenderer} of current {@link LineRenderer}
* @param isChildFloating true if {@link #isChildFloating(IRenderer)}
*/
void updateAscentDescentAfterChildLayout(float[] childAscentDescent, IRenderer childRenderer,
boolean isChildFloating) {
float childAscent = childAscentDescent[0];
float childDescent = childAscentDescent[1];
this.maxAscent = Math.max(this.maxAscent, childAscent);
if (childRenderer instanceof TextRenderer) {
this.maxTextAscent = Math.max(this.maxTextAscent, childAscent);
} else if (!isChildFloating) {
this.maxBlockAscent = Math.max(this.maxBlockAscent, childAscent);
}
this.maxDescent = Math.min(this.maxDescent, childDescent);
if (childRenderer instanceof TextRenderer) {
this.maxTextDescent = Math.min(this.maxTextDescent, childDescent);
} else if (!isChildFloating) {
this.maxBlockDescent = Math.min(this.maxBlockDescent, childDescent);
}
}
private void updateBidiLevels(int totalNumberOfTrimmedGlyphs, BaseDirection baseDirection) {
if (totalNumberOfTrimmedGlyphs != 0 && levels != null) {
levels = Arrays.copyOfRange(levels, totalNumberOfTrimmedGlyphs, levels.length);
}
List unicodeIdsReorderingList = null;
if (levels == null && baseDirection != null && baseDirection != BaseDirection.NO_BIDI) {
unicodeIdsReorderingList = new ArrayList<>();
boolean newLineFound = false;
for (final IRenderer child : getChildRenderers()) {
if (newLineFound) {
break;
}
if (child instanceof TextRenderer) {
GlyphLine text = ((TextRenderer) child).getText();
for (int i = text.start; i < text.end; i++) {
Glyph glyph = text.get(i);
if (TextUtil.isNewLine(glyph)) {
newLineFound = true;
break;
}
// we assume all the chars will have the same bidi group
// we also assume pairing symbols won't get merged with other ones
int unicode = glyph.hasValidUnicode() ? glyph.getUnicode() : glyph.getUnicodeChars()[0];
unicodeIdsReorderingList.add(unicode);
}
}
}
if (unicodeIdsReorderingList.size() > 0) {
final PdfDocument pdfDocument = getPdfDocument();
final SequenceId sequenceId = pdfDocument == null ? null : pdfDocument.getDocumentIdWrapper();
final MetaInfoContainer metaInfoContainer = this.getProperty(Property.META_INFO);
final IMetaInfo metaInfo = metaInfoContainer == null ? null : metaInfoContainer.getMetaInfo();
levels = TypographyUtils.getBidiLevels(baseDirection, ArrayUtil.toIntArray(unicodeIdsReorderingList),
sequenceId, metaInfo);
} else {
levels = null;
}
}
}
/**
* While resolving TextRenderer may split into several ones with different fonts.
*/
private void resolveChildrenFonts() {
final List newChildRenderers = new ArrayList<>(getChildRenderers().size());
boolean updateChildRenderers = false;
for (final IRenderer child : getChildRenderers()) {
if (child instanceof TextRenderer) {
if (((TextRenderer) child).resolveFonts(newChildRenderers)) {
updateChildRenderers = true;
}
} else {
newChildRenderers.add(child);
}
}
// this mean, that some TextRenderer has been replaced.
if (updateChildRenderers) {
setChildRenderers(newChildRenderers);
}
}
private float decreaseRelativeWidthByChildAdditionalWidth(IRenderer childRenderer, float normalizedChildWidth) {
// Decrease the calculated width by margins, paddings and borders so that
// even for 100% width the content definitely fits.
if (childRenderer instanceof AbstractRenderer) {
Rectangle dummyRect = new Rectangle(normalizedChildWidth, 0);
((AbstractRenderer) childRenderer).applyMargins(dummyRect, false);
if (!isBorderBoxSizing(childRenderer)) {
((AbstractRenderer) childRenderer).applyBorderBox(dummyRect, false);
((AbstractRenderer) childRenderer).applyPaddings(dummyRect, false);
}
normalizedChildWidth = dummyRect.getWidth();
}
return normalizedChildWidth;
}
private void adjustChildrenYLineDefaultMode() {
float actualYLine = occupiedArea.getBBox().getY() + occupiedArea.getBBox().getHeight() - maxAscent;
for (final IRenderer renderer : getChildRenderers()) {
if (FloatingHelper.isRendererFloating(renderer)) {
continue;
}
if (renderer instanceof ILeafElementRenderer) {
float descent = ((ILeafElementRenderer) renderer).getDescent();
renderer.move(0, actualYLine - renderer.getOccupiedArea().getBBox().getBottom() + descent);
} else {
Float yLine = isInlineBlockChild(renderer) && renderer instanceof AbstractRenderer ?
((AbstractRenderer) renderer).getLastYLineRecursively() : null;
renderer.move(0, actualYLine - (yLine == null ?
renderer.getOccupiedArea().getBBox().getBottom() : (float) yLine));
}
}
}
private boolean hasInlineBlocksWithVerticalAlignment() {
for (IRenderer child : getChildRenderers()) {
if (child.hasProperty(Property.INLINE_VERTICAL_ALIGNMENT) &&
InlineVerticalAlignmentType.BASELINE != ((InlineVerticalAlignment)child.
getProperty(Property.INLINE_VERTICAL_ALIGNMENT)).getType()) {
return true;
}
}
return false;
}
public static class RendererGlyph {
public Glyph glyph;
public TextRenderer renderer;
public RendererGlyph(Glyph glyph, TextRenderer textRenderer) {
this.glyph = glyph;
this.renderer = textRenderer;
}
}
static class LineAscentDescentState {
float maxAscent;
float maxDescent;
float maxTextAscent;
float maxTextDescent;
LineAscentDescentState(float maxAscent, float maxDescent, float maxTextAscent, float maxTextDescent) {
this.maxAscent = maxAscent;
this.maxDescent = maxDescent;
this.maxTextAscent = maxTextAscent;
this.maxTextDescent = maxTextDescent;
}
}
static class LineSplitIntoGlyphsData {
private final List lineGlyphs;
private final Map> insertAfter;
private final List starterNonTextRenderers;
public LineSplitIntoGlyphsData() {
lineGlyphs = new ArrayList<>();
insertAfter = new HashMap<>();
starterNonTextRenderers = new ArrayList<>();
}
public List getLineGlyphs() {
return lineGlyphs;
}
public List getInsertAfterAndRemove(TextRenderer afterRenderer) {
return insertAfter.remove(afterRenderer);
}
public List getStarterNonTextRenderers() {
return starterNonTextRenderers;
}
public void addLineGlyph(RendererGlyph glyph) {
lineGlyphs.add(glyph);
}
public void addInsertAfter(TextRenderer afterRenderer, IRenderer toInsert) {
if (afterRenderer == null) {
// null indicates that there were no previous renderers
starterNonTextRenderers.add(toInsert);
} else {
if (!insertAfter.containsKey(afterRenderer)) {
insertAfter.put(afterRenderer, new ArrayList());
}
insertAfter.get(afterRenderer).add(toInsert);
}
}
}
}