Java tutorial
/* * JasperReports - Free Java Reporting Library. * Copyright (C) 2001 - 2019 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 <http://www.gnu.org/licenses/>. */ package net.sf.jasperreports.engine.fill; import java.awt.font.FontRenderContext; import java.text.BreakIterator; import java.util.ArrayList; import java.util.List; 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.ParagraphUtil; import net.sf.jasperreports.properties.PropertyConstants; /** * Default text measurer implementation. * <h3>Text Measuring</h3> * 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. * <p/> * 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. * <p/> * 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 <code>jasperreports.properties</code> 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 * <code>net.sf.jasperreports.text.measurer.factory.<alias></code> as key and the factory * class name as value. Take the following examples of text measurer factory properties: * <ul> * <li>in jasperreports.properties set a custom default text measurer factory: * <br/> * <code>net.sf.jasperreports.text.measurer.factory=com.jasperreports.MyTextMeasurerFactory</code></li> * <li>define an alias for a different text measurer factory: * <br/> * <code>net.sf.jasperreports.text.measurer.factory.fast=com.jasperreports.MyFastTextMeasurerFactory</code></li> * <li>in a JRXML, use the fast text measurer for a static text:</li> * </ul> * <pre> * <staticText> * <reportElement ...> * <property name="net.sf.jasperreports.text.measurer.factory" value="fast"/> * </reportElement> * <text>...</text> * </staticText> * </pre> * 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 <code>default</code>. * <h3>Text Truncation</h3> * 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. * <p/> * The default behavior is to use the standard AWT line break logic (as returned by the * <code>java.text.BreakIterator.getLineInstance()</code> 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. * <p/> * 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. * <p/> * 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: * <ul> * <li>globally in <code>jasperreports.properties</code>: * <br/> * <code>net.sf.jasperreports.text.truncate.suffix=\u2026</code></li> * <li>at report level:</li> * </ul> * <pre> * <jasperReport ...> * <property name="net.sf.jasperreports.text.truncate.suffix" value="&#x2026;"/> * ... * </jasperReport> * </pre> * 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. * <p/> * 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. * <p/> * 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. * <p/> * 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. * <p/> * 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. * <p/> * 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 (teodord@users.sourceforge.net) */ 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 complextLineWrapper; protected int width; private int height; private int topPadding; protected int leftPadding; private int bottomPadding; protected int rightPadding; private JRParagraph jrParagraph; private float formatWidth; protected int maxHeight; 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 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 String textSuffix; protected boolean isMeasured = true; protected int lastOffset; protected ArrayList<Integer> 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 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<Integer>) lineBreakOffsets.clone(); } return clone; } catch (CloneNotSupportedException e) { //never throw new JRRuntimeException(e); } } protected void addLineBreak() { if (saveLineBreakOffsets) { if (lineBreakOffsets == null) { lineBreakOffsets = new ArrayList<Integer>(); } 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); complextLineWrapper = new ComplexTextLineWrapper(); complextLineWrapper.init(measureContext); } /** * */ protected void initialize(JRStyledText styledText, int remainingTextStart, int availableStretchHeight, 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.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 canOverflow) { /* */ initialize(styledText, remainingTextStart, availableStretchHeight, canOverflow); TextLineWrapper lineWrapper = simpleLineWrapper; // check if the simple wrapper would handle the text if (!lineWrapper.start(styledText)) { lineWrapper = complextLineWrapper; lineWrapper.start(styledText); } int tokenPosition = remainingTextStart; int lastParagraphStart = remainingTextStart; String lastParagraphText = null; String remainingText = styledText.getText().substring(remainingTextStart); StringTokenizer tkzer = new StringTokenizer(remainingText, "\n", true); boolean rendered = true; // text is split into paragraphs, using the newline character as delimiter while (tkzer.hasMoreTokens() && rendered) { String token = tkzer.nextToken(); if ("\n".equals(token)) { rendered = renderParagraph(lineWrapper, lastParagraphStart, lastParagraphText); lastParagraphStart = tokenPosition + (tkzer.hasMoreTokens() || tokenPosition == 0 ? 1 : 0); lastParagraphText = null; } else { lastParagraphStart = tokenPosition; lastParagraphText = token; } tokenPosition += token.length(); } if (rendered && lastParagraphStart < remainingTextStart + remainingText.length()) { renderParagraph(lineWrapper, lastParagraphStart, lastParagraphText); } 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 lastParagraphStart, String lastParagraphText) { if (lastParagraphText == null) { lineWrapper.startEmptyParagraph(lastParagraphStart); } else { lineWrapper.startParagraph(lastParagraphStart, lastParagraphStart + lastParagraphText.length(), false); } List<Integer> tabIndexes = JRStringUtil.getTabIndexes(lastParagraphText); int[] currentTabHolder = new int[] { 0 }; TabStop[] nextTabStopHolder = new TabStop[] { null }; boolean[] requireNextWordHolder = new boolean[] { false }; measuredState.paragraphStartLine = measuredState.lines; measuredState.textOffset = lastParagraphStart; 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, lastParagraphText, lastParagraphStart, renderedLine); } return rendered; } 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<Integer> 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<TabSegment> segments = new ArrayList<TabSegment>(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); float startX = (lineWrapper.paragraphPosition() == 0 ? jrParagraph.getFirstLineIndent() : 0) + 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(); // } } 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; }