net.sf.jasperreports.engine.fill.TextMeasurer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jasperreports Show documentation
Show all versions of jasperreports Show documentation
Free Java Reporting Library
/*
* JasperReports - Free Java Reporting Library.
* Copyright (C) 2001 - 2022 TIBCO Software Inc. All rights reserved.
* http://www.jaspersoft.com
*
* Unless you have purchased a commercial license agreement from Jaspersoft,
* the following license terms apply:
*
* This program is part of JasperReports.
*
* JasperReports is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* JasperReports 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with JasperReports. If not, see .
*/
package net.sf.jasperreports.engine.fill;
import java.awt.font.FontRenderContext;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.sf.jasperreports.annotations.properties.Property;
import net.sf.jasperreports.annotations.properties.PropertyScope;
import net.sf.jasperreports.engine.JRCommonText;
import net.sf.jasperreports.engine.JRParagraph;
import net.sf.jasperreports.engine.JRPrintText;
import net.sf.jasperreports.engine.JRPropertiesHolder;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRTextElement;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.TabStop;
import net.sf.jasperreports.engine.export.AbstractTextRenderer;
import net.sf.jasperreports.engine.export.AwtTextRenderer;
import net.sf.jasperreports.engine.util.DelegatePropertiesHolder;
import net.sf.jasperreports.engine.util.JRStringUtil;
import net.sf.jasperreports.engine.util.JRStyledText;
import net.sf.jasperreports.engine.util.JRTextAttribute;
import net.sf.jasperreports.engine.util.ParagraphUtil;
import net.sf.jasperreports.engine.util.StyledTextWriteContext;
import net.sf.jasperreports.properties.PropertyConstants;
/**
* Default text measurer implementation.
* Text Measuring
* When a the contents of a text element do not fit
* into the area given by the element width and height, the engine will either truncate the
* text contents or, in the case of a text field that is allowed to stretch, increase the height of
* the element to accommodate the contents. To do so, the JasperReports engine needs to
* measure the text and calculate how much of it fits in the element area, or how much the
* element needs to stretch in order to fit the entire text.
*
* JasperReports does this, by default, by using standard Java AWT classes to layout and
* measure the text with its style information given by the text font and by other style
* attributes. This ensures that the result of the text layout calculation is exact according to
* the JasperReports principle of pixel perfectness.
*
* However, this comes at a price - the AWT text layout calls contribute to the overall
* report fill performance. For this reason and possibly others, it might be desired in some
* cases to implement a different text measuring mechanism. JasperReports allows users to
* employ custom text measurer implementations by setting a value for the
* {@link net.sf.jasperreports.engine.util.JRTextMeasurerUtil#PROPERTY_TEXT_MEASURER_FACTORY net.sf.jasperreports.text.measurer.factory} property.
* The property can be set globally (in jasperreports.properties
or via the
* {@link net.sf.jasperreports.engine.JRPropertiesUtil#setProperty(String, String)} method), at
* report level or at element level (as an element property). The property value should be
* either the name of a class that implements the
* {@link net.sf.jasperreports.engine.util.JRTextMeasurerFactory} interface, or an
* alias defined for such a text measurer factory class. To define an alias, one needs to
* define a property having
* net.sf.jasperreports.text.measurer.factory.<alias>
as key and the factory
* class name as value. Take the following examples of text measurer factory properties:
*
* - in jasperreports.properties set a custom default text measurer factory:
*
* net.sf.jasperreports.text.measurer.factory=com.jasperreports.MyTextMeasurerFactory
* - define an alias for a different text measurer factory:
*
* net.sf.jasperreports.text.measurer.factory.fast=com.jasperreports.MyFastTextMeasurerFactory
* - in a JRXML, use the fast text measurer for a static text:
*
*
* <staticText>
* <reportElement ...>
* <property name="net.sf.jasperreports.text.measurer.factory" value="fast"/>
* </reportElement>
* <text>...</text>
* </staticText>
*
* The default text measurer factory used by JasperReports is
* {@link net.sf.jasperreports.engine.fill.TextMeasurerFactory}; the factory is also
* registered under an alias named default
.
* Text Truncation
* The built-in text measurer supports a series of text truncation customizations. As a
* reminder, text truncation occurs when a the contents of a static text element or of a text
* field that is not set as stretchable do not fit the area reserved for the element in the report
* template. Note that text truncation only refers to the truncation of the last line of a text
* element, and not to the word wrapping of a text element that spans across multiple lines.
*
* The default behavior is to use the standard AWT line break logic (as returned by the
* java.text.BreakIterator.getLineInstance()
method) to determine where to
* truncate the text. This means that the last line of text will be truncated after the last word
* that fits on the line, or after the last character when the first word on the line does not
* entirely fit.
*
* This behavior can be changed by forcing the text to always get truncated at the last
* character that fits the element area, and by appending one or more characters to the
* truncated text to notify a report reader that the text has been truncated.
* To force the text to be wrapped at the last character, the
* {@link net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_AT_CHAR net.sf.jasperreports.text.truncate.at.char}
* property needs to be set to true
* globally, at report level or at text element level. The levels at which the property can be
* set are listed in a decreasing order of precedence, therefore an element level property
* overrides the report level property, which in its turn overrides the global property. The
* property can also be set to false at report or element level to override the true value of the
* property set at a higher level.
*
* To append a suffix to the truncated text, one needs to set the desired suffix as the value
* of the {@link net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_SUFFIX net.sf.jasperreports.text.truncate.suffix}
* property globally, at report level or at element level. For instance, to use a Unicode
* horizontal ellipsis character (code point U+2026) as text truncation suffix, one would set
* the property globally or at report level as following:
*
* - globally in
jasperreports.properties
:
*
* net.sf.jasperreports.text.truncate.suffix=\u2026
* - at report level:
*
*
* <jasperReport ...>
* <property name="net.sf.jasperreports.text.truncate.suffix" value="…"/>
* ...
* </jasperReport>
*
* Note that in the JRXML the ellipsis character was introduced via an XML numerical
* character entity. If the JRXML file uses a Unicode XML encoding, the Unicode
* character can also be directly written in the JRXML.
*
* When using a truncation suffix, the truncate at character property is taken into
* consideration in order to determine where to append the truncation suffix. If the
* truncation at character property is set to false, the suffix is appended after the last word
* that fits; if the property is set to true, the suffix is appended after the last text character
* that fits.
*
* When used for a text element that produces styled text, the truncation suffix is placed
* outside the styled text, that is, the truncation suffix will be displayed using the style
* defined at element level.
*
* Text truncation is desirable when producing reports for that are displayed on a screen or
* printed on paper - in such scenarios the layout of the report is important. On the other
* hand, some JasperReports exporters, such as the Excel or CSV ones, produce output
* which in many cases is intended as data-centric. In such cases, it could be useful not to
* truncate any text generated by the report, even if some texts would not fit when rendered
* on a layout-sensitive media.
*
* To inhibit the unconditional truncation of report texts, one would need to set the
* {@link net.sf.jasperreports.engine.JRTextElement#PROPERTY_PRINT_KEEP_FULL_TEXT net.sf.jasperreports.print.keep.full.text}
* property to true globally, at report level or at text element level. When the
* property is set to true, the text is not truncated at fill time and the generated
* report preserves the full text as produced by the text element.
*
* Visual report exporters (such as the exporters used for PDF, HTML, RTF, printing or the
* Java report viewer) would still truncate the rendered text, but the Excel and CSV data-centric
* exporters would use the full text. Note that preserving the full text does not affect
* the size of the text element, therefore the Excel exporter would display the full text
* inside a cell that has the size of the truncated text.
*
* @see net.sf.jasperreports.engine.JRTextElement#PROPERTY_PRINT_KEEP_FULL_TEXT
* @see net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_AT_CHAR
* @see net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_SUFFIX
* @see net.sf.jasperreports.engine.fill.TextMeasurerFactory
* @see net.sf.jasperreports.engine.util.JRTextMeasurerUtil#PROPERTY_TEXT_MEASURER_FACTORY
* @author Teodor Danciu ([email protected])
*/
public class TextMeasurer implements JRTextMeasurer
{
private static final Log log = LogFactory.getLog(TextMeasurer.class);
//FIXME remove this after measureSimpleText() is proven to be stable
@Property(
category = PropertyConstants.CATEGORY_FILL,
defaultValue = PropertyConstants.BOOLEAN_TRUE,
scopes = {PropertyScope.CONTEXT},
sinceVersion = PropertyConstants.VERSION_4_6_0,
valueType = Boolean.class
)
public static final String PROPERTY_MEASURE_SIMPLE_TEXTS = JRPropertiesUtil.PROPERTY_PREFIX + "measure.simple.text";
protected JasperReportsContext jasperReportsContext;
protected JRCommonText textElement;
private JRPropertiesHolder propertiesHolder;
private DynamicPropertiesHolder dynamicPropertiesHolder;
private SimpleTextLineWrapper simpleLineWrapper;
private ComplexTextLineWrapper complexLineWrapper;
protected int width;
private int height;
private int topPadding;
protected int leftPadding;
private int bottomPadding;
protected int rightPadding;
private JRParagraph jrParagraph;
private boolean isFirstParagraph;
private float formatWidth;
protected int maxHeight;
private boolean indentFirstLine;
private boolean canOverflow;
private boolean hasDynamicIgnoreMissingFontProp;
private boolean defaultIgnoreMissingFont;
private boolean ignoreMissingFont;
private boolean hasDynamicSaveLineBreakOffsetsProp;
private boolean defaultSaveLineBreakOffsets;
protected TextMeasuredState measuredState;
protected TextMeasuredState prevMeasuredState;
protected int htmlListIndent;
protected static class TextMeasuredState implements JRMeasuredText, Cloneable
{
private final boolean saveLineBreakOffsets;
protected int textOffset;
protected int lines;
protected float fontSizeSum;
protected float firstLineMaxFontSize;
protected int paragraphStartLine;
protected float textWidth;
protected float textHeight;
protected float firstLineLeading;
protected boolean isLeftToRight = true;
protected boolean isParagraphCut;
protected String textSuffix;
protected boolean isMeasured = true;
protected int lastOffset;
protected ArrayList lineBreakOffsets;
public TextMeasuredState(boolean saveLineBreakOffsets)
{
this.saveLineBreakOffsets = saveLineBreakOffsets;
}
@Override
public boolean isLeftToRight()
{
return isLeftToRight;
}
@Override
public int getTextOffset()
{
return textOffset;
}
@Override
public float getTextWidth()
{
return textWidth;
}
@Override
public float getTextHeight()
{
return textHeight;
}
@Override
public float getLineSpacingFactor()
{
if (isMeasured && lines > 0 && fontSizeSum > 0)
{
return textHeight / fontSizeSum;
}
return 0;
}
@Override
public float getLeadingOffset()
{
if (isMeasured && lines > 0 && fontSizeSum > 0)
{
return firstLineLeading - firstLineMaxFontSize * getLineSpacingFactor();
}
return 0;
}
@Override
public boolean isParagraphCut()
{
return isParagraphCut;
}
@Override
public String getTextSuffix()
{
return textSuffix;
}
public TextMeasuredState cloneState()
{
try
{
TextMeasuredState clone = (TextMeasuredState) super.clone();
//clone the list of offsets
//might be a performance problem on very large texts
if (lineBreakOffsets != null)
{
clone.lineBreakOffsets = (ArrayList) lineBreakOffsets.clone();
}
return clone;
}
catch (CloneNotSupportedException e)
{
//never
throw new JRRuntimeException(e);
}
}
protected void addLineBreak()
{
if (saveLineBreakOffsets)
{
if (lineBreakOffsets == null)
{
lineBreakOffsets = new ArrayList<>();
}
int breakOffset = textOffset - lastOffset;
lineBreakOffsets.add(breakOffset);
lastOffset = textOffset;
}
}
@Override
public short[] getLineBreakOffsets()
{
if (!saveLineBreakOffsets)
{
//if no line breaks are to be saved, return null
return null;
}
//if the last line break occurred at the truncation position
//exclude the last break offset
int exclude = lastOffset == textOffset ? 1 : 0;
if (lineBreakOffsets == null
|| lineBreakOffsets.size() <= exclude)
{
//use the zero length array singleton
return JRPrintText.ZERO_LINE_BREAK_OFFSETS;
}
short[] offsets = new short[lineBreakOffsets.size() - exclude];
boolean overflow = false;
for (int i = 0; i < offsets.length; i++)
{
int offset = lineBreakOffsets.get(i);
if (offset > Short.MAX_VALUE)
{
if (log.isWarnEnabled())
{
log.warn("Line break offset value " + offset
+ " is bigger than the maximum supported value of"
+ Short.MAX_VALUE
+ ". Line break offsets will not be saved for this text.");
}
overflow = true;
break;
}
offsets[i] = (short) offset;
}
if (overflow)
{
//if a line break offset overflow occurred, do not return any
//line break offsets
return null;
}
return offsets;
}
}
/**
*
*/
public TextMeasurer(JasperReportsContext jasperReportsContext, JRCommonText textElement)
{
this.jasperReportsContext = jasperReportsContext;
this.textElement = textElement;
this.propertiesHolder = textElement instanceof JRPropertiesHolder ? (JRPropertiesHolder) textElement : null;//FIXMENOW all elements are now properties holders, so interfaces might be rearranged
if (textElement.getDefaultStyleProvider() instanceof JRPropertiesHolder)
{
this.propertiesHolder =
new DelegatePropertiesHolder(
propertiesHolder,
(JRPropertiesHolder)textElement.getDefaultStyleProvider()
);
}
if (textElement instanceof DynamicPropertiesHolder)
{
this.dynamicPropertiesHolder = (DynamicPropertiesHolder) textElement;
// we can check this from the beginning
this.hasDynamicIgnoreMissingFontProp = this.dynamicPropertiesHolder.hasDynamicProperty(
JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT);
this.hasDynamicSaveLineBreakOffsetsProp = this.dynamicPropertiesHolder.hasDynamicProperty(
JRTextElement.PROPERTY_SAVE_LINE_BREAKS);
}
// read static property values
JRPropertiesUtil propertiesUtil = JRPropertiesUtil.getInstance(jasperReportsContext);
defaultIgnoreMissingFont = propertiesUtil.getBooleanProperty(propertiesHolder,
JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT, false);
defaultSaveLineBreakOffsets = propertiesUtil.getBooleanProperty(propertiesHolder,
JRTextElement.PROPERTY_SAVE_LINE_BREAKS, false);
Context measureContext = new Context();
simpleLineWrapper = new SimpleTextLineWrapper();
simpleLineWrapper.init(measureContext);
complexLineWrapper = new ComplexTextLineWrapper();
complexLineWrapper.init(measureContext);
}
/**
*
*/
protected void initialize(
JRStyledText styledText,
int remainingTextStart,
int availableStretchHeight,
boolean indentFirstLine,
boolean canOverflow
)
{
width = textElement.getWidth();
height = textElement.getHeight();
topPadding = textElement.getLineBox().getTopPadding();
leftPadding = textElement.getLineBox().getLeftPadding();
bottomPadding = textElement.getLineBox().getBottomPadding();
rightPadding = textElement.getLineBox().getRightPadding();
jrParagraph = textElement.getParagraph();
switch (textElement.getRotationValue())
{
case LEFT :
{
width = textElement.getHeight();
height = textElement.getWidth();
int tmpPadding = topPadding;
topPadding = leftPadding;
leftPadding = bottomPadding;
bottomPadding = rightPadding;
rightPadding = tmpPadding;
break;
}
case RIGHT :
{
width = textElement.getHeight();
height = textElement.getWidth();
int tmpPadding = topPadding;
topPadding = rightPadding;
rightPadding = bottomPadding;
bottomPadding = leftPadding;
leftPadding = tmpPadding;
break;
}
case UPSIDE_DOWN :
{
int tmpPadding = topPadding;
topPadding = bottomPadding;
bottomPadding = tmpPadding;
tmpPadding = leftPadding;
leftPadding = rightPadding;
rightPadding = tmpPadding;
break;
}
case NONE :
default :
{
}
}
formatWidth = width - leftPadding - rightPadding;
formatWidth = formatWidth < 0 ? 0 : formatWidth;
maxHeight = height + availableStretchHeight - topPadding - bottomPadding;
maxHeight = maxHeight < 0 ? 0 : maxHeight;
this.indentFirstLine = indentFirstLine;
this.canOverflow = canOverflow;
// refresh properties if required
ignoreMissingFont = defaultIgnoreMissingFont;
if (hasDynamicIgnoreMissingFontProp)
{
String dynamicIgnoreMissingFontProp = dynamicPropertiesHolder.getDynamicProperties().getProperty(
JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT);
if (dynamicIgnoreMissingFontProp != null)
{
ignoreMissingFont = JRPropertiesUtil.asBoolean(dynamicIgnoreMissingFontProp);
}
}
boolean saveLineBreakOffsets = defaultSaveLineBreakOffsets;
if (hasDynamicSaveLineBreakOffsetsProp)
{
String dynamicSaveLineBreakOffsetsProp = dynamicPropertiesHolder.getDynamicProperties().getProperty(
JRTextElement.PROPERTY_SAVE_LINE_BREAKS);
if (dynamicSaveLineBreakOffsetsProp != null)
{
saveLineBreakOffsets = JRPropertiesUtil.asBoolean(dynamicSaveLineBreakOffsetsProp);
}
}
measuredState = new TextMeasuredState(saveLineBreakOffsets);
measuredState.lastOffset = remainingTextStart;
prevMeasuredState = null;
}
@Override
public JRMeasuredText measure(
JRStyledText styledText,
int remainingTextStart,
int availableStretchHeight,
boolean indentFirstLine,
boolean canOverflow
)
{
/* */
initialize(styledText, remainingTextStart, availableStretchHeight, indentFirstLine, canOverflow);
TextLineWrapper lineWrapper = simpleLineWrapper;
// check if the simple wrapper would handle the text
if (!lineWrapper.start(styledText))
{
lineWrapper = complexLineWrapper;
lineWrapper.start(styledText);
}
// preparing bulleted list cuts is a share responsibility between the TextMeasurer here and the JRFillTextElement;
// the TextMeasurer has the ability to count how many items have been rendered from each nested list so far and sets their itemIndex and cutStart,
// while in the JRFillTextElement we are able to see where does the actual cut go, either cutting through items or in between them and thus
// decide if a bullet should be rendered and/or the cutStart adjusted by 1
StyledTextWriteContext context = new StyledTextWriteContext(true);
AttributedCharacterIterator allParagraphs = styledText.getAwtAttributedString(jasperReportsContext, ignoreMissingFont).getIterator();
allParagraphs.setIndex(remainingTextStart);
isFirstParagraph = true;
boolean rendered = true;
int runLimit = remainingTextStart;
while (rendered && runLimit < allParagraphs.getEndIndex() && (runLimit = allParagraphs.getRunLimit(JRTextAttribute.HTML_LIST_ATTRIBUTES)) <= allParagraphs.getEndIndex())
{
Map attributes = allParagraphs.getAttributes();
context.next(attributes);
prepareBullet(context);
int tokenPosition = 0;
int prevParagraphStart = 0;
String prevParagraphText = null;
String runText = styledText.getText().substring(allParagraphs.getIndex(), runLimit);
StringTokenizer tkzer = new StringTokenizer(runText, "\n", true);
// text is split into paragraphs, using the newline character as delimiter
while(tkzer.hasMoreTokens() && rendered)
{
String token = tkzer.nextToken();
if ("\n".equals(token))
{
if (tokenPosition > 0 || context.isListItemStart() || !(context.isListItemEnd() || context.isListStart() || context.isListEnd()))
{
rendered = renderParagraph(lineWrapper, allParagraphs.getIndex() + prevParagraphStart, prevParagraphText);
}
isFirstParagraph = false;
prevParagraphStart = tokenPosition + (tkzer.hasMoreTokens() || tokenPosition == 0 ? 1 : 0);
prevParagraphText = null;
}
else
{
prevParagraphStart = tokenPosition;
prevParagraphText = token;
}
tokenPosition += token.length();
}
if (rendered && prevParagraphStart < runText.length())
{
if (prevParagraphText != null || runLimit == allParagraphs.getEndIndex())
{
rendered = renderParagraph(lineWrapper, allParagraphs.getIndex() + prevParagraphStart, prevParagraphText);
}
}
allParagraphs.setIndex(runLimit);
}
return measuredState;
}
protected boolean hasParagraphIndents()
{
Integer firstLineIndent = jrParagraph.getFirstLineIndent();
if (firstLineIndent != null && firstLineIndent > 0)
{
return true;
}
Integer leftIndent = jrParagraph.getLeftIndent();
if (leftIndent != null && leftIndent > 0)
{
return true;
}
Integer rightIndent = jrParagraph.getRightIndent();
return rightIndent != null && rightIndent > 0;
}
/**
*
*/
protected boolean renderParagraph(
TextLineWrapper lineWrapper,
int paragraphStart,
String paragraphText
)
{
if (paragraphText == null)
{
lineWrapper.startEmptyParagraph(paragraphStart);
}
else
{
lineWrapper.startParagraph(
paragraphStart,
paragraphStart + paragraphText.length(),
false
);
}
List tabIndexes = JRStringUtil.getTabIndexes(paragraphText);
int[] currentTabHolder = new int[]{0};
TabStop[] nextTabStopHolder = new TabStop[]{null};
boolean[] requireNextWordHolder = new boolean[]{false};
measuredState.paragraphStartLine = measuredState.lines;
measuredState.textOffset = paragraphStart;
boolean rendered = true;
boolean renderedLine = false;
// the paragraph is measured one line at a time
while (lineWrapper.paragraphPosition() < lineWrapper.paragraphEnd() && rendered)
{
rendered = renderNextLine(lineWrapper, tabIndexes, currentTabHolder, nextTabStopHolder, requireNextWordHolder);
renderedLine = renderedLine || rendered;
}
//if we rendered at least one line, and the last line didn't fit
//and the text does not overflow
if (!rendered && prevMeasuredState != null && !canOverflow)
{
//handle last rendered row
processLastTruncatedRow(lineWrapper, paragraphText, paragraphStart, renderedLine);
}
return rendered;
}
private void prepareBullet(StyledTextWriteContext context)
{
htmlListIndent = context.getDepth() * 50;
}
protected void processLastTruncatedRow(
TextLineWrapper lineWrapper,
String paragraphText,
int paragraphOffset,
boolean lineTruncated
)
{
//FIXME move all this to TextLineWrapper?
if (lineTruncated && isToTruncateAtChar())
{
truncateLastLineAtChar(lineWrapper, paragraphText, paragraphOffset);
}
appendTruncateSuffix(lineWrapper);
}
protected void truncateLastLineAtChar(
TextLineWrapper lineWrapper,
String paragraphText,
int paragraphOffset
)
{
//truncate the original line at char
measuredState = prevMeasuredState.cloneState();
lineWrapper.startParagraph(measuredState.textOffset,
paragraphOffset + paragraphText.length(),
true);
//render again the last line
//if the line does not fit now, it will remain empty
renderNextLine(lineWrapper, null, new int[]{0}, new TabStop[]{null}, new boolean[]{false});
}
protected void appendTruncateSuffix(TextLineWrapper lineWrapper)
{
String truncateSuffx = getTruncateSuffix();
if (truncateSuffx == null)
{
return;
}
int lineStart = prevMeasuredState.textOffset;
//advance from the line start until the next line start or the first newline
String lineText = lineWrapper.getLineText(lineStart, measuredState.textOffset);
int linePosition = lineText.length();
//iterate to the beginning of the line
boolean done = false;
do
{
measuredState = prevMeasuredState.cloneState();
String text = lineText.substring(0, linePosition) + truncateSuffx;
boolean truncateAtChar = isToTruncateAtChar();
TextLineWrapper lastLineWrapper = lineWrapper.lastLineWrapper(text,
measuredState.textOffset, linePosition, truncateAtChar);
BreakIterator breakIterator =
truncateAtChar
? BreakIterator.getCharacterInstance()
: BreakIterator.getLineInstance();
breakIterator.setText(text);
if (renderNextLine(lastLineWrapper, null, new int[]{0}, new TabStop[]{null}, new boolean[]{false}))
{
int lastPos = lastLineWrapper.paragraphPosition();
//test if the entire suffix fit
if (lastPos == linePosition + truncateSuffx.length())
{
//subtract the suffix from the offset
measuredState.textOffset -= truncateSuffx.length();
measuredState.textSuffix = truncateSuffx;
done = true;
}
else
{
linePosition = breakIterator.preceding(linePosition);
if (linePosition == BreakIterator.DONE)
{
//if the text suffix did not fit the line, only the part of it that fits will show
//truncate the suffix
String actualSuffix = truncateSuffx.substring(0,
measuredState.textOffset - prevMeasuredState.textOffset);
//if the last text char is not a new line
if (prevMeasuredState.textOffset > 0
&& lineWrapper.charAt(prevMeasuredState.textOffset - 1) != '\n')
{
//force a new line so that the suffix is displayed on the last line
actualSuffix = '\n' + actualSuffix;
}
measuredState.textSuffix = actualSuffix;
//restore the next to last line offset
measuredState.textOffset = prevMeasuredState.textOffset;
done = true;
}
}
}
else
{
//if the line did not fit, leave it empty
done = true;
}
}
while (!done);
}
protected boolean isToTruncateAtChar()
{
//FIXME do not read each time
return JRPropertiesUtil.getInstance(jasperReportsContext).getBooleanProperty(propertiesHolder,
JRTextElement.PROPERTY_TRUNCATE_AT_CHAR, false);
}
protected String getTruncateSuffix()
{
//FIXME do not read each time
String truncateSuffx = JRPropertiesUtil.getInstance(jasperReportsContext).getProperty(propertiesHolder,
JRTextElement.PROPERTY_TRUNCATE_SUFFIX);
if (truncateSuffx != null)
{
truncateSuffx = truncateSuffx.trim();
if (truncateSuffx.length() == 0)
{
truncateSuffx = null;
}
}
return truncateSuffx;
}
protected boolean renderNextLine(TextLineWrapper lineWrapper, List tabIndexes, int[] currentTabHolder, TabStop[] nextTabStopHolder, boolean[] requireNextWordHolder)
{
boolean lineComplete = false;
int lineStartPosition = lineWrapper.paragraphPosition();
float maxAscent = 0;
float maxDescent = 0;
float maxLeading = 0;
int characterCount = 0;
boolean isLeftToRight = true;
// each line is split into segments, using the tab character as delimiter
List segments = new ArrayList<>(1);
TabSegment oldSegment = null;
TabSegment crtSegment = null;
// splitting the current line into tab segments
while (!lineComplete)
{
// the current segment limit is either the next tab character or the paragraph end
int tabIndexOrEndIndex = (tabIndexes == null || currentTabHolder[0] >= tabIndexes.size() ? lineWrapper.paragraphEnd() : tabIndexes.get(currentTabHolder[0]) + 1);
int firstLineIndent = lineWrapper.paragraphPosition() == 0 ? jrParagraph.getFirstLineIndent() : 0;
if (
firstLineIndent != 0
&& (isFirstParagraph && !indentFirstLine)
)
{
firstLineIndent = 0;
}
float startX = htmlListIndent + firstLineIndent + leftPadding;
float endX = width - jrParagraph.getRightIndent() - rightPadding;
endX = endX < startX ? startX : endX;
//formatWidth = endX - startX;
//formatWidth = endX;
int startIndex = lineWrapper.paragraphPosition();
float rightX = 0;
if (segments.size() == 0)
{
rightX = startX;
//nextTabStop = nextTabStop;
}
else
{
rightX = oldSegment.rightX;
nextTabStopHolder[0] = ParagraphUtil.getNextTabStop(jrParagraph, endX, rightX);
}
//float availableWidth = formatWidth - ParagraphUtil.getSegmentOffset(nextTabStopHolder[0], rightX); // nextTabStop can be null here; and that's OK
float availableWidth = endX - jrParagraph.getLeftIndent() - ParagraphUtil.getSegmentOffset(nextTabStopHolder[0], rightX); // nextTabStop can be null here; and that's OK
// creating a text layout object for each tab segment
TextLine textLine =
lineWrapper.nextLine(
availableWidth,
tabIndexOrEndIndex,
requireNextWordHolder[0]
);
if (textLine != null)
{
maxAscent = Math.max(maxAscent, textLine.getAscent());
maxDescent = Math.max(maxDescent, textLine.getDescent());
maxLeading = Math.max(maxLeading, textLine.getLeading());
characterCount += textLine.getCharacterCount();
isLeftToRight = isLeftToRight && textLine.isLeftToRight();
//creating the current segment
crtSegment = new TabSegment();
crtSegment.textLine = textLine;
float leftX = ParagraphUtil.getLeftX(nextTabStopHolder[0], textLine.getAdvance()); // nextTabStop can be null here; and that's OK
if (rightX > leftX)
{
crtSegment.leftX = rightX;
crtSegment.rightX = rightX + textLine.getAdvance();
}
else
{
crtSegment.leftX = leftX;
// we need this special tab stop based utility call because adding the advance to leftX causes rounding issues
crtSegment.rightX = ParagraphUtil.getRightX(nextTabStopHolder[0], textLine.getAdvance()); // nextTabStop can be null here; and that's OK
}
segments.add(crtSegment);
}
requireNextWordHolder[0] = true;
if (lineWrapper.paragraphPosition() == tabIndexOrEndIndex)
{
// the segment limit was a tab; going to the next tab
currentTabHolder[0] = currentTabHolder[0] + 1;
}
if (lineWrapper.paragraphPosition() == lineWrapper.paragraphEnd())
{
// the segment limit was the paragraph end; line completed and next line should start at normal zero x offset
lineComplete = true;
nextTabStopHolder[0] = null;
}
else
{
// there is paragraph text remaining
if (lineWrapper.paragraphPosition() == tabIndexOrEndIndex)
{
// the segment limit was a tab
if (crtSegment.rightX >= ParagraphUtil.getLastTabStop(jrParagraph, endX).getPosition())
{
// current segment stretches out beyond the last tab stop; line complete
lineComplete = true;
// next line should should start at first tab stop indent
nextTabStopHolder[0] = ParagraphUtil.getFirstTabStop(jrParagraph, endX);
}
// else
// {
// //nothing; this leaves lineComplete=false
// }
}
else
{
// the segment did not fit entirely
lineComplete = true;
if (textLine == null)
{
// nothing fitted; next line should start at first tab stop indent
if (nextTabStopHolder[0].getPosition() == ParagraphUtil.getFirstTabStop(jrParagraph, endX).getPosition())//FIXMETAB check based on segments.size()
{
// at second attempt we give up to avoid infinite loop
nextTabStopHolder[0] = null;
requireNextWordHolder[0] = false;
//provide dummy maxFontSize because it is used for the line height of this empty line when attempting drawing below
TextLine baseLine = lineWrapper.baseTextLine(startIndex);
maxAscent = baseLine.getAscent();
maxDescent = baseLine.getDescent();
maxLeading = baseLine.getLeading();
}
else
{
nextTabStopHolder[0] = ParagraphUtil.getFirstTabStop(jrParagraph, endX);
}
}
else
{
// something fitted
nextTabStopHolder[0] = null;
requireNextWordHolder[0] = false;
}
}
}
oldSegment = crtSegment;
}
float lineHeight = AbstractTextRenderer.getLineHeight(measuredState.lines == 0, jrParagraph, maxLeading, maxAscent);
if (measuredState.lines == 0) //FIXMEPARA
//if (measuredState.paragraphStartLine == measuredState.lines)
{
lineHeight += jrParagraph.getSpacingBefore();
}
float newTextHeight = measuredState.textHeight + lineHeight;
//this test is somewhat inconsistent with JRFillTextElement.chopTextElement which truncates the text height to int.
//thus it can happen that a text which would normally be measured to textHeight=18.6 and element height=18
//overflows when there are exactly 18 pixels left on the page.
boolean fits = newTextHeight + maxDescent <= maxHeight;
if (fits)
{
prevMeasuredState = measuredState.cloneState();
measuredState.isLeftToRight = isLeftToRight;//run direction is per layout; but this is the best we can do for now
measuredState.textWidth = Math.max(measuredState.textWidth, (crtSegment == null ? 0 : (crtSegment.rightX - leftPadding)));//FIXMENOW is RTL text actually working here?
measuredState.textHeight = newTextHeight;
measuredState.lines++;
if (
measuredState.isMeasured
&& (tabIndexes == null || tabIndexes.size() == 0)
&& !hasParagraphIndents()
)
{
measuredState.fontSizeSum +=
lineWrapper.maxFontsize(lineStartPosition, lineStartPosition + characterCount);
if (measuredState.lines == 1)
{
measuredState.firstLineLeading = measuredState.textHeight;
measuredState.firstLineMaxFontSize = measuredState.fontSizeSum;
}
}
else
{
measuredState.isMeasured = false;
}
// here is the Y offset where we would draw the line
//lastDrawPosY = drawPosY;
//
measuredState.textHeight += maxDescent;
measuredState.textOffset += lineWrapper.paragraphPosition() - lineStartPosition;
if (lineWrapper.paragraphPosition() < lineWrapper.paragraphEnd())
{
//if not the last line in a paragraph, save the line break position
measuredState.addLineBreak();
}
// else //FIXMEPARA
// {
// measuredState.textHeight += jrParagraph.getSpacingAfter();
// }
measuredState.isParagraphCut = lineWrapper.paragraphPosition() < lineWrapper.paragraphEnd();
}
return fits;
}
protected JRPropertiesHolder getTextPropertiesHolder()
{
return propertiesHolder;
}
/**
*
*/
public FontRenderContext getFontRenderContext()
{
return AwtTextRenderer.LINE_BREAK_FONT_RENDER_CONTEXT;
}
class Context implements TextMeasureContext
{
@Override
public JasperReportsContext getJasperReportsContext()
{
return jasperReportsContext;
}
@Override
public JRCommonText getElement()
{
return textElement;
}
@Override
public JRPropertiesHolder getPropertiesHolder()
{
return propertiesHolder;
}
@Override
public boolean isIgnoreMissingFont()
{
return ignoreMissingFont;
}
@Override
public FontRenderContext getFontRenderContext()
{
return TextMeasurer.this.getFontRenderContext();
}
}
}
class TabSegment
{
public TextLine textLine;
public float leftX;
public float rightX;
}