/* * JasperReports - Free Java Reporting Library. * Copyright (C) 2001 - 2009 Jaspersoft Corporation. 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.awt.font.LineBreakMeasurer; import java.awt.font.TextLayout; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.text.BreakIterator; import java.text.CharacterIterator; import java.text.AttributedCharacterIterator.Attribute; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.StringTokenizer; import net.sf.jasperreports.engine.JRCommonText; import net.sf.jasperreports.engine.JRPrintText; import net.sf.jasperreports.engine.JRPropertiesHolder; import net.sf.jasperreports.engine.JRRuntimeException; import net.sf.jasperreports.engine.JRTextElement; import net.sf.jasperreports.engine.export.TextRenderer; import net.sf.jasperreports.engine.util.DelegatePropertiesHolder; import net.sf.jasperreports.engine.util.JRProperties; import net.sf.jasperreports.engine.util.JRStyledText; import net.sf.jasperreports.engine.util.MaxFontSizeFinder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Default text measurer implementation. * * @author Teodor Danciu (teodord@users.sourceforge.net) * @version $Id: TextMeasurer.java 3746 2010-04-16 17:05:18Z lucianc $ */ public class TextMeasurer implements JRTextMeasurer { private static final Log log = LogFactory.getLog(TextMeasurer.class); /** * */ private static final FontRenderContext FONT_RENDER_CONTEXT = TextRenderer.LINE_BREAK_FONT_RENDER_CONTEXT; private JRCommonText textElement; private JRPropertiesHolder propertiesHolder; /** * */ private MaxFontSizeFinder maxFontSizeFinder = null; private int width = 0; private int height = 0; private int topPadding = 0; private int leftPadding = 0; private int bottomPadding = 0; private int rightPadding = 0; private float lineSpacing = 0; private float formatWidth = 0; private int maxHeight = 0; private boolean canOverflow; private Map globalAttributes; private TextMeasuredState measuredState; private TextMeasuredState prevMeasuredState; protected static class TextMeasuredState implements JRMeasuredText, Cloneable { private final boolean saveLineBreakOffsets; protected int textOffset = 0; protected int lines = 0; protected int fontSizeSum = 0; protected int firstLineMaxFontSize = 0; protected float textHeight = 0; protected float firstLineLeading = 0; protected boolean isLeftToRight = true; protected String textSuffix = null; protected int lastOffset = 0; protected ArrayList lineBreakOffsets; public TextMeasuredState(boolean saveLineBreakOffsets) { this.saveLineBreakOffsets = saveLineBreakOffsets; } public boolean isLeftToRight() { return isLeftToRight; } public int getTextOffset() { return textOffset; } public float getTextHeight() { return textHeight; } public float getLineSpacingFactor() { if (lines > 0) { return textHeight / fontSizeSum; } return 0; } public float getLeadingOffset() { return firstLineLeading - firstLineMaxFontSize * getLineSpacingFactor(); } 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(Integer.valueOf(breakOffset)); lastOffset = textOffset; } } 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 = ((Integer) lineBreakOffsets.get(i)).intValue(); 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(JRCommonText textElement) { 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() ); } } /** * */ protected void initialize(JRStyledText styledText, int remainingTextStart, int availableStretchHeight, boolean canOverflow) { width = textElement.getWidth(); height = textElement.getHeight(); topPadding = textElement.getLineBox().getTopPadding().intValue(); leftPadding = textElement.getLineBox().getLeftPadding().intValue(); bottomPadding = textElement.getLineBox().getBottomPadding().intValue(); rightPadding = textElement.getLineBox().getRightPadding().intValue(); 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 : { } } /* */ switch (textElement.getLineSpacingValue()) { case SINGLE : { lineSpacing = 1f; break; } case ONE_AND_HALF : { lineSpacing = 1.5f; break; } case DOUBLE : { lineSpacing = 2f; break; } default : { lineSpacing = 1f; } } maxFontSizeFinder = MaxFontSizeFinder.getInstance(!JRCommonText.MARKUP_NONE.equals(textElement.getMarkup())); formatWidth = width - leftPadding - rightPadding; formatWidth = formatWidth < 0 ? 0 : formatWidth; maxHeight = height + availableStretchHeight - topPadding - bottomPadding; maxHeight = maxHeight < 0 ? 0 : maxHeight; this.canOverflow = canOverflow; this.globalAttributes = styledText.getGlobalAttributes(); boolean saveLineBreakOffsets = JRProperties.getBooleanProperty(propertiesHolder, JRTextElement.PROPERTY_SAVE_LINE_BREAKS, false); measuredState = new TextMeasuredState(saveLineBreakOffsets); measuredState.lastOffset = remainingTextStart; prevMeasuredState = null; } /** * */ public JRMeasuredText measure( JRStyledText styledText, int remainingTextStart, int availableStretchHeight, boolean canOverflow ) { /* */ initialize(styledText, remainingTextStart, availableStretchHeight, canOverflow); AttributedCharacterIterator allParagraphs = styledText.getAwtAttributedString( JRProperties.getBooleanProperty(propertiesHolder, JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT, false) ).getIterator(); 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; while(tkzer.hasMoreTokens() && rendered) { String token = tkzer.nextToken(); if ("\n".equals(token)) { rendered = renderParagraph(allParagraphs, 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(allParagraphs, lastParagraphStart, lastParagraphText); } return measuredState; } /** * */ protected boolean renderParagraph( AttributedCharacterIterator allParagraphs, int lastParagraphStart, String lastParagraphText ) { AttributedCharacterIterator paragraph = null; if (lastParagraphText == null) { paragraph = new AttributedString( " ", new AttributedString( allParagraphs, lastParagraphStart, lastParagraphStart + 1 ).getIterator().getAttributes() ).getIterator(); } else { paragraph = new AttributedString( allParagraphs, lastParagraphStart, lastParagraphStart + lastParagraphText.length() ).getIterator(); } LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, FONT_RENDER_CONTEXT); measuredState.textOffset = lastParagraphStart; boolean rendered = true; boolean renderedLine = false; while (lineMeasurer.getPosition() < paragraph.getEndIndex() && rendered) { rendered = renderNextLine(lineMeasurer, paragraph); 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(allParagraphs, lastParagraphText, lastParagraphStart, renderedLine); } return rendered; } protected void processLastTruncatedRow(AttributedCharacterIterator allParagraphs, String paragraphText, int paragraphOffset, boolean lineTruncated) { if (lineTruncated && isToTruncateAtChar()) { truncateLastLineAtChar(allParagraphs, paragraphText, paragraphOffset); } appendTruncateSuffix(allParagraphs); } protected void truncateLastLineAtChar(AttributedCharacterIterator allParagraphs, String paragraphText, int paragraphOffset) { //truncate the original line at char measuredState = prevMeasuredState.cloneState(); AttributedCharacterIterator lineParagraph = new AttributedString( allParagraphs, measuredState.textOffset, paragraphOffset + paragraphText.length()).getIterator(); LineBreakMeasurer lineMeasurer = new LineBreakMeasurer( lineParagraph, BreakIterator.getCharacterInstance(), FONT_RENDER_CONTEXT); //render again the last line //if the line does not fit now, it will remain empty renderNextLine(lineMeasurer, lineParagraph); } protected void appendTruncateSuffix(AttributedCharacterIterator allParagraphs) { 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 StringBuffer lineText = new StringBuffer(); allParagraphs.setIndex(lineStart); while (allParagraphs.getIndex() < measuredState.textOffset && allParagraphs.current() != '\n') { lineText.append(allParagraphs.current()); allParagraphs.next(); } int linePosition = allParagraphs.getIndex() - lineStart; //iterate to the beginning of the line boolean done = false; do { measuredState = prevMeasuredState.cloneState(); String text = lineText.substring(0, linePosition) + truncateSuffx; AttributedString attributedText = new AttributedString(text); //set original attributes for the text part AttributedCharacterIterator lineAttributes = new AttributedString( allParagraphs, measuredState.textOffset, measuredState.textOffset + linePosition).getIterator(); setAttributes(attributedText, lineAttributes, 0); //set global attributes for the suffix part setAttributes(attributedText, globalAttributes, text.length() - truncateSuffx.length(), text.length()); AttributedCharacterIterator lineParagraph = attributedText.getIterator(); BreakIterator breakIterator = isToTruncateAtChar() ? BreakIterator.getCharacterInstance() : BreakIterator.getLineInstance(); LineBreakMeasurer lineMeasurer = new LineBreakMeasurer( lineParagraph, breakIterator, FONT_RENDER_CONTEXT); if (renderNextLine(lineMeasurer, lineParagraph)) { int lastPos = lineMeasurer.getPosition(); //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 && allParagraphs.setIndex(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() { return JRProperties.getBooleanProperty(propertiesHolder, JRTextElement.PROPERTY_TRUNCATE_AT_CHAR, false); } protected String getTruncateSuffix() { String truncateSuffx = JRProperties.getProperty(propertiesHolder, JRTextElement.PROPERTY_TRUNCATE_SUFFIX); if (truncateSuffx != null) { truncateSuffx = truncateSuffx.trim(); if (truncateSuffx.length() == 0) { truncateSuffx = null; } } return truncateSuffx; } protected boolean renderNextLine(LineBreakMeasurer lineMeasurer, AttributedCharacterIterator paragraph) { int lineStartPosition = lineMeasurer.getPosition(); TextLayout layout = lineMeasurer.nextLayout(formatWidth); float newTextHeight = measuredState.textHeight + layout.getLeading() + lineSpacing * layout.getAscent(); boolean fits = newTextHeight + layout.getDescent() <= maxHeight; if (fits) { prevMeasuredState = measuredState.cloneState(); measuredState.isLeftToRight = measuredState.isLeftToRight && layout.isLeftToRight(); measuredState.textHeight = newTextHeight; measuredState.lines++; measuredState.fontSizeSum += maxFontSizeFinder.findMaxFontSize( new AttributedString( paragraph, lineStartPosition, lineStartPosition + layout.getCharacterCount() ).getIterator(), textElement.getFontSize() ); if (measuredState.lines == 1) { measuredState.firstLineLeading = measuredState.textHeight; measuredState.firstLineMaxFontSize = measuredState.fontSizeSum; } // here is the Y offset where we would draw the line //lastDrawPosY = drawPosY; // measuredState.textHeight += layout.getDescent(); measuredState.textOffset += lineMeasurer.getPosition() - lineStartPosition; if (lineMeasurer.getPosition() < paragraph.getEndIndex()) { //if not the last line in a paragraph, save the line break position measuredState.addLineBreak(); } } return fits; } protected JRPropertiesHolder getTextPropertiesHolder() { return propertiesHolder; } protected void setAttributes( AttributedString string, AttributedCharacterIterator attributes, int stringOffset) { for (char c = attributes.first(); c != CharacterIterator.DONE; c = attributes.next()) { for (Iterator it = attributes.getAttributes().entrySet().iterator(); it.hasNext();) { Map.Entry attributeEntry = (Map.Entry) it.next(); AttributedCharacterIterator.Attribute attribute = (Attribute) attributeEntry.getKey(); if (attributes.getRunStart(attribute) == attributes.getIndex()) { Object attributeValue = attributeEntry.getValue(); string.addAttribute(attribute, attributeValue, attributes.getIndex() + stringOffset, attributes.getRunLimit(attribute) + stringOffset); } } } } protected void setAttributes( AttributedString string, Map attributes, int startIndex, int endIndex) { for (Iterator it = attributes.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); AttributedCharacterIterator.Attribute attribute = (Attribute) entry.getKey(); Object attributeValue = entry.getValue(); string.addAttribute(attribute, attributeValue, startIndex, endIndex); } } }