net.sf.jasperreports.engine.export.AbstractTextRenderer Maven / Gradle / Ivy
The newest version!
/*
* JasperReports - Free Java Reporting Library.
* Copyright (C) 2001 - 2023 Cloud Software Group, 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.export;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextLayout;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import net.sf.jasperreports.engine.JRParagraph;
import net.sf.jasperreports.engine.JRPrintText;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.TabStop;
import net.sf.jasperreports.engine.type.HorizontalTextAlignEnum;
import net.sf.jasperreports.engine.util.JRStringUtil;
import net.sf.jasperreports.engine.util.JRStyledText;
import net.sf.jasperreports.engine.util.JRStyledTextUtil;
import net.sf.jasperreports.engine.util.JRTextAttribute;
import net.sf.jasperreports.engine.util.ParagraphUtil;
import net.sf.jasperreports.engine.util.StyledTextListWriter;
import net.sf.jasperreports.engine.util.StyledTextWriteContext;
/**
* @author Teodor Danciu ([email protected])
*/
public abstract class AbstractTextRenderer
{
public static final FontRenderContext LINE_BREAK_FONT_RENDER_CONTEXT = new FontRenderContext(null, true, true);
protected final JasperReportsContext jasperReportsContext;
protected final JRPropertiesUtil propUtil;
protected JRPrintText text;
protected JRStyledText styledText;
protected String allText;
protected int x;
protected int y;
protected int width;
protected int height;
protected int topPadding;
protected int leftPadding;
protected int bottomPadding;
protected int rightPadding;
//protected float formatWidth;
protected float verticalAlignOffset;
protected float drawPosY;
protected float drawPosX;
protected float lineHeight;
protected boolean isMaxHeightReached;
protected boolean isFirstParagraph;
protected boolean isLastParagraph;
protected List segments;
protected int segmentIndex;
protected boolean indentFirstLine;
protected boolean justifyLastLine;
protected int htmlListIndent;
protected String bulletText;
protected AttributedString bulletChunk;
/**
*
*
private MaxFontSizeFinder maxFontSizeFinder;
/**
*
*/
private final boolean isMinimizePrinterJobSize;
protected final boolean ignoreMissingFont;
private final boolean defaultIndentFirstLine;
private final boolean defaultJustifyLastLine;
/**
*
*/
public AbstractTextRenderer(
JasperReportsContext jasperReportsContext,
boolean isMinimizePrinterJobSize,
boolean ignoreMissingFont,
boolean defaultIndentFirstLine,
boolean defaultJustifyLastLine
)
{
this.jasperReportsContext = jasperReportsContext;
this.propUtil = JRPropertiesUtil.getInstance(jasperReportsContext);
this.isMinimizePrinterJobSize = isMinimizePrinterJobSize;
this.ignoreMissingFont = ignoreMissingFont;
this.defaultIndentFirstLine = defaultIndentFirstLine;
this.defaultJustifyLastLine = defaultJustifyLastLine;
}
/**
*
*/
public int getX()
{
return x;
}
/**
*
*/
public int getY()
{
return y;
}
/**
*
*/
public int getWidth()
{
return width;
}
/**
*
*/
public int getHeight()
{
return height;
}
/**
*
*/
public int getTopPadding()
{
return topPadding;
}
/**
*
*/
public int getLeftPadding()
{
return leftPadding;
}
/**
*
*/
public int getBottomPadding()
{
return bottomPadding;
}
/**
*
*/
public int getRightPadding()
{
return rightPadding;
}
/**
*
*/
public JRStyledText getStyledText()
{
return styledText;
}
/**
*
*/
public String getPlainText()
{
return allText;
}
/**
*
*/
public void initialize(JRPrintText text, JRStyledText styledText, int offsetX, int offsetY)
{
this.styledText = styledText;
allText = styledText.getText();
x = text.getX() + offsetX;
y = text.getY() + offsetY;
width = text.getWidth();
height = text.getHeight();
topPadding = text.getLineBox().getTopPadding();
leftPadding = text.getLineBox().getLeftPadding();
bottomPadding = text.getLineBox().getBottomPadding();
rightPadding = text.getLineBox().getRightPadding();
switch (text.getRotationValue())
{
case LEFT :
{
y = text.getY() + offsetY + text.getHeight();
width = text.getHeight();
height = text.getWidth();
int tmpPadding = topPadding;
topPadding = leftPadding;
leftPadding = bottomPadding;
bottomPadding = rightPadding;
rightPadding = tmpPadding;
break;
}
case RIGHT :
{
x = text.getX() + offsetX + text.getWidth();
width = text.getHeight();
height = text.getWidth();
int tmpPadding = topPadding;
topPadding = rightPadding;
rightPadding = bottomPadding;
bottomPadding = leftPadding;
leftPadding = tmpPadding;
break;
}
case UPSIDE_DOWN :
{
int tmpPadding = topPadding;
x = text.getX() + offsetX + text.getWidth();
y = text.getY() + offsetY + text.getHeight();
topPadding = bottomPadding;
bottomPadding = tmpPadding;
tmpPadding = leftPadding;
leftPadding = rightPadding;
rightPadding = tmpPadding;
break;
}
case NONE :
default :
{
}
}
this.text = text;
verticalAlignOffset = 0f;
switch (text.getVerticalTextAlign())
{
case BOTTOM :
{
verticalAlignOffset = height - topPadding - bottomPadding - text.getTextHeight();
break;
}
case MIDDLE :
{
verticalAlignOffset = (height - topPadding - bottomPadding - text.getTextHeight()) / 2f;
break;
}
case TOP :
case JUSTIFIED :
default :
{
verticalAlignOffset = 0f;
}
}
indentFirstLine = defaultIndentFirstLine;
if (text.getPropertiesMap().containsProperty(JRPrintText.PROPERTY_AWT_INDENT_FIRST_LINE))
{
indentFirstLine = propUtil.getBooleanProperty(text, JRPrintText.PROPERTY_AWT_INDENT_FIRST_LINE, defaultIndentFirstLine);
}
justifyLastLine = defaultJustifyLastLine;
if (text.getPropertiesMap().containsProperty(JRPrintText.PROPERTY_AWT_JUSTIFY_LAST_LINE))
{
justifyLastLine = propUtil.getBooleanProperty(text, JRPrintText.PROPERTY_AWT_JUSTIFY_LAST_LINE, defaultJustifyLastLine);
}
// formatWidth = width - leftPadding - rightPadding;
// formatWidth = formatWidth < 0 ? 0 : formatWidth;
drawPosY = 0;
drawPosX = 0;
isMaxHeightReached = false;
//maxFontSizeFinder = MaxFontSizeFinder.getInstance(!JRCommonText.MARKUP_NONE.equals(text.getMarkup()));
}
/**
*
*/
public void render()
{
StyledTextWriteContext context = new StyledTextWriteContext();
StyledTextListWriter listWriter = getListWriter();
AttributedCharacterIterator allParagraphs = getAttributedString().getIterator();
isFirstParagraph = true;
isLastParagraph = false;
int runLimit = 0; //first value does not matter; will be assigned a proper value in the while statement below
int runStart = 0;
while (!isMaxHeightReached && runStart < allParagraphs.getEndIndex() && (runLimit = allParagraphs.getRunLimit(JRTextAttribute.HTML_LIST_ATTRIBUTES)) <= allParagraphs.getEndIndex())
{
Map attributes = allParagraphs.getAttributes();
context.next(attributes);
context.writeLists(listWriter);
prepareBullet(context, attributes);
int paragraphStart = 0;
boolean lastTokenWasNewline = false;
String runText = allText.substring(runStart, runLimit);
StringTokenizer tkzer = new StringTokenizer(runText, "\n", true);
// text is split into paragraphs, using the newline character as delimiter
while(tkzer.hasMoreTokens() && !isMaxHeightReached)
{
String paragraphText = tkzer.nextToken();
isLastParagraph = !tkzer.hasMoreTokens();
if ("\n".equals(paragraphText))
{
if (lastTokenWasNewline) // the previous newline becomes itself a paragraph when followed by another newline
{
renderParagraph(
allParagraphs,
runStart + paragraphStart - 1,
null // null paragraphText is the way to render newlines
);
}
if (
paragraphStart == 0 // this newline is the first character in the first paragraph
|| paragraphStart == allParagraphs.getEndIndex() - 1 // this newline is the last character in the last paragraph
)
{
renderParagraph(
allParagraphs,
runStart + paragraphStart,
null // null paragraphText is the way to render newlines
);
}
lastTokenWasNewline = true;
}
else
{
renderParagraph(
allParagraphs,
runStart + paragraphStart,
paragraphText
);
lastTokenWasNewline = false;
}
paragraphStart += paragraphText.length();
isFirstParagraph = false;
}
runStart = runLimit;
allParagraphs.setIndex(runStart);
}
context.next(null);
context.writeLists(listWriter);
}
protected StyledTextListWriter getListWriter()
{
return null;
}
/**
*
*/
protected void renderParagraph(
AttributedCharacterIterator allParagraphs,
int paragraphStart,
String paragraphText
)
{
AttributedCharacterIterator paragraph = null;
if (paragraphText == null)
{
paragraphText = " ";
paragraph =
new AttributedString(
paragraphText,
new AttributedString(
allParagraphs,
paragraphStart,
paragraphStart + paragraphText.length()
).getIterator().getAttributes()
).getIterator();
}
else
{
paragraph =
new AttributedString(
allParagraphs,
paragraphStart,
paragraphStart + paragraphText.length()
).getIterator();
}
List tabIndexes = JRStringUtil.getTabIndexes(paragraphText);
int currentTab = 0;
int lines = 0;
float endX = 0;
TabStop nextTabStop = null;
boolean requireNextWord = false;
LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, getFontRenderContext());//grx.getFontRenderContext()
// the paragraph is rendered one line at a time
while (lineMeasurer.getPosition() < paragraph.getEndIndex() && !isMaxHeightReached)
{
boolean lineComplete = false;
float maxAscent = 0;
float maxDescent = 0;
float maxLeading = 0;
// each line is split into segments, using the tab character as delimiter
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 || currentTab >= tabIndexes.size() ? paragraph.getEndIndex() : tabIndexes.get(currentTab) + 1); // this +1 here means
// that each segment would contain its terminal tab character, except the last segment which ends where the paragraph ends;
// the tab character at the end of the segment, although it is not actually rendered, it still causes the layout.getAdvance() to equal layout.getVisibleAdvance()
// meaning that any white spaces before the tab are not considered trailing spaces, so they contribute to segment width and thus impact segment text alignment
int firstLineIndent = lineMeasurer.getPosition() == 0 ? text.getParagraph().getFirstLineIndent() : 0;
if (
firstLineIndent != 0
&& (isFirstParagraph && !indentFirstLine)
)
{
firstLineIndent = 0;
}
float startX = htmlListIndent + firstLineIndent + leftPadding;
endX = width - text.getParagraph().getRightIndent() - rightPadding;
endX = endX < startX ? startX : endX;
//formatWidth = endX - startX;
//formatWidth = endX;
int startIndex = lineMeasurer.getPosition();
float rightX = 0;
if (segments.size() == 0)
{
rightX = startX;
//nextTabStop = nextTabStop;
}
else
{
rightX = oldSegment.rightX;
nextTabStop = ParagraphUtil.getNextTabStop(text.getParagraph(), endX, rightX);
}
//float availableWidth = formatWidth - ParagraphUtil.getSegmentOffset(nextTabStop, rightX); // nextTabStop can be null here; and that's OK
float availableWidth = endX - text.getParagraph().getLeftIndent() - ParagraphUtil.getSegmentOffset(nextTabStop, rightX); // nextTabStop can be null here; and that's OK
// creating a text layout object for each tab segment
TextLayout layout =
lineMeasurer.nextLayout(
availableWidth,
tabIndexOrEndIndex,
requireNextWord
);
if (layout != null)
{
AttributedString tmpText =
new AttributedString(
paragraph,
startIndex,
startIndex + layout.getCharacterCount()
);
if (isMinimizePrinterJobSize)
{
//eugene fix - start
layout = new TextLayout(tmpText.getIterator(), getFontRenderContext());
//eugene fix - end
}
if (
text.getHorizontalTextAlign() == HorizontalTextAlignEnum.JUSTIFIED
&& (lineMeasurer.getPosition() < paragraph.getEndIndex() || (isLastParagraph && justifyLastLine))
)
{
layout = layout.getJustifiedLayout(availableWidth);
}
maxAscent = Math.max(maxAscent, layout.getAscent());
maxDescent = Math.max(maxDescent, layout.getDescent());
maxLeading = Math.max(maxLeading, layout.getLeading());
//creating the current segment
crtSegment = new TabSegment();
crtSegment.layout = layout;
crtSegment.as = tmpText;
crtSegment.text = paragraphText.substring(startIndex, startIndex + layout.getCharacterCount());
crtSegment.isLastLine = lineMeasurer.getPosition() == paragraph.getEndIndex();
// using layout.getVisibleAdvance() here means trailing white space characters at the end of the line do not contribute to line width,
// which is important when aligning the line of text, to match how text alignment works in PDF, DOCX and other formats;
// unlike entire lines of text which might end up with white space characters and are thus considered trailing spaces,
// segments separated by tab character contain the tab character as last character and any white space character preceding the tab are not
// considered trailing spaces; they contribute to the segment width and impact segment alignment because layout.getAvance() equals layout.getVisibleAdvance()
// in their case
float advance = layout.getVisibleAdvance();
//float advance = layout.getAdvance();
float leftX = ParagraphUtil.getLeftX(nextTabStop, advance); // nextTabStop can be null here; and that's OK
if (rightX > leftX)
{
crtSegment.leftX = rightX;
crtSegment.rightX = rightX + advance;
}
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(nextTabStop, advance); // nextTabStop can be null here; and that's OK
}
segments.add(crtSegment);
}
requireNextWord = true;
if (lineMeasurer.getPosition() == tabIndexOrEndIndex)
{
// the segment limit was a tab; going to the next tab
currentTab++;
}
if (lineMeasurer.getPosition() == paragraph.getEndIndex())
{
// the segment limit was the paragraph end; line completed and next line should start at normal zero x offset
lineComplete = true;
nextTabStop = null;
}
else
{
// there is paragraph text remaining
if (lineMeasurer.getPosition() == tabIndexOrEndIndex)
{
// the segment limit was a tab
if (crtSegment.rightX >= ParagraphUtil.getLastTabStop(text.getParagraph(), 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
nextTabStop = ParagraphUtil.getFirstTabStop(text.getParagraph(), endX);
}
// else
// {
// //nothing; this leaves lineComplete=false
// }
}
else
{
// the segment did not fit entirely
lineComplete = true;
if (layout == null)
{
// nothing fitted; next line should start at first tab stop indent
if (nextTabStop.getPosition() == ParagraphUtil.getFirstTabStop(text.getParagraph(), endX).getPosition())//FIXMETAB check based on segments.size()
{
// at second attempt we give up to avoid infinite loop
nextTabStop = null;
requireNextWord = false;
//provide dummy maxFontSize because it is used for the line height of this empty line when attempting drawing below
AttributedString tmpText =
new AttributedString(
paragraph,
startIndex,
startIndex + 1
);
LineBreakMeasurer lbm = new LineBreakMeasurer(tmpText.getIterator(), getFontRenderContext());
TextLayout tlyt = lbm.nextLayout(100);
maxAscent = tlyt.getAscent();
maxDescent = tlyt.getDescent();
maxLeading = tlyt.getLeading();
}
else
{
nextTabStop = ParagraphUtil.getFirstTabStop(text.getParagraph(), endX);
}
}
else
{
// something fitted
nextTabStop = null;
requireNextWord = false;
}
}
}
oldSegment = crtSegment;
}
lineHeight = getLineHeight(paragraphStart == 0 && lines == 0, text.getParagraph(), maxLeading, maxAscent);// + maxDescent;
if (paragraphStart == 0 && lines == 0)
//if (lines == 0) //FIXMEPARA
{
lineHeight += text.getParagraph().getSpacingBefore();
}
if (drawPosY + lineHeight <= text.getTextHeight())
{
lines++;
drawPosY += lineHeight;
float lastRightX = (segments == null || segments.size() == 0 ? 0 : segments.get(segments.size() - 1).rightX);
// now iterate through segments and draw their layouts
for (segmentIndex = 0; segmentIndex < segments.size(); segmentIndex++)
{
TabSegment segment = segments.get(segmentIndex);
TextLayout layout = segment.layout;
switch (text.getHorizontalTextAlign())
{
case JUSTIFIED :
{
if (layout.isLeftToRight())
{
drawPosX = text.getParagraph().getLeftIndent() + segment.leftX;
}
else
{
drawPosX = (endX - lastRightX + segment.leftX);
}
break;
}
case RIGHT ://FIXMETAB RTL writings
{
drawPosX = (endX - lastRightX + segment.leftX);
break;
}
case CENTER :
{
drawPosX = ((endX - lastRightX) / 2) + segment.leftX;
break;
}
case LEFT :
default :
{
drawPosX = text.getParagraph().getLeftIndent() + segment.leftX;
}
}
/* */
draw();
bulletText = null;
bulletChunk = null;
}
drawPosY += maxDescent;
// if (lineMeasurer.getPosition() == paragraph.getEndIndex()) //FIXMEPARA
// {
// drawPosY += text.getParagraph().getSpacingAfter();
// }
}
else
{
isMaxHeightReached = true;
}
}
}
private void prepareBullet(
StyledTextWriteContext context,
Map attributes
)
{
htmlListIndent = context.getDepth() * 50; //FIXMEBULLET always 50?
bulletText = JRStyledTextUtil.getBulletText(context);
if (bulletText == null)
{
bulletChunk = null;
}
else
{
bulletChunk =
new AttributedString(
bulletText,
attributes
);
}
}
/**
*
*/
protected AttributedString getAttributedString()
{
return styledText.getAwtAttributedString(jasperReportsContext, ignoreMissingFont);
}
/**
*
*/
public abstract void draw();
/**
*
*/
public static float getLineHeight(boolean isFirstLine, JRParagraph paragraph, float maxLeading, float maxAscent)
{
float lineHeight = 0;
switch(paragraph.getLineSpacing())
{
case ONE_AND_HALF:
{
if (isFirstLine)
{
lineHeight = maxLeading + 1f * maxAscent;
}
else
{
lineHeight = maxLeading + 1.5f * maxAscent;
}
break;
}
case DOUBLE:
{
if (isFirstLine)
{
lineHeight = maxLeading + 1f * maxAscent;
}
else
{
lineHeight = maxLeading + 2f * maxAscent;
}
break;
}
case PROPORTIONAL:
{
if (isFirstLine)
{
lineHeight = maxLeading + 1f * maxAscent;
}
else
{
lineHeight = maxLeading + paragraph.getLineSpacingSize() * maxAscent;
}
break;
}
case AT_LEAST:
{
if (isFirstLine)
{
lineHeight = maxLeading + 1f * maxAscent;
}
else
{
lineHeight = Math.max(maxLeading + 1f * maxAscent, paragraph.getLineSpacingSize());
}
break;
}
case FIXED:
{
if (isFirstLine)
{
lineHeight = maxLeading + 1f * maxAscent;
}
else
{
lineHeight = paragraph.getLineSpacingSize();
}
break;
}
case SINGLE:
default :
{
lineHeight = maxLeading + 1f * maxAscent;
break;
}
}
return lineHeight;
}
/**
*
*
public static float getLineHeight(JRParagraph paragraph, float lineSpacingFactor, int maxFontSize)
{
float lineHeight = 0;
switch(paragraph.getLineSpacing())
{
case SINGLE:
case ONE_AND_HALF:
case DOUBLE:
case PROPORTIONAL:
{
lineHeight = lineSpacingFactor * maxFontSize;
break;
}
case AT_LEAST:
{
lineHeight = Math.max(lineSpacingFactor * maxFontSize, paragraph.getLineSpacingSize());
break;
}
case FIXED:
{
lineHeight = paragraph.getLineSpacingSize();
break;
}
default :
{
throw new JRRuntimeException("Invalid line space type: " + paragraph.getLineSpacing());
}
}
return lineHeight;
}
/**
*
*/
public FontRenderContext getFontRenderContext()
{
return LINE_BREAK_FONT_RENDER_CONTEXT;
}
/**
*
*/
public static class TabSegment
{
public TextLayout layout;
public AttributedString as;//FIXMETAB rename these
public String text;
public float leftX;
public float rightX;
public boolean isLastLine;
}
}