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.&lt;alias&gt;</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  * &lt;staticText&gt;
95  *   &lt;reportElement ...&gt;
96  *     &lt;property name="net.sf.jasperreports.text.measurer.factory" value="fast"/&gt;
97  *   &lt;/reportElement&gt;
98  *   &lt;text&gt;...&lt;/text&gt;
99  * &lt;/staticText&gt;
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=&#92;u2026</code></li>
138  * <li>at report level:</li>
139  * </ul>
140  * <pre>
141  * &lt;jasperReport ...&gt;
142  *   &lt;property name="net.sf.jasperreports.text.truncate.suffix" value="&amp;#x2026;"/&gt;
143  *   ...
144  * &lt;/jasperReport&gt;
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, nullnew 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, nullnew 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