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;
27 import java.awt.font.LineBreakMeasurer;
28 import java.awt.font.LineMetrics;
29 import java.awt.font.TextAttribute;
30 import java.awt.geom.Rectangle2D;
31 import java.lang.Character.UnicodeBlock;
32 import java.text.AttributedCharacterIterator.Attribute;
33 import java.text.AttributedString;
34 import java.text.Bidi;
35 import java.text.BreakIterator;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.LinkedHashMap;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 import java.util.Set;
44 import java.util.UUID;
45
46 import org.apache.commons.logging.Log;
47 import org.apache.commons.logging.LogFactory;
48
49 import net.sf.jasperreports.annotations.properties.Property;
50 import net.sf.jasperreports.annotations.properties.PropertyScope;
51 import net.sf.jasperreports.engine.JRPropertiesUtil;
52 import net.sf.jasperreports.engine.fonts.AwtFontAttribute;
53 import net.sf.jasperreports.engine.fonts.FontUtil;
54 import net.sf.jasperreports.engine.util.JRStyledText;
55 import net.sf.jasperreports.engine.util.JRStyledText.Run;
56 import net.sf.jasperreports.engine.util.Pair;
57 import net.sf.jasperreports.properties.PropertyConstants;
58
59 /**
60  * @author Lucian Chirita (lucianc@users.sourceforge.net)
61  */

62 public class SimpleTextLineWrapper implements TextLineWrapper
63 {
64     
65     @Property(
66             category = PropertyConstants.CATEGORY_FILL,
67             scopes = {PropertyScope.CONTEXT},
68             sinceVersion = PropertyConstants.VERSION_4_7_1
69             )
70     public static final String PROPERTY_MEASURE_EXACT = 
71             JRPropertiesUtil.PROPERTY_PREFIX + "measure.simple.text.exact";
72     
73     @Property(
74             category = PropertyConstants.CATEGORY_FILL,
75             defaultValue = "2000",
76             scopes = {PropertyScope.CONTEXT},
77             sinceVersion = PropertyConstants.VERSION_4_7_1,
78             valueType = Integer.class
79             )
80     public static final String PROPERTY_ELEMENT_CACHE_SIZE = 
81             JRPropertiesUtil.PROPERTY_PREFIX + "measure.simple.text.element.cache.size";
82
83     public static final String MEASURE_EXACT_ALWAYS = "always";
84     public static final String MEASURE_EXACT_MULTILINE = "multiline";
85
86     private static final Log log = LogFactory.getLog(SimpleTextLineWrapper.class);
87
88     protected static final int FONT_MIN_COUNT = 10;
89     protected static final double FONT_SIZE_MIN_FACTOR = 0.1;
90     protected static final double FONT_WIDTH_CHECK_FACTOR = 1.2;
91     
92     protected static final int NEXT_BREAK_INDEX_THRESHOLD = 3;
93     protected static final int COMPEX_LAYOUT_START_CHAR = 0x0300;// got this from sun.font.FontUtilities
94     protected static final int COMPEX_LAYOUT_END_CHAR = 0x206F;// got this from sun.font.FontUtilities
95     
96     protected static final String FILL_CACHE_KEY_ELEMENT_FONT_INFOS = 
97             SimpleTextLineWrapper.class.getName() + "#elementFontInfos";
98     
99     protected static final String FILL_CACHE_KEY_GENERAL_FONT_INFOS = 
100             SimpleTextLineWrapper.class.getName() + "#generalFontInfos";
101     
102     protected static final Set<Character.UnicodeBlock> simpleLayoutBlocks;
103     static
104     {
105         // white list of Unicode blocks that have simple text layout
106         simpleLayoutBlocks = new HashSet<Character.UnicodeBlock>();
107         // got these from sun.font.FontUtilities, but the list is not exhaustive
108         simpleLayoutBlocks.add(Character.UnicodeBlock.GREEK);
109         simpleLayoutBlocks.add(Character.UnicodeBlock.CYRILLIC);
110         simpleLayoutBlocks.add(Character.UnicodeBlock.CYRILLIC_SUPPLEMENTARY);
111         simpleLayoutBlocks.add(Character.UnicodeBlock.ARMENIAN);
112         simpleLayoutBlocks.add(Character.UnicodeBlock.SYRIAC);
113         simpleLayoutBlocks.add(Character.UnicodeBlock.THAANA);
114         simpleLayoutBlocks.add(Character.UnicodeBlock.MYANMAR);
115         simpleLayoutBlocks.add(Character.UnicodeBlock.GEORGIAN);
116         simpleLayoutBlocks.add(Character.UnicodeBlock.ETHIOPIC);
117         simpleLayoutBlocks.add(Character.UnicodeBlock.TAGALOG);
118         simpleLayoutBlocks.add(Character.UnicodeBlock.MONGOLIAN);
119         simpleLayoutBlocks.add(Character.UnicodeBlock.LATIN_EXTENDED_ADDITIONAL);
120         simpleLayoutBlocks.add(Character.UnicodeBlock.GREEK_EXTENDED);
121     }
122     
123     // storing per instance to avoid too many calls (and to allow runtime level changes)
124     private final boolean logTrace = log.isTraceEnabled();
125     
126     private TextMeasureContext context;
127     private boolean measureSimpleTexts;
128     private boolean measureExact;
129     private boolean measureExactMultiline;
130     private Map<FontKey, ElementFontInfo> fontInfos;
131     
132     private String wholeText;
133     private FontKey fontKey;
134     private ElementFontInfo fontInfo;
135
136     private String paragraphText;
137     private boolean paragraphTruncateAtChar;
138     private boolean paragraphLeftToRight;
139     private boolean paragraphMeasureExact;
140     private int paragraphOffset;
141     private int paragraphPosition;
142     private BreakIterator paragraphBreakIterator;
143
144     public SimpleTextLineWrapper()
145     {
146     }
147
148     public SimpleTextLineWrapper(SimpleTextLineWrapper parent)
149     {
150         this.context = parent.context;
151         this.measureSimpleTexts = parent.measureSimpleTexts;
152         this.measureExact = parent.measureExact;
153         this.measureExactMultiline = parent.measureExactMultiline;
154         this.fontInfos = parent.fontInfos;
155         
156         this.wholeText = parent.wholeText;
157         this.fontKey = parent.fontKey;
158         this.fontInfo = parent.fontInfo;
159     }
160     
161     @Override
162     public void init(TextMeasureContext context)
163     {
164         this.context = context;
165         
166         JRPropertiesUtil properties = JRPropertiesUtil.getInstance(context.getJasperReportsContext());
167         measureSimpleTexts = properties.getBooleanProperty(context.getPropertiesHolder(), 
168                 TextMeasurer.PROPERTY_MEASURE_SIMPLE_TEXTS, true);
169         if (measureSimpleTexts)
170         {
171             String exactProp = properties.getProperty(context.getPropertiesHolder(), PROPERTY_MEASURE_EXACT);
172             if (exactProp != null)
173             {
174                 if (MEASURE_EXACT_ALWAYS.equals(exactProp))
175                 {
176                     measureExact = true;
177                 }
178                 else if (MEASURE_EXACT_MULTILINE.equals(exactProp))
179                 {
180                     measureExactMultiline = true;
181                 }
182             }
183
184             fontInfos = new HashMap<FontKey, ElementFontInfo>();
185         }
186     }
187
188     @Override
189     public boolean start(JRStyledText styledText)
190     {
191         if (!measureSimpleTexts)
192         {
193             return false;
194         }
195         
196         List<Run> runs = styledText.getRuns();
197         if (runs.size() != 1)
198         {
199             // multiple styles
200             return false;
201         }
202         
203         wholeText = styledText.getText();
204         if (wholeText.indexOf('\t') >= 0)
205         {
206             // supporting tabs is more difficult because we'd need
207             // measureParagraphFragment to include the white space advance.
208             return false;
209         }
210         
211         Run run = styledText.getRuns().get(0);
212         if (run.attributes.get(TextAttribute.SUPERSCRIPT) != null)
213         {
214             // not handling this case, see JRStyledText.getAwtAttributedString
215             return false;
216         }
217
218         AwtFontAttribute fontAttribute = AwtFontAttribute.fromAttributes(run.attributes);
219         Number size = (Number) run.attributes.get(TextAttribute.SIZE);
220         if (!fontAttribute.hasAttribute() || size == null)
221         {
222             // this should not happen, but still checking
223             return false;
224         }
225         
226         int style = 0;
227         Number posture = (Number) run.attributes.get(TextAttribute.POSTURE);
228         if (posture != null && !TextAttribute.POSTURE_REGULAR.equals(posture))
229         {
230             if (TextAttribute.POSTURE_OBLIQUE.equals(posture))
231             {
232                 style |= Font.ITALIC;
233             }
234             else
235             {
236                 // non standard posture
237                 return false;
238             }
239         }
240         
241         Number weight = (Number) run.attributes.get(TextAttribute.WEIGHT);
242         if (weight != null && !TextAttribute.WEIGHT_REGULAR.equals(weight))
243         {
244             if (TextAttribute.WEIGHT_BOLD.equals(weight))
245             {
246                 style |= Font.BOLD;
247             }
248             else
249             {
250                 // non standard weight
251                 return false;
252             }
253         }
254         
255         fontKey = new FontKey(fontAttribute, size.floatValue(), style, styledText.getLocale());
256         createFontInfo(run.attributes);
257         
258         return true;
259     }
260
261     protected void createFontInfo(Map<Attribute, Object> textAttributes)
262     {
263         fontInfo = fontInfos.get(fontKey);
264         if (fontInfo != null)
265         {
266             // found in local cache
267             return;
268         }
269         
270         Map<Pair<UUID, FontKey>, ElementFontInfo> elementFontInfos = null;
271         Pair<UUID, FontKey> elementFontKey = null;
272         
273         // look in the fill cache
274         if (context.getElement() instanceof JRFillElement)
275         {
276             JRFillElement fillElement = (JRFillElement) context.getElement();
277             JRFillContext fillContext = fillElement.getFiller().getFillContext();
278             elementFontKey = new Pair<UUID, FontKey>(fillElement.getUUID(), fontKey);
279             
280             elementFontInfos = (Map<Pair<UUID, FontKey>, ElementFontInfo>) fillContext.getFillCache(FILL_CACHE_KEY_ELEMENT_FONT_INFOS);
281             if (elementFontInfos == null)
282             {
283                 elementFontInfos = createElementFontInfosFillCache();
284                 fillContext.setFillCache(FILL_CACHE_KEY_ELEMENT_FONT_INFOS, elementFontInfos);
285             }
286
287             fontInfo = elementFontInfos.get(elementFontKey);
288         }
289         
290         if (fontInfo == null)
291         {
292             // did not find in the general cache, create the font info
293             // we first need the general font info
294             FontInfo generalFontInfo = getGeneralFontInfo(textAttributes);
295             
296             if (logTrace)
297             {
298                 log.trace("creating element font info for " + fontKey
299                         + (elementFontKey == null ? "" : (" and element " + elementFontKey.first())));
300             }
301             
302             fontInfo = new ElementFontInfo(generalFontInfo);
303             fontInfos.put(fontKey, fontInfo);
304             
305             if (elementFontInfos != null && elementFontKey.first() != null)//UUID should not be null but check to be sure
306             {
307                 elementFontInfos.put(elementFontKey, fontInfo);
308             }
309         }
310     }
311
312     protected HashMap<Pair<UUID, FontKey>, ElementFontInfo> createElementFontInfosFillCache()
313     {
314         final int cacheSize = JRPropertiesUtil.getInstance(context.getJasperReportsContext()).getIntegerProperty(
315                 PROPERTY_ELEMENT_CACHE_SIZE, 2000);//hardcoded default
316         if (log.isDebugEnabled())
317         {
318             log.debug("creating element font infos cache of size " + cacheSize);
319         }
320         
321         // creating a LRU map
322         return new LinkedHashMap<Pair<UUID,FontKey>, SimpleTextLineWrapper.ElementFontInfo>(64, 0.75f, true)
323         {
324             @Override
325             protected boolean removeEldestEntry(Entry<Pair<UUID, FontKey>, ElementFontInfo> eldest)
326             {
327                 return size() > cacheSize;
328             }
329         };
330     }
331     
332     protected FontInfo getGeneralFontInfo(Map<Attribute, Object> textAttributes)
333     {
334         Map<FontKey, FontInfo> generalFontInfos = null;
335         FontInfo generalFontInfo = null;
336         // look in the fill cache
337         if (context.getElement() instanceof JRFillElement)
338         {
339             JRFillElement fillElement = (JRFillElement) context.getElement();
340             JRFillContext fillContext = fillElement.getFiller().getFillContext();
341             
342             generalFontInfos = (Map<FontKey, FontInfo>) fillContext.getFillCache(FILL_CACHE_KEY_GENERAL_FONT_INFOS);
343             if (generalFontInfos == null)
344             {
345                 generalFontInfos = new HashMap<FontKey, FontInfo>();
346                 fillContext.setFillCache(FILL_CACHE_KEY_GENERAL_FONT_INFOS, generalFontInfos);
347             }
348             
349             generalFontInfo = generalFontInfos.get(fontKey);            
350         }
351         
352         if (generalFontInfo == null)
353         {
354             Font font = loadFont(textAttributes);
355             boolean complexLayout = determineComplexLayout(font);
356             // computing the leading a single time, assuming that it doesn't change with text
357             //FIXME verify if computing leading for each line is needed
358             float leading = determineLeading(font);
359             if (logTrace)
360             {
361                 log.trace("font " + font + " has complex layout " + complexLayout
362                         + ", leading " + leading);
363             }
364
365             generalFontInfo = new FontInfo(font, complexLayout, leading);
366             
367             if (generalFontInfos != null)
368             {
369                 generalFontInfos.put(fontKey, generalFontInfo);
370             }
371         }
372         
373         return generalFontInfo;
374     }
375
376     protected Font loadFont(Map<Attribute, Object> textAttributes)
377     {
378         // check bundled fonts
379         FontUtil fontUtil = FontUtil.getInstance(context.getJasperReportsContext());
380         Font font = fontUtil.getAwtFontFromBundles(fontKey.fontAttribute, fontKey.style, fontKey.size, fontKey.locale, false);
381         if (font == null)
382         {
383             // checking AWT font
384             fontUtil.checkAwtFont(fontKey.fontAttribute.getFamily(), context.isIgnoreMissingFont());
385             // creating AWT font
386             // FIXME using the current text attributes might be slightly dangerous as we are sharing font metrics
387             font = Font.getFont(textAttributes);
388         }
389         return font;
390     }
391
392     protected boolean determineComplexLayout(Font font)
393     {
394         // this tries to emulate the tests in Font.getStringBounds()
395         //FIXME use font.hasLayoutAttributes() instead of this?
396         Map<TextAttribute, ?> fontAttributes = font.getAttributes();
397         Object kerning = fontAttributes.get(TextAttribute.KERNING);
398         Object ligatures = fontAttributes.get(TextAttribute.LIGATURES);
399         return (kerning != null && TextAttribute.KERNING_ON.equals(kerning))
400                 || (ligatures != null && TextAttribute.LIGATURES_ON.equals(ligatures))
401                 || font.isTransformed();
402     }
403     
404     protected float determineLeading(Font font)
405     {
406         LineMetrics lineMetrics = font.getLineMetrics(" ", context.getFontRenderContext());
407         return lineMetrics.getLeading();
408     }
409
410     @Override
411     public void startParagraph(int paragraphStart, int paragraphEnd,
412             boolean truncateAtChar)
413     {
414         String text = wholeText.substring(paragraphStart, paragraphEnd);
415         startParagraph(text, paragraphStart, truncateAtChar);
416     }
417
418     @Override
419     public void startEmptyParagraph(int paragraphStart)
420     {
421         startParagraph(" ", paragraphStart, false);
422     }
423     
424     protected void startParagraph(String text, int start, boolean truncateAtChar)
425     {
426         paragraphText = text;
427         paragraphTruncateAtChar = truncateAtChar;
428         
429         char[] textChars = text.toCharArray();
430         // direction is per paragraph
431         paragraphLeftToRight = isLeftToRight(textChars);
432         paragraphMeasureExact = isParagraphMeasureExact(textChars);
433         
434         if (logTrace)
435         {
436             log.trace("paragraph start at " + start
437                     + ", truncate at char " + truncateAtChar
438                     + ", LTR " + paragraphLeftToRight
439                     + ", exact measure " + paragraphMeasureExact);
440         }
441         
442         paragraphOffset = start;
443         paragraphPosition = 0;
444         
445         paragraphBreakIterator = truncateAtChar ? BreakIterator.getCharacterInstance()
446                 : BreakIterator.getLineInstance();
447         paragraphBreakIterator.setText(paragraphText);
448     }
449
450     protected boolean isLeftToRight(char[] chars)
451     {
452         boolean leftToRight = true;
453         if (Bidi.requiresBidi(chars, 0, chars.length))
454         {
455             // determining the text direction
456             // using default LTR as there's no way to have other default in the text
457             Bidi bidi = new Bidi(chars, 0, null, 0, chars.length, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
458             leftToRight = bidi.baseIsLeftToRight();
459         }
460         return leftToRight;
461     }
462
463     protected boolean isParagraphMeasureExact(char[] chars)
464     {
465         // when we have complex text layout or truncating at char,
466         // perform exact break measurement as estimating/guessing could be slower
467         if (measureExact
468                 || fontInfo.fontInfo.complexLayout
469                 || paragraphTruncateAtChar)
470         {
471             return true;
472         }
473         
474         return hasComplexLayout(chars);
475     }
476
477     protected boolean hasComplexLayout(char[] chars)
478     {
479         UnicodeBlock prevBlock = null;
480         for (int i = 0; i < chars.length; i++)
481         {
482             char ch = chars[i];
483             if (ch >= COMPEX_LAYOUT_START_CHAR && ch <= COMPEX_LAYOUT_END_CHAR)
484             {
485                 //FIXME use icu4j or CharPredicateCache
486                 UnicodeBlock chBlock = Character.UnicodeBlock.of(ch);
487                 if (chBlock == null)
488                 {
489                     // being conservative
490                     return true;
491                 }
492                 
493                 // if the same block as the previous block, avoid going to the hash set
494                 // this could offer some speed improvement
495                 if (prevBlock != chBlock)
496                 {
497                     prevBlock = chBlock;
498                     
499                     if (!simpleLayoutBlocks.contains(chBlock))
500                     {
501                         return true;
502                     }
503                 }
504             }
505         }
506         return false;
507     }
508
509     @Override
510     public int paragraphPosition()
511     {
512         return paragraphPosition;
513     }
514
515     @Override
516     public int paragraphEnd()
517     {
518         return paragraphText.length();
519     }
520     
521     @Override
522     public TextLine nextLine(float width, int endLimit, boolean requireWord)
523     {
524         if (logTrace)
525         {
526             log.trace("simple line measurement at " + (paragraphOffset + paragraphPosition)
527                     + " to " + (paragraphOffset + endLimit)
528                     + " in width " + width
529                     + " with font " + fontInfo);
530         }
531         
532         // the result
533         TextLine textLine;
534         if (useExactLineMeasurement())
535         {
536             textLine = measureExactLine(width, endLimit, requireWord);
537         }
538         else
539         {
540             textLine = measureLine(width, requireWord, endLimit);
541         }
542         return textLine;
543     }
544
545     protected boolean useExactLineMeasurement()
546     {
547         // when missing a character width estimate perform one exact measurement
548         return paragraphMeasureExact
549                 || !fontInfo.hasCharWidthEstimate();
550     }
551     
552     protected TextLine measureExactLine(float width, int endLimit, boolean requireWord)
553     {
554         int breakIndex = measureExactLineBreakIndex(width, endLimit, requireWord);
555         if (breakIndex <= paragraphPosition)
556         {
557             // nothing fit
558             return null;
559         }
560         
561         Rectangle2D lineBounds = measureParagraphFragment(breakIndex);
562         return toTextLine(breakIndex, lineBounds);
563     }
564     
565     protected int measureExactLineBreakIndex(float width, int endLimit, boolean requireWord)
566     {
567         //FIXME would it be faster to create and cache a LineBreakMeasurer for the whole paragraph?
568         Map<Attribute, Object> attributes = new HashMap<Attribute, Object>();
569         // we only need the font as it includes the size and style
570         attributes.put(TextAttribute.FONT, fontInfo.fontInfo.font);
571         
572         String textLine = paragraphText.substring(paragraphPosition, endLimit);
573         AttributedString attributedLine = new AttributedString(textLine, attributes);
574
575         // we need a fresh iterator for the line
576         BreakIterator breakIterator = paragraphTruncateAtChar ? BreakIterator.getCharacterInstance()
577                 : BreakIterator.getLineInstance();
578         LineBreakMeasurer breakMeasurer = new LineBreakMeasurer(attributedLine.getIterator(), 
579                 breakIterator, context.getFontRenderContext());
580         int breakIndex = breakMeasurer.nextOffset(width, endLimit - paragraphPosition, requireWord) 
581                 + paragraphPosition;
582         if (logTrace)
583         {
584             log.trace("exact line break index measured at " + (paragraphOffset + breakIndex));
585         }
586         
587         return breakIndex;
588     }
589
590     protected TextLine measureLine(float width, boolean requireWord, int endLimit)
591     {
592         // try to guess how much of the text would fit based on the average char width
593         int measureIndex = estimateBreakIndex(width, endLimit);
594         
595         // if estimating that there's more than a line, check measureExactMultiline
596         if (measureIndex < endLimit && measureExactMultiline)
597         {
598             return measureExactLine(width, endLimit, requireWord);
599         }
600         
601         // measure the text
602         Rectangle2D bounds = measureParagraphFragment(measureIndex);
603         //FIXME fast exit when the height is exceeded
604         
605         Rectangle2D measuredBounds = bounds;
606         if (bounds.getWidth() <= width)
607         {
608             // see if there's more that could fit
609             boolean done = false;
610             do
611             {
612                 int nextBreakIndex = measureIndex < endLimit? paragraphBreakIterator.following(measureIndex) 
613                         : BreakIterator.DONE;
614                 if (nextBreakIndex == BreakIterator.DONE || nextBreakIndex > endLimit)
615                 {
616                     // the next break is after the limit, we're done
617                     done = true;
618                 }
619                 else
620                 {
621                     // measure to the next break
622                     Rectangle2D nextBounds = measureParagraphFragment(nextBreakIndex);
623                     if (nextBounds.getWidth() <= width)
624                     {
625                         measuredBounds = nextBounds;
626                         measureIndex = nextBreakIndex;
627                         // loop
628                     }
629                     else
630                     {
631                         done = true;
632                     }
633                 }
634             } while (!done);
635         }
636         else
637         {
638             // didn't fit, try shorter texts
639             boolean done = false;
640             do
641             {
642                 int previousBreakIndex = measureIndex > paragraphPosition ? paragraphBreakIterator.preceding(measureIndex) 
643                         : BreakIterator.DONE;
644                 if (previousBreakIndex == BreakIterator.DONE || previousBreakIndex <= paragraphPosition)
645                 {
646                     if (requireWord)
647                     {
648                         // no full word fits, returning empty
649                         measureIndex = paragraphPosition;
650                     }
651                     else
652                     {
653                         // we need to break inside the word.
654                         // measuring the exact break index as estimating/guessing might be slower.
655                         measureIndex = measureExactLineBreakIndex(width, endLimit, requireWord);
656                         measuredBounds = measureParagraphFragment(measureIndex);
657                     }
658                     done = true;
659                 }
660                 else
661                 {
662                     measureIndex = previousBreakIndex;
663                     Rectangle2D prevBounds = measureParagraphFragment(measureIndex);
664                     if (prevBounds.getWidth() <= width)
665                     {
666                         // fitted, we're done
667                         measuredBounds = prevBounds;
668                         done = true;
669                     }
670                 }
671             } while (!done);
672         }
673         
674         if (measureIndex <= paragraphPosition)
675         {
676             // nothing fit
677             return null;
678         }
679         return toTextLine(measureIndex, measuredBounds);
680     }
681
682     protected int estimateBreakIndex(float width, int endLimit)
683     {
684         double avgCharWidth = fontInfo.charWidthEstimate();
685         if ((endLimit - paragraphPosition) * avgCharWidth <= width * FONT_WIDTH_CHECK_FACTOR)
686         {
687             // there are chances that the entire text would fit, let's be optimistic
688             return endLimit;
689         }
690         
691         // estimate how many characters would fit
692         int charCountEstimate =  (int) Math.ceil(width / avgCharWidth);
693         int estimateFitPosition = paragraphPosition + charCountEstimate;
694         if (estimateFitPosition > endLimit)
695         {
696             // estimated that everything would fit
697             return endLimit;
698         }
699         
700         // find the break after the estimate
701         int breakAfterEstimatePosition = paragraphBreakIterator.following(estimateFitPosition);
702         if (breakAfterEstimatePosition == BreakIterator.DONE || breakAfterEstimatePosition > endLimit)
703         {
704             breakAfterEstimatePosition = endLimit;
705         }
706         
707         int estimateIndex = breakAfterEstimatePosition;
708         // if the after break is too far way from the estimate, see if the break before is closer
709         if (breakAfterEstimatePosition > estimateFitPosition + NEXT_BREAK_INDEX_THRESHOLD)
710         {
711             int breakBeforeEstimatePosition = paragraphBreakIterator.previous();
712             // if the break before is closer than the break after, use the break before
713             if (breakBeforeEstimatePosition == BreakIterator.DONE
714                     && breakBeforeEstimatePosition > paragraphPosition
715                     && estimateFitPosition - breakBeforeEstimatePosition < breakAfterEstimatePosition - estimateFitPosition)
716             {
717                 estimateIndex = breakBeforeEstimatePosition;
718             }
719         }
720         return estimateIndex;
721     }
722
723     protected Rectangle2D measureParagraphFragment(int measureIndex)
724     {
725         int endIndex = measureIndex;
726         if (endIndex > paragraphPosition + 1) {
727             char lastMeasureChar = paragraphText.charAt(endIndex - 1);
728             if (Character.isWhitespace(lastMeasureChar)) {
729                 // exclude trailing white space from the text to measure.
730                 // use the previous break as limit, but always keep at least one character to measure.
731                 int preceding = paragraphBreakIterator.preceding(endIndex);
732                 if (preceding == BreakIterator.DONE || preceding <= paragraphPosition) {
733                     preceding = paragraphPosition + 1;
734                 }
735
736                 do {
737                     --endIndex;
738                     lastMeasureChar = paragraphText.charAt(endIndex - 1);
739                 } while (endIndex > preceding 
740                         && Character.isWhitespace(lastMeasureChar));
741             }
742         }
743
744         // note that trailing white space will not be included in the advance
745         Rectangle2D bounds = fontInfo.fontInfo.font.getStringBounds(paragraphText, paragraphPosition, endIndex, 
746                 context.getFontRenderContext());
747         
748         // adding the measurement to the font info statistics
749         fontInfo.recordMeasurement(bounds.getWidth() / (endIndex - paragraphPosition));
750         
751         if (logTrace)
752         {
753             log.trace("measured to index " + (endIndex + paragraphOffset) + " at width " + bounds.getWidth());
754         }
755         
756         return bounds;
757     }
758
759     protected TextLine toTextLine(int measureIndex,
760             Rectangle2D measuredBounds)
761     {
762         SimpleTextLine textLine = new SimpleTextLine();
763         textLine.setAscent((float) -measuredBounds.getY());
764         textLine.setDescent((float) (measuredBounds.getMaxY() - fontInfo.fontInfo.leading));
765         textLine.setLeading(fontInfo.fontInfo.leading); 
766         textLine.setCharacterCount(measureIndex - paragraphPosition);
767         textLine.setAdvance((float) measuredBounds.getWidth());
768         textLine.setLeftToRight(paragraphLeftToRight);
769         
770         // update the paragraph position
771         paragraphPosition = measureIndex;
772         
773         return textLine;
774     }
775
776     @Override
777     public TextLine baseTextLine(int index)
778     {
779         // this should only be called when the text is tabbed, which is not supported 
780         throw new UnsupportedOperationException();
781     }
782
783     @Override
784     public float maxFontsize(int start, int end)
785     {
786         return fontKey.size;
787     }
788
789     @Override
790     public String getLineText(int start, int end)
791     {
792         int newLineIdx = wholeText.indexOf('\n', start);
793         int endIdx = (newLineIdx >= 0 && newLineIdx < end) ? newLineIdx : end;
794         return wholeText.substring(start, endIdx);
795     }
796
797     @Override
798     public char charAt(int index)
799     {
800         return wholeText.charAt(index);
801     }
802
803     @Override
804     public TextLineWrapper lastLineWrapper(String lineText, int start, int textLength, 
805             boolean truncateAtChar)
806     {
807         if (logTrace)
808         {
809             log.trace("last line wrapper at " + start + ", textLength " + textLength);
810         }
811         
812         SimpleTextLineWrapper lastLineWrapper = new SimpleTextLineWrapper(this);
813         lastLineWrapper.startParagraph(lineText, start, truncateAtChar);
814         return lastLineWrapper;
815     }
816     
817     protected static class FontKey
818     {
819         AwtFontAttribute fontAttribute;
820         float size;
821         int style;
822         Locale locale;
823         
824         public FontKey(AwtFontAttribute fontAttribute, float size, int style, Locale locale)
825         {
826             super();
827             this.fontAttribute = fontAttribute;
828             this.size = size;
829             this.style = style;
830             this.locale = locale;
831         }
832         
833         @Override
834         public int hashCode()
835         {
836             int hash = 43;
837             hash = hash*29 + fontAttribute.hashCode();
838             hash = hash*29 + Float.floatToIntBits(size);
839             hash = hash*29 + style;
840             hash = hash*29 + (locale == null ? 0 : locale.hashCode());
841             return hash;
842         }
843         
844         @Override
845         public boolean equals(Object obj)
846         {
847             FontKey info = (FontKey) obj;
848             return fontAttribute.equals(info.fontAttribute) && size == info.size && style == info.style
849                     && ((locale == null) ? (info.locale == null) : (info.locale != null && locale.equals(info.locale)));
850         }
851         
852         @Override
853         public String toString()
854         {
855             return "{font: " + fontAttribute
856                     + ", size: " + size
857                     + ", style: " + style
858                     + "}";
859         }
860     }
861     
862     protected static class FontInfo
863     {
864         final Font font;
865         final boolean complexLayout;
866         final float leading;
867         final FontStatistics fontStatistics;
868         
869         public FontInfo(Font font, boolean complexLayout, float leading)
870         {
871             this.font = font;
872             this.complexLayout = complexLayout;
873             this.leading = leading;
874             this.fontStatistics = new FontStatistics();
875         }
876         
877         @Override
878         public String toString()
879         {
880             return font.toString();
881         }
882     }
883     
884     protected static class FontStatistics
885     {
886         int measurementsCount;
887         double characterWidthSum;
888         
889         public void recordMeasurement(double avgWidth)
890         {
891             ++measurementsCount;
892             characterWidthSum += avgWidth;
893         }
894     }
895     
896     protected static class ElementFontInfo
897     {
898         final FontInfo fontInfo;
899         final FontStatistics fontStatistics;
900         
901         public ElementFontInfo(FontInfo fontInfo)
902         {
903             this.fontInfo = fontInfo;
904             this.fontStatistics = new FontStatistics();
905         }
906         
907         public boolean hasCharWidthEstimate()
908         {
909             return fontStatistics.measurementsCount > 0
910                     || fontInfo.fontStatistics.measurementsCount > 0;
911         }
912
913         public double charWidthEstimate()
914         {
915             double avgCharWidth;
916             if (fontStatistics.measurementsCount > 0)
917             {
918                 avgCharWidth = fontStatistics.characterWidthSum / fontStatistics.measurementsCount;
919             }
920             else if (fontInfo.fontStatistics.measurementsCount > 0)
921             {
922                 avgCharWidth = fontInfo.fontStatistics.characterWidthSum / fontInfo.fontStatistics.measurementsCount;
923             }
924             else
925             {
926                 throw new IllegalStateException("No measurement available for char width estimate");
927             }
928             
929             return avgCharWidth;
930         }
931         
932         public void recordMeasurement(double avgWidth)
933         {
934             fontStatistics.recordMeasurement(avgWidth);
935             fontInfo.fontStatistics.recordMeasurement(avgWidth);
936         }
937         
938         @Override
939         public String toString()
940         {
941             return fontInfo.font.toString();
942         }
943     }
944
945 }
946