1 /*
2 * JasperReports - Free Java Reporting Library.
3 * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
4 * http://www.jaspersoft.com
5 *
6 * Unless you have purchased a commercial license agreement from Jaspersoft,
7 * the following license terms apply:
8 *
9 * This program is part of JasperReports.
10 *
11 * JasperReports is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * JasperReports is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU Lesser General Public License for more details.
20 *
21 * You should have received a copy of the GNU Lesser General Public License
22 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
23 */
24 package net.sf.jasperreports.engine.fill;
25
26 import java.awt.font.FontRenderContext;
27 import java.text.BreakIterator;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.StringTokenizer;
31
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34
35 import net.sf.jasperreports.annotations.properties.Property;
36 import net.sf.jasperreports.annotations.properties.PropertyScope;
37 import net.sf.jasperreports.engine.JRCommonText;
38 import net.sf.jasperreports.engine.JRParagraph;
39 import net.sf.jasperreports.engine.JRPrintText;
40 import net.sf.jasperreports.engine.JRPropertiesHolder;
41 import net.sf.jasperreports.engine.JRPropertiesUtil;
42 import net.sf.jasperreports.engine.JRRuntimeException;
43 import net.sf.jasperreports.engine.JRTextElement;
44 import net.sf.jasperreports.engine.JasperReportsContext;
45 import net.sf.jasperreports.engine.TabStop;
46 import net.sf.jasperreports.engine.export.AbstractTextRenderer;
47 import net.sf.jasperreports.engine.export.AwtTextRenderer;
48 import net.sf.jasperreports.engine.util.DelegatePropertiesHolder;
49 import net.sf.jasperreports.engine.util.JRStringUtil;
50 import net.sf.jasperreports.engine.util.JRStyledText;
51 import net.sf.jasperreports.engine.util.ParagraphUtil;
52 import net.sf.jasperreports.properties.PropertyConstants;
53
54
55 /**
56 * Default text measurer implementation.
57 * <h3>Text Measuring</h3>
58 * When a the contents of a text element do not fit
59 * into the area given by the element width and height, the engine will either truncate the
60 * text contents or, in the case of a text field that is allowed to stretch, increase the height of
61 * the element to accommodate the contents. To do so, the JasperReports engine needs to
62 * measure the text and calculate how much of it fits in the element area, or how much the
63 * element needs to stretch in order to fit the entire text.
64 * <p/>
65 * JasperReports does this, by default, by using standard Java AWT classes to layout and
66 * measure the text with its style information given by the text font and by other style
67 * attributes. This ensures that the result of the text layout calculation is exact according to
68 * the JasperReports principle of pixel perfectness.
69 * <p/>
70 * However, this comes at a price - the AWT text layout calls contribute to the overall
71 * report fill performance. For this reason and possibly others, it might be desired in some
72 * cases to implement a different text measuring mechanism. JasperReports allows users to
73 * employ custom text measurer implementations by setting a value for the
74 * {@link net.sf.jasperreports.engine.util.JRTextMeasurerUtil#PROPERTY_TEXT_MEASURER_FACTORY net.sf.jasperreports.text.measurer.factory} property.
75 * The property can be set globally (in <code>jasperreports.properties</code> or via the
76 * {@link net.sf.jasperreports.engine.JRPropertiesUtil#setProperty(String, String)} method), at
77 * report level or at element level (as an element property). The property value should be
78 * either the name of a class that implements the
79 * {@link net.sf.jasperreports.engine.util.JRTextMeasurerFactory} interface, or an
80 * alias defined for such a text measurer factory class. To define an alias, one needs to
81 * define a property having
82 * <code>net.sf.jasperreports.text.measurer.factory.<alias></code> as key and the factory
83 * class name as value. Take the following examples of text measurer factory properties:
84 * <ul>
85 * <li>in jasperreports.properties set a custom default text measurer factory:
86 * <br/>
87 * <code>net.sf.jasperreports.text.measurer.factory=com.jasperreports.MyTextMeasurerFactory</code></li>
88 * <li>define an alias for a different text measurer factory:
89 * <br/>
90 * <code>net.sf.jasperreports.text.measurer.factory.fast=com.jasperreports.MyFastTextMeasurerFactory</code></li>
91 * <li>in a JRXML, use the fast text measurer for a static text:</li>
92 * </ul>
93 * <pre>
94 * <staticText>
95 * <reportElement ...>
96 * <property name="net.sf.jasperreports.text.measurer.factory" value="fast"/>
97 * </reportElement>
98 * <text>...</text>
99 * </staticText>
100 * </pre>
101 * The default text measurer factory used by JasperReports is
102 * {@link net.sf.jasperreports.engine.fill.TextMeasurerFactory}; the factory is also
103 * registered under an alias named <code>default</code>.
104 * <h3>Text Truncation</h3>
105 * The built-in text measurer supports a series of text truncation customizations. As a
106 * reminder, text truncation occurs when a the contents of a static text element or of a text
107 * field that is not set as stretchable do not fit the area reserved for the element in the report
108 * template. Note that text truncation only refers to the truncation of the last line of a text
109 * element, and not to the word wrapping of a text element that spans across multiple lines.
110 * <p/>
111 * The default behavior is to use the standard AWT line break logic (as returned by the
112 * <code>java.text.BreakIterator.getLineInstance()</code> method) to determine where to
113 * truncate the text. This means that the last line of text will be truncated after the last word
114 * that fits on the line, or after the last character when the first word on the line does not
115 * entirely fit.
116 * <p/>
117 * This behavior can be changed by forcing the text to always get truncated at the last
118 * character that fits the element area, and by appending one or more characters to the
119 * truncated text to notify a report reader that the text has been truncated.
120 * To force the text to be wrapped at the last character, the
121 * {@link net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_AT_CHAR net.sf.jasperreports.text.truncate.at.char}
122 * property needs to be set to true
123 * globally, at report level or at text element level. The levels at which the property can be
124 * set are listed in a decreasing order of precedence, therefore an element level property
125 * overrides the report level property, which in its turn overrides the global property. The
126 * property can also be set to false at report or element level to override the true value of the
127 * property set at a higher level.
128 * <p/>
129 * To append a suffix to the truncated text, one needs to set the desired suffix as the value
130 * of the {@link net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_SUFFIX net.sf.jasperreports.text.truncate.suffix}
131 * property globally, at report level or at element level. For instance, to use a Unicode
132 * horizontal ellipsis character (code point U+2026) as text truncation suffix, one would set
133 * the property globally or at report level as following:
134 * <ul>
135 * <li>globally in <code>jasperreports.properties</code>:
136 * <br/>
137 * <code>net.sf.jasperreports.text.truncate.suffix=\u2026</code></li>
138 * <li>at report level:</li>
139 * </ul>
140 * <pre>
141 * <jasperReport ...>
142 * <property name="net.sf.jasperreports.text.truncate.suffix" value="&#x2026;"/>
143 * ...
144 * </jasperReport>
145 * </pre>
146 * Note that in the JRXML the ellipsis character was introduced via an XML numerical
147 * character entity. If the JRXML file uses a Unicode XML encoding, the Unicode
148 * character can also be directly written in the JRXML.
149 * <p/>
150 * When using a truncation suffix, the truncate at character property is taken into
151 * consideration in order to determine where to append the truncation suffix. If the
152 * truncation at character property is set to false, the suffix is appended after the last word
153 * that fits; if the property is set to true, the suffix is appended after the last text character
154 * that fits.
155 * <p/>
156 * When used for a text element that produces styled text, the truncation suffix is placed
157 * outside the styled text, that is, the truncation suffix will be displayed using the style
158 * defined at element level.
159 * <p/>
160 * Text truncation is desirable when producing reports for that are displayed on a screen or
161 * printed on paper - in such scenarios the layout of the report is important. On the other
162 * hand, some JasperReports exporters, such as the Excel or CSV ones, produce output
163 * which in many cases is intended as data-centric. In such cases, it could be useful not to
164 * truncate any text generated by the report, even if some texts would not fit when rendered
165 * on a layout-sensitive media.
166 * <p/>
167 * To inhibit the unconditional truncation of report texts, one would need to set the
168 * {@link net.sf.jasperreports.engine.JRTextElement#PROPERTY_PRINT_KEEP_FULL_TEXT net.sf.jasperreports.print.keep.full.text}
169 * property to true globally, at report level or at text element level. When the
170 * property is set to true, the text is not truncated at fill time and the generated
171 * report preserves the full text as produced by the text element.
172 * <p/>
173 * Visual report exporters (such as the exporters used for PDF, HTML, RTF, printing or the
174 * Java report viewer) would still truncate the rendered text, but the Excel and CSV data-centric
175 * exporters would use the full text. Note that preserving the full text does not affect
176 * the size of the text element, therefore the Excel exporter would display the full text
177 * inside a cell that has the size of the truncated text.
178 *
179 * @see net.sf.jasperreports.engine.JRTextElement#PROPERTY_PRINT_KEEP_FULL_TEXT
180 * @see net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_AT_CHAR
181 * @see net.sf.jasperreports.engine.JRTextElement#PROPERTY_TRUNCATE_SUFFIX
182 * @see net.sf.jasperreports.engine.fill.TextMeasurerFactory
183 * @see net.sf.jasperreports.engine.util.JRTextMeasurerUtil#PROPERTY_TEXT_MEASURER_FACTORY
184 * @author Teodor Danciu (teodord@users.sourceforge.net)
185 */
186 public class TextMeasurer implements JRTextMeasurer
187 {
188 private static final Log log = LogFactory.getLog(TextMeasurer.class);
189
190 //FIXME remove this after measureSimpleText() is proven to be stable
191 @Property(
192 category = PropertyConstants.CATEGORY_FILL,
193 defaultValue = PropertyConstants.BOOLEAN_TRUE,
194 scopes = {PropertyScope.CONTEXT},
195 sinceVersion = PropertyConstants.VERSION_4_6_0,
196 valueType = Boolean.class
197 )
198 public static final String PROPERTY_MEASURE_SIMPLE_TEXTS = JRPropertiesUtil.PROPERTY_PREFIX + "measure.simple.text";
199
200 protected JasperReportsContext jasperReportsContext;
201 protected JRCommonText textElement;
202 private JRPropertiesHolder propertiesHolder;
203 private DynamicPropertiesHolder dynamicPropertiesHolder;
204
205 private SimpleTextLineWrapper simpleLineWrapper;
206 private ComplexTextLineWrapper complexLineWrapper;
207
208 protected int width;
209 private int height;
210 private int topPadding;
211 protected int leftPadding;
212 private int bottomPadding;
213 protected int rightPadding;
214 private JRParagraph jrParagraph;
215 private boolean isFirstParagraph;
216
217 private float formatWidth;
218 protected int maxHeight;
219 private boolean indentFirstLine;
220 private boolean canOverflow;
221
222 private boolean hasDynamicIgnoreMissingFontProp;
223 private boolean defaultIgnoreMissingFont;
224 private boolean ignoreMissingFont;
225
226 private boolean hasDynamicSaveLineBreakOffsetsProp;
227 private boolean defaultSaveLineBreakOffsets;
228
229 protected TextMeasuredState measuredState;
230 protected TextMeasuredState prevMeasuredState;
231
232 protected static class TextMeasuredState implements JRMeasuredText, Cloneable
233 {
234 private final boolean saveLineBreakOffsets;
235
236 protected int textOffset;
237 protected int lines;
238 protected float fontSizeSum;
239 protected float firstLineMaxFontSize;
240 protected int paragraphStartLine;
241 protected float textWidth;
242 protected float textHeight;
243 protected float firstLineLeading;
244 protected boolean isLeftToRight = true;
245 protected boolean isParagraphCut;
246 protected String textSuffix;
247 protected boolean isMeasured = true;
248
249 protected int lastOffset;
250 protected ArrayList<Integer> lineBreakOffsets;
251
252 public TextMeasuredState(boolean saveLineBreakOffsets)
253 {
254 this.saveLineBreakOffsets = saveLineBreakOffsets;
255 }
256
257 @Override
258 public boolean isLeftToRight()
259 {
260 return isLeftToRight;
261 }
262
263 @Override
264 public int getTextOffset()
265 {
266 return textOffset;
267 }
268
269 @Override
270 public float getTextWidth()
271 {
272 return textWidth;
273 }
274
275 @Override
276 public float getTextHeight()
277 {
278 return textHeight;
279 }
280
281 @Override
282 public float getLineSpacingFactor()
283 {
284 if (isMeasured && lines > 0 && fontSizeSum > 0)
285 {
286 return textHeight / fontSizeSum;
287 }
288 return 0;
289 }
290
291 @Override
292 public float getLeadingOffset()
293 {
294 if (isMeasured && lines > 0 && fontSizeSum > 0)
295 {
296 return firstLineLeading - firstLineMaxFontSize * getLineSpacingFactor();
297 }
298 return 0;
299 }
300
301 @Override
302 public boolean isParagraphCut()
303 {
304 return isParagraphCut;
305 }
306
307 @Override
308 public String getTextSuffix()
309 {
310 return textSuffix;
311 }
312
313 public TextMeasuredState cloneState()
314 {
315 try
316 {
317 TextMeasuredState clone = (TextMeasuredState) super.clone();
318
319 //clone the list of offsets
320 //might be a performance problem on very large texts
321 if (lineBreakOffsets != null)
322 {
323 clone.lineBreakOffsets = (ArrayList<Integer>) lineBreakOffsets.clone();
324 }
325
326 return clone;
327 }
328 catch (CloneNotSupportedException e)
329 {
330 //never
331 throw new JRRuntimeException(e);
332 }
333 }
334
335 protected void addLineBreak()
336 {
337 if (saveLineBreakOffsets)
338 {
339 if (lineBreakOffsets == null)
340 {
341 lineBreakOffsets = new ArrayList<Integer>();
342 }
343
344 int breakOffset = textOffset - lastOffset;
345 lineBreakOffsets.add(breakOffset);
346 lastOffset = textOffset;
347 }
348 }
349
350 @Override
351 public short[] getLineBreakOffsets()
352 {
353 if (!saveLineBreakOffsets)
354 {
355 //if no line breaks are to be saved, return null
356 return null;
357 }
358
359 //if the last line break occurred at the truncation position
360 //exclude the last break offset
361 int exclude = lastOffset == textOffset ? 1 : 0;
362 if (lineBreakOffsets == null
363 || lineBreakOffsets.size() <= exclude)
364 {
365 //use the zero length array singleton
366 return JRPrintText.ZERO_LINE_BREAK_OFFSETS;
367 }
368
369 short[] offsets = new short[lineBreakOffsets.size() - exclude];
370 boolean overflow = false;
371 for (int i = 0; i < offsets.length; i++)
372 {
373 int offset = lineBreakOffsets.get(i);
374 if (offset > Short.MAX_VALUE)
375 {
376 if (log.isWarnEnabled())
377 {
378 log.warn("Line break offset value " + offset
379 + " is bigger than the maximum supported value of"
380 + Short.MAX_VALUE
381 + ". Line break offsets will not be saved for this text.");
382 }
383
384 overflow = true;
385 break;
386 }
387 offsets[i] = (short) offset;
388 }
389
390 if (overflow)
391 {
392 //if a line break offset overflow occurred, do not return any
393 //line break offsets
394 return null;
395 }
396
397 return offsets;
398 }
399 }
400
401 /**
402 *
403 */
404 public TextMeasurer(JasperReportsContext jasperReportsContext, JRCommonText textElement)
405 {
406 this.jasperReportsContext = jasperReportsContext;
407 this.textElement = textElement;
408 this.propertiesHolder = textElement instanceof JRPropertiesHolder ? (JRPropertiesHolder) textElement : null;//FIXMENOW all elements are now properties holders, so interfaces might be rearranged
409 if (textElement.getDefaultStyleProvider() instanceof JRPropertiesHolder)
410 {
411 this.propertiesHolder =
412 new DelegatePropertiesHolder(
413 propertiesHolder,
414 (JRPropertiesHolder)textElement.getDefaultStyleProvider()
415 );
416 }
417
418 if (textElement instanceof DynamicPropertiesHolder)
419 {
420 this.dynamicPropertiesHolder = (DynamicPropertiesHolder) textElement;
421
422 // we can check this from the beginning
423 this.hasDynamicIgnoreMissingFontProp = this.dynamicPropertiesHolder.hasDynamicProperty(
424 JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT);
425 this.hasDynamicSaveLineBreakOffsetsProp = this.dynamicPropertiesHolder.hasDynamicProperty(
426 JRTextElement.PROPERTY_SAVE_LINE_BREAKS);
427 }
428
429 // read static property values
430 JRPropertiesUtil propertiesUtil = JRPropertiesUtil.getInstance(jasperReportsContext);
431 defaultIgnoreMissingFont = propertiesUtil.getBooleanProperty(propertiesHolder,
432 JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT, false);
433 defaultSaveLineBreakOffsets = propertiesUtil.getBooleanProperty(propertiesHolder,
434 JRTextElement.PROPERTY_SAVE_LINE_BREAKS, false);
435
436 Context measureContext = new Context();
437 simpleLineWrapper = new SimpleTextLineWrapper();
438 simpleLineWrapper.init(measureContext);
439
440 complexLineWrapper = new ComplexTextLineWrapper();
441 complexLineWrapper.init(measureContext);
442 }
443
444 /**
445 *
446 */
447 protected void initialize(
448 JRStyledText styledText,
449 int remainingTextStart,
450 int availableStretchHeight,
451 boolean indentFirstLine,
452 boolean canOverflow
453 )
454 {
455 width = textElement.getWidth();
456 height = textElement.getHeight();
457
458 topPadding = textElement.getLineBox().getTopPadding();
459 leftPadding = textElement.getLineBox().getLeftPadding();
460 bottomPadding = textElement.getLineBox().getBottomPadding();
461 rightPadding = textElement.getLineBox().getRightPadding();
462
463 jrParagraph = textElement.getParagraph();
464
465 switch (textElement.getRotationValue())
466 {
467 case LEFT :
468 {
469 width = textElement.getHeight();
470 height = textElement.getWidth();
471 int tmpPadding = topPadding;
472 topPadding = leftPadding;
473 leftPadding = bottomPadding;
474 bottomPadding = rightPadding;
475 rightPadding = tmpPadding;
476 break;
477 }
478 case RIGHT :
479 {
480 width = textElement.getHeight();
481 height = textElement.getWidth();
482 int tmpPadding = topPadding;
483 topPadding = rightPadding;
484 rightPadding = bottomPadding;
485 bottomPadding = leftPadding;
486 leftPadding = tmpPadding;
487 break;
488 }
489 case UPSIDE_DOWN :
490 {
491 int tmpPadding = topPadding;
492 topPadding = bottomPadding;
493 bottomPadding = tmpPadding;
494 tmpPadding = leftPadding;
495 leftPadding = rightPadding;
496 rightPadding = tmpPadding;
497 break;
498 }
499 case NONE :
500 default :
501 {
502 }
503 }
504
505 formatWidth = width - leftPadding - rightPadding;
506 formatWidth = formatWidth < 0 ? 0 : formatWidth;
507 maxHeight = height + availableStretchHeight - topPadding - bottomPadding;
508 maxHeight = maxHeight < 0 ? 0 : maxHeight;
509 this.indentFirstLine = indentFirstLine;
510 this.canOverflow = canOverflow;
511
512 // refresh properties if required
513 ignoreMissingFont = defaultIgnoreMissingFont;
514 if (hasDynamicIgnoreMissingFontProp)
515 {
516 String dynamicIgnoreMissingFontProp = dynamicPropertiesHolder.getDynamicProperties().getProperty(
517 JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT);
518 if (dynamicIgnoreMissingFontProp != null)
519 {
520 ignoreMissingFont = JRPropertiesUtil.asBoolean(dynamicIgnoreMissingFontProp);
521 }
522 }
523
524 boolean saveLineBreakOffsets = defaultSaveLineBreakOffsets;
525 if (hasDynamicSaveLineBreakOffsetsProp)
526 {
527 String dynamicSaveLineBreakOffsetsProp = dynamicPropertiesHolder.getDynamicProperties().getProperty(
528 JRTextElement.PROPERTY_SAVE_LINE_BREAKS);
529 if (dynamicSaveLineBreakOffsetsProp != null)
530 {
531 saveLineBreakOffsets = JRPropertiesUtil.asBoolean(dynamicSaveLineBreakOffsetsProp);
532 }
533 }
534
535 measuredState = new TextMeasuredState(saveLineBreakOffsets);
536 measuredState.lastOffset = remainingTextStart;
537 prevMeasuredState = null;
538
539 }
540
541 @Override
542 public JRMeasuredText measure(
543 JRStyledText styledText,
544 int remainingTextStart,
545 int availableStretchHeight,
546 boolean indentFirstLine,
547 boolean canOverflow
548 )
549 {
550 /* */
551 initialize(styledText, remainingTextStart, availableStretchHeight, indentFirstLine, canOverflow);
552
553 TextLineWrapper lineWrapper = simpleLineWrapper;
554 // check if the simple wrapper would handle the text
555 if (!lineWrapper.start(styledText))
556 {
557 lineWrapper = complexLineWrapper;
558 lineWrapper.start(styledText);
559 }
560
561 int tokenPosition = remainingTextStart;
562 int prevParagraphStart = remainingTextStart;
563 String prevParagraphText = null;
564
565 isFirstParagraph = true;
566
567 String remainingText = styledText.getText().substring(remainingTextStart);
568 StringTokenizer tkzer = new StringTokenizer(remainingText, "\n", true);
569
570 boolean rendered = true;
571 // text is split into paragraphs, using the newline character as delimiter
572 while(tkzer.hasMoreTokens() && rendered)
573 {
574 String token = tkzer.nextToken();
575
576 if ("\n".equals(token))
577 {
578 rendered = renderParagraph(lineWrapper, prevParagraphStart, prevParagraphText);
579
580 isFirstParagraph = false;
581 prevParagraphStart = tokenPosition + (tkzer.hasMoreTokens() || tokenPosition == 0 ? 1 : 0);
582 prevParagraphText = null;
583 }
584 else
585 {
586 prevParagraphStart = tokenPosition;
587 prevParagraphText = token;
588 }
589
590 tokenPosition += token.length();
591 }
592
593 if (rendered && prevParagraphStart < remainingTextStart + remainingText.length())
594 {
595 renderParagraph(lineWrapper, prevParagraphStart, prevParagraphText);
596 }
597
598 return measuredState;
599 }
600
601 protected boolean hasParagraphIndents()
602 {
603 Integer firstLineIndent = jrParagraph.getFirstLineIndent();
604 if (firstLineIndent != null && firstLineIndent > 0)
605 {
606 return true;
607 }
608
609 Integer leftIndent = jrParagraph.getLeftIndent();
610 if (leftIndent != null && leftIndent > 0)
611 {
612 return true;
613 }
614
615 Integer rightIndent = jrParagraph.getRightIndent();
616 return rightIndent != null && rightIndent > 0;
617 }
618
619 /**
620 *
621 */
622 protected boolean renderParagraph(
623 TextLineWrapper lineWrapper,
624 int paragraphStart,
625 String paragraphText
626 )
627 {
628 if (paragraphText == null)
629 {
630 lineWrapper.startEmptyParagraph(paragraphStart);
631 }
632 else
633 {
634 lineWrapper.startParagraph(
635 paragraphStart,
636 paragraphStart + paragraphText.length(),
637 false
638 );
639 }
640
641 List<Integer> tabIndexes = JRStringUtil.getTabIndexes(paragraphText);
642
643 int[] currentTabHolder = new int[]{0};
644 TabStop[] nextTabStopHolder = new TabStop[]{null};
645 boolean[] requireNextWordHolder = new boolean[]{false};
646
647 measuredState.paragraphStartLine = measuredState.lines;
648 measuredState.textOffset = paragraphStart;
649
650 boolean rendered = true;
651 boolean renderedLine = false;
652
653 // the paragraph is measured one line at a time
654 while (lineWrapper.paragraphPosition() < lineWrapper.paragraphEnd() && rendered)
655 {
656 rendered = renderNextLine(lineWrapper, tabIndexes, currentTabHolder, nextTabStopHolder, requireNextWordHolder);
657 renderedLine = renderedLine || rendered;
658 }
659
660 //if we rendered at least one line, and the last line didn't fit
661 //and the text does not overflow
662 if (!rendered && prevMeasuredState != null && !canOverflow)
663 {
664 //handle last rendered row
665 processLastTruncatedRow(lineWrapper, paragraphText, paragraphStart, renderedLine);
666 }
667
668 return rendered;
669 }
670
671 protected void processLastTruncatedRow(
672 TextLineWrapper lineWrapper,
673 String paragraphText,
674 int paragraphOffset,
675 boolean lineTruncated
676 )
677 {
678 //FIXME move all this to TextLineWrapper?
679 if (lineTruncated && isToTruncateAtChar())
680 {
681 truncateLastLineAtChar(lineWrapper, paragraphText, paragraphOffset);
682 }
683
684 appendTruncateSuffix(lineWrapper);
685 }
686
687 protected void truncateLastLineAtChar(
688 TextLineWrapper lineWrapper,
689 String paragraphText,
690 int paragraphOffset
691 )
692 {
693 //truncate the original line at char
694 measuredState = prevMeasuredState.cloneState();
695 lineWrapper.startParagraph(measuredState.textOffset,
696 paragraphOffset + paragraphText.length(),
697 true);
698 //render again the last line
699 //if the line does not fit now, it will remain empty
700 renderNextLine(lineWrapper, null, new int[]{0}, new TabStop[]{null}, new boolean[]{false});
701 }
702
703 protected void appendTruncateSuffix(TextLineWrapper lineWrapper)
704 {
705 String truncateSuffx = getTruncateSuffix();
706 if (truncateSuffx == null)
707 {
708 return;
709 }
710
711 int lineStart = prevMeasuredState.textOffset;
712
713 //advance from the line start until the next line start or the first newline
714 String lineText = lineWrapper.getLineText(lineStart, measuredState.textOffset);
715 int linePosition = lineText.length();
716
717 //iterate to the beginning of the line
718 boolean done = false;
719 do
720 {
721 measuredState = prevMeasuredState.cloneState();
722
723 String text = lineText.substring(0, linePosition) + truncateSuffx;
724 boolean truncateAtChar = isToTruncateAtChar();
725 TextLineWrapper lastLineWrapper = lineWrapper.lastLineWrapper(text,
726 measuredState.textOffset, linePosition, truncateAtChar);
727
728 BreakIterator breakIterator =
729 truncateAtChar
730 ? BreakIterator.getCharacterInstance()
731 : BreakIterator.getLineInstance();
732 breakIterator.setText(text);
733
734 if (renderNextLine(lastLineWrapper, null, new int[]{0}, new TabStop[]{null}, new boolean[]{false}))
735 {
736 int lastPos = lastLineWrapper.paragraphPosition();
737 //test if the entire suffix fit
738 if (lastPos == linePosition + truncateSuffx.length())
739 {
740 //subtract the suffix from the offset
741 measuredState.textOffset -= truncateSuffx.length();
742 measuredState.textSuffix = truncateSuffx;
743 done = true;
744 }
745 else
746 {
747 linePosition = breakIterator.preceding(linePosition);
748 if (linePosition == BreakIterator.DONE)
749 {
750 //if the text suffix did not fit the line, only the part of it that fits will show
751
752 //truncate the suffix
753 String actualSuffix = truncateSuffx.substring(0,
754 measuredState.textOffset - prevMeasuredState.textOffset);
755 //if the last text char is not a new line
756 if (prevMeasuredState.textOffset > 0
757 && lineWrapper.charAt(prevMeasuredState.textOffset - 1) != '\n')
758 {
759 //force a new line so that the suffix is displayed on the last line
760 actualSuffix = '\n' + actualSuffix;
761 }
762 measuredState.textSuffix = actualSuffix;
763
764 //restore the next to last line offset
765 measuredState.textOffset = prevMeasuredState.textOffset;
766
767 done = true;
768 }
769 }
770 }
771 else
772 {
773 //if the line did not fit, leave it empty
774 done = true;
775 }
776 }
777 while (!done);
778 }
779
780 protected boolean isToTruncateAtChar()
781 {
782 //FIXME do not read each time
783 return JRPropertiesUtil.getInstance(jasperReportsContext).getBooleanProperty(propertiesHolder,
784 JRTextElement.PROPERTY_TRUNCATE_AT_CHAR, false);
785 }
786
787 protected String getTruncateSuffix()
788 {
789 //FIXME do not read each time
790 String truncateSuffx = JRPropertiesUtil.getInstance(jasperReportsContext).getProperty(propertiesHolder,
791 JRTextElement.PROPERTY_TRUNCATE_SUFFIX);
792 if (truncateSuffx != null)
793 {
794 truncateSuffx = truncateSuffx.trim();
795 if (truncateSuffx.length() == 0)
796 {
797 truncateSuffx = null;
798 }
799 }
800 return truncateSuffx;
801 }
802
803
804 protected boolean renderNextLine(TextLineWrapper lineWrapper, List<Integer> tabIndexes, int[] currentTabHolder, TabStop[] nextTabStopHolder, boolean[] requireNextWordHolder)
805 {
806 boolean lineComplete = false;
807
808 int lineStartPosition = lineWrapper.paragraphPosition();
809
810 float maxAscent = 0;
811 float maxDescent = 0;
812 float maxLeading = 0;
813 int characterCount = 0;
814 boolean isLeftToRight = true;
815
816 // each line is split into segments, using the tab character as delimiter
817 List<TabSegment> segments = new ArrayList<TabSegment>(1);
818
819 TabSegment oldSegment = null;
820 TabSegment crtSegment = null;
821
822 // splitting the current line into tab segments
823 while (!lineComplete)
824 {
825 // the current segment limit is either the next tab character or the paragraph end
826 int tabIndexOrEndIndex = (tabIndexes == null || currentTabHolder[0] >= tabIndexes.size() ? lineWrapper.paragraphEnd() : tabIndexes.get(currentTabHolder[0]) + 1);
827
828 int firstLineIndent = lineWrapper.paragraphPosition() == 0 ? jrParagraph.getFirstLineIndent() : 0;
829
830 if (
831 firstLineIndent != 0
832 && (isFirstParagraph && !indentFirstLine)
833 )
834 {
835 firstLineIndent = 0;
836 }
837
838 float startX = firstLineIndent + leftPadding;
839 float endX = width - jrParagraph.getRightIndent() - rightPadding;
840 endX = endX < startX ? startX : endX;
841 //formatWidth = endX - startX;
842 //formatWidth = endX;
843
844 int startIndex = lineWrapper.paragraphPosition();
845
846 float rightX = 0;
847
848 if (segments.size() == 0)
849 {
850 rightX = startX;
851 //nextTabStop = nextTabStop;
852 }
853 else
854 {
855 rightX = oldSegment.rightX;
856 nextTabStopHolder[0] = ParagraphUtil.getNextTabStop(jrParagraph, endX, rightX);
857 }
858
859 //float availableWidth = formatWidth - ParagraphUtil.getSegmentOffset(nextTabStopHolder[0], rightX); // nextTabStop can be null here; and that's OK
860 float availableWidth = endX - jrParagraph.getLeftIndent() - ParagraphUtil.getSegmentOffset(nextTabStopHolder[0], rightX); // nextTabStop can be null here; and that's OK
861
862 // creating a text layout object for each tab segment
863 TextLine textLine =
864 lineWrapper.nextLine(
865 availableWidth,
866 tabIndexOrEndIndex,
867 requireNextWordHolder[0]
868 );
869
870 if (textLine != null)
871 {
872 maxAscent = Math.max(maxAscent, textLine.getAscent());
873 maxDescent = Math.max(maxDescent, textLine.getDescent());
874 maxLeading = Math.max(maxLeading, textLine.getLeading());
875 characterCount += textLine.getCharacterCount();
876 isLeftToRight = isLeftToRight && textLine.isLeftToRight();
877
878 //creating the current segment
879 crtSegment = new TabSegment();
880 crtSegment.textLine = textLine;
881
882 float leftX = ParagraphUtil.getLeftX(nextTabStopHolder[0], textLine.getAdvance()); // nextTabStop can be null here; and that's OK
883 if (rightX > leftX)
884 {
885 crtSegment.leftX = rightX;
886 crtSegment.rightX = rightX + textLine.getAdvance();
887 }
888 else
889 {
890 crtSegment.leftX = leftX;
891 // we need this special tab stop based utility call because adding the advance to leftX causes rounding issues
892 crtSegment.rightX = ParagraphUtil.getRightX(nextTabStopHolder[0], textLine.getAdvance()); // nextTabStop can be null here; and that's OK
893 }
894
895 segments.add(crtSegment);
896 }
897
898 requireNextWordHolder[0] = true;
899
900 if (lineWrapper.paragraphPosition() == tabIndexOrEndIndex)
901 {
902 // the segment limit was a tab; going to the next tab
903 currentTabHolder[0] = currentTabHolder[0] + 1;
904 }
905
906 if (lineWrapper.paragraphPosition() == lineWrapper.paragraphEnd())
907 {
908 // the segment limit was the paragraph end; line completed and next line should start at normal zero x offset
909 lineComplete = true;
910 nextTabStopHolder[0] = null;
911 }
912 else
913 {
914 // there is paragraph text remaining
915 if (lineWrapper.paragraphPosition() == tabIndexOrEndIndex)
916 {
917 // the segment limit was a tab
918 if (crtSegment.rightX >= ParagraphUtil.getLastTabStop(jrParagraph, endX).getPosition())
919 {
920 // current segment stretches out beyond the last tab stop; line complete
921 lineComplete = true;
922 // next line should should start at first tab stop indent
923 nextTabStopHolder[0] = ParagraphUtil.getFirstTabStop(jrParagraph, endX);
924 }
925 // else
926 // {
927 // //nothing; this leaves lineComplete=false
928 // }
929 }
930 else
931 {
932 // the segment did not fit entirely
933 lineComplete = true;
934 if (textLine == null)
935 {
936 // nothing fitted; next line should start at first tab stop indent
937 if (nextTabStopHolder[0].getPosition() == ParagraphUtil.getFirstTabStop(jrParagraph, endX).getPosition())//FIXMETAB check based on segments.size()
938 {
939 // at second attempt we give up to avoid infinite loop
940 nextTabStopHolder[0] = null;
941 requireNextWordHolder[0] = false;
942
943 //provide dummy maxFontSize because it is used for the line height of this empty line when attempting drawing below
944 TextLine baseLine = lineWrapper.baseTextLine(startIndex);
945 maxAscent = baseLine.getAscent();
946 maxDescent = baseLine.getDescent();
947 maxLeading = baseLine.getLeading();
948 }
949 else
950 {
951 nextTabStopHolder[0] = ParagraphUtil.getFirstTabStop(jrParagraph, endX);
952 }
953 }
954 else
955 {
956 // something fitted
957 nextTabStopHolder[0] = null;
958 requireNextWordHolder[0] = false;
959 }
960 }
961 }
962
963 oldSegment = crtSegment;
964 }
965
966 float lineHeight = AbstractTextRenderer.getLineHeight(measuredState.lines == 0, jrParagraph, maxLeading, maxAscent);
967
968 if (measuredState.lines == 0) //FIXMEPARA
969 //if (measuredState.paragraphStartLine == measuredState.lines)
970 {
971 lineHeight += jrParagraph.getSpacingBefore();
972 }
973
974 float newTextHeight = measuredState.textHeight + lineHeight;
975
976 //this test is somewhat inconsistent with JRFillTextElement.chopTextElement which truncates the text height to int.
977 //thus it can happen that a text which would normally be measured to textHeight=18.6 and element height=18
978 //overflows when there are exactly 18 pixels left on the page.
979 boolean fits = newTextHeight + maxDescent <= maxHeight;
980 if (fits)
981 {
982 prevMeasuredState = measuredState.cloneState();
983
984 measuredState.isLeftToRight = isLeftToRight;//run direction is per layout; but this is the best we can do for now
985 measuredState.textWidth = Math.max(measuredState.textWidth, (crtSegment == null ? 0 : (crtSegment.rightX - leftPadding)));//FIXMENOW is RTL text actually working here?
986 measuredState.textHeight = newTextHeight;
987 measuredState.lines++;
988
989 if (
990 measuredState.isMeasured
991 && (tabIndexes == null || tabIndexes.size() == 0)
992 && !hasParagraphIndents()
993 )
994 {
995 measuredState.fontSizeSum +=
996 lineWrapper.maxFontsize(lineStartPosition, lineStartPosition + characterCount);
997
998 if (measuredState.lines == 1)
999 {
1000 measuredState.firstLineLeading = measuredState.textHeight;
1001 measuredState.firstLineMaxFontSize = measuredState.fontSizeSum;
1002 }
1003 }
1004 else
1005 {
1006 measuredState.isMeasured = false;
1007 }
1008
1009 // here is the Y offset where we would draw the line
1010 //lastDrawPosY = drawPosY;
1011 //
1012 measuredState.textHeight += maxDescent;
1013
1014 measuredState.textOffset += lineWrapper.paragraphPosition() - lineStartPosition;
1015
1016 if (lineWrapper.paragraphPosition() < lineWrapper.paragraphEnd())
1017 {
1018 //if not the last line in a paragraph, save the line break position
1019 measuredState.addLineBreak();
1020 }
1021 // else //FIXMEPARA
1022 // {
1023 // measuredState.textHeight += jrParagraph.getSpacingAfter();
1024 // }
1025
1026 measuredState.isParagraphCut = lineWrapper.paragraphPosition() < lineWrapper.paragraphEnd();
1027 }
1028
1029 return fits;
1030 }
1031
1032 protected JRPropertiesHolder getTextPropertiesHolder()
1033 {
1034 return propertiesHolder;
1035 }
1036
1037
1038
1039 /**
1040 *
1041 */
1042 public FontRenderContext getFontRenderContext()
1043 {
1044 return AwtTextRenderer.LINE_BREAK_FONT_RENDER_CONTEXT;
1045 }
1046
1047 class Context implements TextMeasureContext
1048 {
1049 @Override
1050 public JasperReportsContext getJasperReportsContext()
1051 {
1052 return jasperReportsContext;
1053 }
1054
1055 @Override
1056 public JRCommonText getElement()
1057 {
1058 return textElement;
1059 }
1060
1061 @Override
1062 public JRPropertiesHolder getPropertiesHolder()
1063 {
1064 return propertiesHolder;
1065 }
1066
1067 @Override
1068 public boolean isIgnoreMissingFont()
1069 {
1070 return ignoreMissingFont;
1071 }
1072
1073 @Override
1074 public FontRenderContext getFontRenderContext()
1075 {
1076 return TextMeasurer.this.getFontRenderContext();
1077 }
1078 }
1079 }
1080
1081 class TabSegment
1082 {
1083 public TextLine textLine;
1084 public float leftX;
1085 public float rightX;
1086 }
1087