net.sf.jasperreports.engine.fill.SimpleTextLineWrapper 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;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.LineMetrics;
import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D;
import java.lang.Character.UnicodeBlock;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.text.Bidi;
import java.text.BreakIterator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
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.JRPropertiesUtil;
import net.sf.jasperreports.engine.fonts.AwtFontAttribute;
import net.sf.jasperreports.engine.fonts.FontUtil;
import net.sf.jasperreports.engine.util.JRStyledText;
import net.sf.jasperreports.engine.util.JRStyledText.Run;
import net.sf.jasperreports.engine.util.Pair;
import net.sf.jasperreports.properties.PropertyConstants;
/**
* @author Lucian Chirita ([email protected])
*/
public class SimpleTextLineWrapper implements TextLineWrapper
{
@Property(
category = PropertyConstants.CATEGORY_FILL,
scopes = {PropertyScope.CONTEXT},
sinceVersion = PropertyConstants.VERSION_4_7_1
)
public static final String PROPERTY_MEASURE_EXACT =
JRPropertiesUtil.PROPERTY_PREFIX + "measure.simple.text.exact";
@Property(
category = PropertyConstants.CATEGORY_FILL,
defaultValue = "2000",
scopes = {PropertyScope.CONTEXT},
sinceVersion = PropertyConstants.VERSION_4_7_1,
valueType = Integer.class
)
public static final String PROPERTY_ELEMENT_CACHE_SIZE =
JRPropertiesUtil.PROPERTY_PREFIX + "measure.simple.text.element.cache.size";
public static final String MEASURE_EXACT_ALWAYS = "always";
public static final String MEASURE_EXACT_MULTILINE = "multiline";
private static final Log log = LogFactory.getLog(SimpleTextLineWrapper.class);
protected static final int FONT_MIN_COUNT = 10;
protected static final double FONT_SIZE_MIN_FACTOR = 0.1;
protected static final double FONT_WIDTH_CHECK_FACTOR = 1.2;
protected static final int NEXT_BREAK_INDEX_THRESHOLD = 3;
protected static final int COMPEX_LAYOUT_START_CHAR = 0x0300;// got this from sun.font.FontUtilities
protected static final int COMPEX_LAYOUT_END_CHAR = 0x206F;// got this from sun.font.FontUtilities
protected static final String FILL_CACHE_KEY_ELEMENT_FONT_INFOS =
SimpleTextLineWrapper.class.getName() + "#elementFontInfos";
protected static final String FILL_CACHE_KEY_GENERAL_FONT_INFOS =
SimpleTextLineWrapper.class.getName() + "#generalFontInfos";
protected static final Set simpleLayoutBlocks;
static
{
// white list of Unicode blocks that have simple text layout
simpleLayoutBlocks = new HashSet<>();
// got these from sun.font.FontUtilities, but the list is not exhaustive
simpleLayoutBlocks.add(Character.UnicodeBlock.GREEK);
simpleLayoutBlocks.add(Character.UnicodeBlock.CYRILLIC);
simpleLayoutBlocks.add(Character.UnicodeBlock.CYRILLIC_SUPPLEMENTARY);
simpleLayoutBlocks.add(Character.UnicodeBlock.ARMENIAN);
simpleLayoutBlocks.add(Character.UnicodeBlock.SYRIAC);
simpleLayoutBlocks.add(Character.UnicodeBlock.THAANA);
simpleLayoutBlocks.add(Character.UnicodeBlock.MYANMAR);
simpleLayoutBlocks.add(Character.UnicodeBlock.GEORGIAN);
simpleLayoutBlocks.add(Character.UnicodeBlock.ETHIOPIC);
simpleLayoutBlocks.add(Character.UnicodeBlock.TAGALOG);
simpleLayoutBlocks.add(Character.UnicodeBlock.MONGOLIAN);
simpleLayoutBlocks.add(Character.UnicodeBlock.LATIN_EXTENDED_ADDITIONAL);
simpleLayoutBlocks.add(Character.UnicodeBlock.GREEK_EXTENDED);
}
// storing per instance to avoid too many calls (and to allow runtime level changes)
private final boolean logTrace = log.isTraceEnabled();
private TextMeasureContext context;
private boolean measureSimpleTexts;
private boolean measureExact;
private boolean measureExactMultiline;
private Map fontInfos;
private String wholeText;
private FontKey fontKey;
private ElementFontInfo fontInfo;
private String paragraphText;
private boolean paragraphTruncateAtChar;
private boolean paragraphLeftToRight;
private boolean paragraphMeasureExact;
private int paragraphOffset;
private int paragraphPosition;
private BreakIterator paragraphBreakIterator;
public SimpleTextLineWrapper()
{
}
public SimpleTextLineWrapper(SimpleTextLineWrapper parent)
{
this.context = parent.context;
this.measureSimpleTexts = parent.measureSimpleTexts;
this.measureExact = parent.measureExact;
this.measureExactMultiline = parent.measureExactMultiline;
this.fontInfos = parent.fontInfos;
this.wholeText = parent.wholeText;
this.fontKey = parent.fontKey;
this.fontInfo = parent.fontInfo;
}
@Override
public void init(TextMeasureContext context)
{
this.context = context;
JRPropertiesUtil properties = JRPropertiesUtil.getInstance(context.getJasperReportsContext());
measureSimpleTexts = properties.getBooleanProperty(context.getPropertiesHolder(),
TextMeasurer.PROPERTY_MEASURE_SIMPLE_TEXTS, true);
if (measureSimpleTexts)
{
String exactProp = properties.getProperty(context.getPropertiesHolder(), PROPERTY_MEASURE_EXACT);
if (exactProp != null)
{
if (MEASURE_EXACT_ALWAYS.equals(exactProp))
{
measureExact = true;
}
else if (MEASURE_EXACT_MULTILINE.equals(exactProp))
{
measureExactMultiline = true;
}
}
fontInfos = new HashMap<>();
}
}
@Override
public boolean start(JRStyledText styledText)
{
if (!measureSimpleTexts)
{
return false;
}
List runs = styledText.getRuns();
if (runs.size() != 1)
{
// multiple styles
return false;
}
wholeText = styledText.getText();
if (wholeText.indexOf('\t') >= 0)
{
// supporting tabs is more difficult because we'd need
// measureParagraphFragment to include the white space advance.
return false;
}
Run run = styledText.getRuns().get(0);
if (run.attributes.get(TextAttribute.SUPERSCRIPT) != null)
{
// not handling this case, see JRStyledText.getAwtAttributedString
return false;
}
AwtFontAttribute fontAttribute = AwtFontAttribute.fromAttributes(run.attributes);
Number size = (Number) run.attributes.get(TextAttribute.SIZE);
if (!fontAttribute.hasAttribute() || size == null)
{
// this should not happen, but still checking
return false;
}
int style = 0;
Number posture = (Number) run.attributes.get(TextAttribute.POSTURE);
if (posture != null && !TextAttribute.POSTURE_REGULAR.equals(posture))
{
if (TextAttribute.POSTURE_OBLIQUE.equals(posture))
{
style |= Font.ITALIC;
}
else
{
// non standard posture
return false;
}
}
Number weight = (Number) run.attributes.get(TextAttribute.WEIGHT);
if (weight != null && !TextAttribute.WEIGHT_REGULAR.equals(weight))
{
if (TextAttribute.WEIGHT_BOLD.equals(weight))
{
style |= Font.BOLD;
}
else
{
// non standard weight
return false;
}
}
fontKey = new FontKey(fontAttribute, size.floatValue(), style, styledText.getLocale());
createFontInfo(run.attributes);
return true;
}
protected void createFontInfo(Map textAttributes)
{
fontInfo = fontInfos.get(fontKey);
if (fontInfo != null)
{
// found in local cache
return;
}
Map, ElementFontInfo> elementFontInfos = null;
Pair elementFontKey = null;
// look in the fill cache
if (context.getElement() instanceof JRFillElement)
{
JRFillElement fillElement = (JRFillElement) context.getElement();
JRFillContext fillContext = fillElement.getFiller().getFillContext();
elementFontKey = new Pair<>(fillElement.getUUID(), fontKey);
elementFontInfos = (Map, ElementFontInfo>) fillContext.getFillCache(FILL_CACHE_KEY_ELEMENT_FONT_INFOS);
if (elementFontInfos == null)
{
elementFontInfos = createElementFontInfosFillCache();
fillContext.setFillCache(FILL_CACHE_KEY_ELEMENT_FONT_INFOS, elementFontInfos);
}
fontInfo = elementFontInfos.get(elementFontKey);
}
if (fontInfo == null)
{
// did not find in the general cache, create the font info
// we first need the general font info
FontInfo generalFontInfo = getGeneralFontInfo(textAttributes);
if (logTrace)
{
log.trace("creating element font info for " + fontKey
+ (elementFontKey == null ? "" : (" and element " + elementFontKey.first())));
}
fontInfo = new ElementFontInfo(generalFontInfo);
fontInfos.put(fontKey, fontInfo);
if (elementFontInfos != null && elementFontKey.first() != null)//UUID should not be null but check to be sure
{
elementFontInfos.put(elementFontKey, fontInfo);
}
}
}
protected HashMap, ElementFontInfo> createElementFontInfosFillCache()
{
final int cacheSize = JRPropertiesUtil.getInstance(context.getJasperReportsContext()).getIntegerProperty(
PROPERTY_ELEMENT_CACHE_SIZE, 2000);//hardcoded default
if (log.isDebugEnabled())
{
log.debug("creating element font infos cache of size " + cacheSize);
}
// creating a LRU map
return new LinkedHashMap, SimpleTextLineWrapper.ElementFontInfo>(64, 0.75f, true)
{
@Override
protected boolean removeEldestEntry(Entry, ElementFontInfo> eldest)
{
return size() > cacheSize;
}
};
}
protected FontInfo getGeneralFontInfo(Map textAttributes)
{
Map generalFontInfos = null;
FontInfo generalFontInfo = null;
// look in the fill cache
if (context.getElement() instanceof JRFillElement)
{
JRFillElement fillElement = (JRFillElement) context.getElement();
JRFillContext fillContext = fillElement.getFiller().getFillContext();
generalFontInfos = (Map) fillContext.getFillCache(FILL_CACHE_KEY_GENERAL_FONT_INFOS);
if (generalFontInfos == null)
{
generalFontInfos = new HashMap<>();
fillContext.setFillCache(FILL_CACHE_KEY_GENERAL_FONT_INFOS, generalFontInfos);
}
generalFontInfo = generalFontInfos.get(fontKey);
}
if (generalFontInfo == null)
{
Font font = loadFont(textAttributes);
boolean complexLayout = determineComplexLayout(font);
// computing the leading a single time, assuming that it doesn't change with text
//FIXME verify if computing leading for each line is needed
float leading = determineLeading(font);
if (logTrace)
{
log.trace("font " + font + " has complex layout " + complexLayout
+ ", leading " + leading);
}
generalFontInfo = new FontInfo(font, complexLayout, leading);
if (generalFontInfos != null)
{
generalFontInfos.put(fontKey, generalFontInfo);
}
}
return generalFontInfo;
}
protected Font loadFont(Map textAttributes)
{
// check bundled fonts
FontUtil fontUtil = FontUtil.getInstance(context.getJasperReportsContext());
Font font = fontUtil.getAwtFontFromBundles(fontKey.fontAttribute, fontKey.style, fontKey.size, fontKey.locale, false);
if (font == null)
{
// checking AWT font
fontUtil.checkAwtFont(fontKey.fontAttribute.getFamily(), context.isIgnoreMissingFont());
// creating AWT font
// FIXME using the current text attributes might be slightly dangerous as we are sharing font metrics
font = Font.getFont(textAttributes);
}
return font;
}
protected boolean determineComplexLayout(Font font)
{
// this tries to emulate the tests in Font.getStringBounds()
//FIXME use font.hasLayoutAttributes() instead of this?
Map fontAttributes = font.getAttributes();
Object kerning = fontAttributes.get(TextAttribute.KERNING);
Object ligatures = fontAttributes.get(TextAttribute.LIGATURES);
return (kerning != null && TextAttribute.KERNING_ON.equals(kerning))
|| (ligatures != null && TextAttribute.LIGATURES_ON.equals(ligatures))
|| font.isTransformed();
}
protected float determineLeading(Font font)
{
LineMetrics lineMetrics = font.getLineMetrics(" ", context.getFontRenderContext());
return lineMetrics.getLeading();
}
@Override
public void startParagraph(int paragraphStart, int paragraphEnd,
boolean truncateAtChar)
{
String text = wholeText.substring(paragraphStart, paragraphEnd);
startParagraph(text, paragraphStart, truncateAtChar);
}
@Override
public void startEmptyParagraph(int paragraphStart)
{
startParagraph(" ", paragraphStart, false);
}
protected void startParagraph(String text, int start, boolean truncateAtChar)
{
paragraphText = text;
paragraphTruncateAtChar = truncateAtChar;
char[] textChars = text.toCharArray();
// direction is per paragraph
paragraphLeftToRight = isLeftToRight(textChars);
paragraphMeasureExact = isParagraphMeasureExact(textChars);
if (logTrace)
{
log.trace("paragraph start at " + start
+ ", truncate at char " + truncateAtChar
+ ", LTR " + paragraphLeftToRight
+ ", exact measure " + paragraphMeasureExact);
}
paragraphOffset = start;
paragraphPosition = 0;
paragraphBreakIterator = truncateAtChar ? BreakIterator.getCharacterInstance()
: BreakIterator.getLineInstance();
paragraphBreakIterator.setText(paragraphText);
}
protected boolean isLeftToRight(char[] chars)
{
boolean leftToRight = true;
if (Bidi.requiresBidi(chars, 0, chars.length))
{
// determining the text direction
// using default LTR as there's no way to have other default in the text
Bidi bidi = new Bidi(chars, 0, null, 0, chars.length, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
leftToRight = bidi.baseIsLeftToRight();
}
return leftToRight;
}
protected boolean isParagraphMeasureExact(char[] chars)
{
// when we have complex text layout or truncating at char,
// perform exact break measurement as estimating/guessing could be slower
if (measureExact
|| fontInfo.fontInfo.complexLayout
|| paragraphTruncateAtChar)
{
return true;
}
return hasComplexLayout(chars);
}
protected boolean hasComplexLayout(char[] chars)
{
UnicodeBlock prevBlock = null;
for (int i = 0; i < chars.length; i++)
{
char ch = chars[i];
if (ch >= COMPEX_LAYOUT_START_CHAR && ch <= COMPEX_LAYOUT_END_CHAR)
{
//FIXME use icu4j or CharPredicateCache
UnicodeBlock chBlock = Character.UnicodeBlock.of(ch);
if (chBlock == null)
{
// being conservative
return true;
}
// if the same block as the previous block, avoid going to the hash set
// this could offer some speed improvement
if (prevBlock != chBlock)
{
prevBlock = chBlock;
if (!simpleLayoutBlocks.contains(chBlock))
{
return true;
}
}
}
}
return false;
}
@Override
public int paragraphPosition()
{
return paragraphPosition;
}
@Override
public int paragraphEnd()
{
return paragraphText.length();
}
@Override
public TextLine nextLine(float width, int endLimit, boolean requireWord)
{
if (logTrace)
{
log.trace("simple line measurement at " + (paragraphOffset + paragraphPosition)
+ " to " + (paragraphOffset + endLimit)
+ " in width " + width
+ " with font " + fontInfo);
}
// the result
TextLine textLine;
if (useExactLineMeasurement())
{
textLine = measureExactLine(width, endLimit, requireWord);
}
else
{
textLine = measureLine(width, requireWord, endLimit);
}
return textLine;
}
protected boolean useExactLineMeasurement()
{
// when missing a character width estimate perform one exact measurement
return paragraphMeasureExact
|| !fontInfo.hasCharWidthEstimate();
}
protected TextLine measureExactLine(float width, int endLimit, boolean requireWord)
{
int breakIndex = measureExactLineBreakIndex(width, endLimit, requireWord);
if (breakIndex <= paragraphPosition)
{
// nothing fit
return null;
}
Rectangle2D lineBounds = measureParagraphFragment(breakIndex);
return toTextLine(breakIndex, lineBounds);
}
protected int measureExactLineBreakIndex(float width, int endLimit, boolean requireWord)
{
//FIXME would it be faster to create and cache a LineBreakMeasurer for the whole paragraph?
Map attributes = new HashMap<>();
// we only need the font as it includes the size and style
attributes.put(TextAttribute.FONT, fontInfo.fontInfo.font);
String textLine = paragraphText.substring(paragraphPosition, endLimit);
AttributedString attributedLine = new AttributedString(textLine, attributes);
// we need a fresh iterator for the line
BreakIterator breakIterator = paragraphTruncateAtChar ? BreakIterator.getCharacterInstance()
: BreakIterator.getLineInstance();
LineBreakMeasurer breakMeasurer = new LineBreakMeasurer(attributedLine.getIterator(),
breakIterator, context.getFontRenderContext());
int breakIndex = breakMeasurer.nextOffset(width, endLimit - paragraphPosition, requireWord)
+ paragraphPosition;
if (logTrace)
{
log.trace("exact line break index measured at " + (paragraphOffset + breakIndex));
}
return breakIndex;
}
protected TextLine measureLine(float width, boolean requireWord, int endLimit)
{
// try to guess how much of the text would fit based on the average char width
int measureIndex = estimateBreakIndex(width, endLimit);
// if estimating that there's more than a line, check measureExactMultiline
if (measureIndex < endLimit && measureExactMultiline)
{
return measureExactLine(width, endLimit, requireWord);
}
// measure the text
Rectangle2D bounds = measureParagraphFragment(measureIndex);
//FIXME fast exit when the height is exceeded
Rectangle2D measuredBounds = bounds;
if (bounds.getWidth() <= width)
{
// see if there's more that could fit
boolean done = false;
do
{
int nextBreakIndex = measureIndex < endLimit? paragraphBreakIterator.following(measureIndex)
: BreakIterator.DONE;
if (nextBreakIndex == BreakIterator.DONE || nextBreakIndex > endLimit)
{
// the next break is after the limit, we're done
done = true;
}
else
{
// measure to the next break
Rectangle2D nextBounds = measureParagraphFragment(nextBreakIndex);
if (nextBounds.getWidth() <= width)
{
measuredBounds = nextBounds;
measureIndex = nextBreakIndex;
// loop
}
else
{
done = true;
}
}
} while (!done);
}
else
{
// didn't fit, try shorter texts
boolean done = false;
do
{
int previousBreakIndex = measureIndex > paragraphPosition ? paragraphBreakIterator.preceding(measureIndex)
: BreakIterator.DONE;
if (previousBreakIndex == BreakIterator.DONE || previousBreakIndex <= paragraphPosition)
{
if (requireWord)
{
// no full word fits, returning empty
measureIndex = paragraphPosition;
}
else
{
// we need to break inside the word.
// measuring the exact break index as estimating/guessing might be slower.
measureIndex = measureExactLineBreakIndex(width, endLimit, requireWord);
measuredBounds = measureParagraphFragment(measureIndex);
}
done = true;
}
else
{
measureIndex = previousBreakIndex;
Rectangle2D prevBounds = measureParagraphFragment(measureIndex);
if (prevBounds.getWidth() <= width)
{
// fitted, we're done
measuredBounds = prevBounds;
done = true;
}
}
} while (!done);
}
if (measureIndex <= paragraphPosition)
{
// nothing fit
return null;
}
return toTextLine(measureIndex, measuredBounds);
}
protected int estimateBreakIndex(float width, int endLimit)
{
double avgCharWidth = fontInfo.charWidthEstimate();
if ((endLimit - paragraphPosition) * avgCharWidth <= width * FONT_WIDTH_CHECK_FACTOR)
{
// there are chances that the entire text would fit, let's be optimistic
return endLimit;
}
// estimate how many characters would fit
int charCountEstimate = (int) Math.ceil(width / avgCharWidth);
int estimateFitPosition = paragraphPosition + charCountEstimate;
if (estimateFitPosition > endLimit)
{
// estimated that everything would fit
return endLimit;
}
// find the break after the estimate
int breakAfterEstimatePosition = paragraphBreakIterator.following(estimateFitPosition);
if (breakAfterEstimatePosition == BreakIterator.DONE || breakAfterEstimatePosition > endLimit)
{
breakAfterEstimatePosition = endLimit;
}
int estimateIndex = breakAfterEstimatePosition;
// if the after break is too far way from the estimate, see if the break before is closer
if (breakAfterEstimatePosition > estimateFitPosition + NEXT_BREAK_INDEX_THRESHOLD)
{
int breakBeforeEstimatePosition = paragraphBreakIterator.previous();
// if the break before is closer than the break after, use the break before
if (breakBeforeEstimatePosition == BreakIterator.DONE
&& breakBeforeEstimatePosition > paragraphPosition
&& estimateFitPosition - breakBeforeEstimatePosition < breakAfterEstimatePosition - estimateFitPosition)
{
estimateIndex = breakBeforeEstimatePosition;
}
}
return estimateIndex;
}
protected Rectangle2D measureParagraphFragment(int measureIndex)
{
int endIndex = measureIndex;
if (endIndex > paragraphPosition + 1) {
char lastMeasureChar = paragraphText.charAt(endIndex - 1);
if (Character.isWhitespace(lastMeasureChar)) {
// exclude trailing white space from the text to measure.
// use the previous break as limit, but always keep at least one character to measure.
int preceding = paragraphBreakIterator.preceding(endIndex);
if (preceding == BreakIterator.DONE || preceding <= paragraphPosition) {
preceding = paragraphPosition + 1;
}
do {
--endIndex;
lastMeasureChar = paragraphText.charAt(endIndex - 1);
} while (endIndex > preceding
&& Character.isWhitespace(lastMeasureChar));
}
}
// note that trailing white space will not be included in the advance
Rectangle2D bounds = fontInfo.fontInfo.font.getStringBounds(paragraphText, paragraphPosition, endIndex,
context.getFontRenderContext());
// adding the measurement to the font info statistics
fontInfo.recordMeasurement(bounds.getWidth() / (endIndex - paragraphPosition));
if (logTrace)
{
log.trace("measured to index " + (endIndex + paragraphOffset) + " at width " + bounds.getWidth());
}
return bounds;
}
protected TextLine toTextLine(int measureIndex,
Rectangle2D measuredBounds)
{
SimpleTextLine textLine = new SimpleTextLine();
textLine.setAscent((float) -measuredBounds.getY());
textLine.setDescent((float) (measuredBounds.getMaxY() - fontInfo.fontInfo.leading));
textLine.setLeading(fontInfo.fontInfo.leading);
textLine.setCharacterCount(measureIndex - paragraphPosition);
textLine.setAdvance((float) measuredBounds.getWidth());
textLine.setLeftToRight(paragraphLeftToRight);
// update the paragraph position
paragraphPosition = measureIndex;
return textLine;
}
@Override
public TextLine baseTextLine(int index)
{
// this should only be called when the text is tabbed, which is not supported
throw new UnsupportedOperationException();
}
@Override
public float maxFontsize(int start, int end)
{
return fontKey.size;
}
@Override
public String getLineText(int start, int end)
{
int newLineIdx = wholeText.indexOf('\n', start);
int endIdx = (newLineIdx >= 0 && newLineIdx < end) ? newLineIdx : end;
return wholeText.substring(start, endIdx);
}
@Override
public char charAt(int index)
{
return wholeText.charAt(index);
}
@Override
public TextLineWrapper lastLineWrapper(String lineText, int start, int textLength,
boolean truncateAtChar)
{
if (logTrace)
{
log.trace("last line wrapper at " + start + ", textLength " + textLength);
}
SimpleTextLineWrapper lastLineWrapper = new SimpleTextLineWrapper(this);
lastLineWrapper.startParagraph(lineText, start, truncateAtChar);
return lastLineWrapper;
}
protected static class FontKey
{
AwtFontAttribute fontAttribute;
float size;
int style;
Locale locale;
public FontKey(AwtFontAttribute fontAttribute, float size, int style, Locale locale)
{
super();
this.fontAttribute = fontAttribute;
this.size = size;
this.style = style;
this.locale = locale;
}
@Override
public int hashCode()
{
int hash = 43;
hash = hash*29 + fontAttribute.hashCode();
hash = hash*29 + Float.floatToIntBits(size);
hash = hash*29 + style;
hash = hash*29 + (locale == null ? 0 : locale.hashCode());
return hash;
}
@Override
public boolean equals(Object obj)
{
FontKey info = (FontKey) obj;
return fontAttribute.equals(info.fontAttribute) && size == info.size && style == info.style
&& ((locale == null) ? (info.locale == null) : (info.locale != null && locale.equals(info.locale)));
}
@Override
public String toString()
{
return "{font: " + fontAttribute
+ ", size: " + size
+ ", style: " + style
+ "}";
}
}
protected static class FontInfo
{
final Font font;
final boolean complexLayout;
final float leading;
final FontStatistics fontStatistics;
public FontInfo(Font font, boolean complexLayout, float leading)
{
this.font = font;
this.complexLayout = complexLayout;
this.leading = leading;
this.fontStatistics = new FontStatistics();
}
@Override
public String toString()
{
return font.toString();
}
}
protected static class FontStatistics
{
int measurementsCount;
double characterWidthSum;
public void recordMeasurement(double avgWidth)
{
++measurementsCount;
characterWidthSum += avgWidth;
}
}
protected static class ElementFontInfo
{
final FontInfo fontInfo;
final FontStatistics fontStatistics;
public ElementFontInfo(FontInfo fontInfo)
{
this.fontInfo = fontInfo;
this.fontStatistics = new FontStatistics();
}
public boolean hasCharWidthEstimate()
{
return fontStatistics.measurementsCount > 0
|| fontInfo.fontStatistics.measurementsCount > 0;
}
public double charWidthEstimate()
{
double avgCharWidth;
if (fontStatistics.measurementsCount > 0)
{
avgCharWidth = fontStatistics.characterWidthSum / fontStatistics.measurementsCount;
}
else if (fontInfo.fontStatistics.measurementsCount > 0)
{
avgCharWidth = fontInfo.fontStatistics.characterWidthSum / fontInfo.fontStatistics.measurementsCount;
}
else
{
throw new IllegalStateException("No measurement available for char width estimate");
}
return avgCharWidth;
}
public void recordMeasurement(double avgWidth)
{
fontStatistics.recordMeasurement(avgWidth);
fontInfo.fontStatistics.recordMeasurement(avgWidth);
}
@Override
public String toString()
{
return fontInfo.font.toString();
}
}
}