1
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
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;
94 protected static final int COMPEX_LAYOUT_END_CHAR = 0x206F;
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
106 simpleLayoutBlocks = new HashSet<Character.UnicodeBlock>();
107
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
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
200 return false;
201 }
202
203 wholeText = styledText.getText();
204 if (wholeText.indexOf('\t') >= 0)
205 {
206
207
208 return false;
209 }
210
211 Run run = styledText.getRuns().get(0);
212 if (run.attributes.get(TextAttribute.SUPERSCRIPT) != null)
213 {
214
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
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
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
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
267 return;
268 }
269
270 Map<Pair<UUID, FontKey>, ElementFontInfo> elementFontInfos = null;
271 Pair<UUID, FontKey> elementFontKey = null;
272
273
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
293
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)
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);
316 if (log.isDebugEnabled())
317 {
318 log.debug("creating element font infos cache of size " + cacheSize);
319 }
320
321
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
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
357
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
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
384 fontUtil.checkAwtFont(fontKey.fontAttribute.getFamily(), context.isIgnoreMissingFont());
385
386
387 font = Font.getFont(textAttributes);
388 }
389 return font;
390 }
391
392 protected boolean determineComplexLayout(Font font)
393 {
394
395
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
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
456
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
466
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
486 UnicodeBlock chBlock = Character.UnicodeBlock.of(ch);
487 if (chBlock == null)
488 {
489
490 return true;
491 }
492
493
494
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
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
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
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
568 Map<Attribute, Object> attributes = new HashMap<Attribute, Object>();
569
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
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
593 int measureIndex = estimateBreakIndex(width, endLimit);
594
595
596 if (measureIndex < endLimit && measureExactMultiline)
597 {
598 return measureExactLine(width, endLimit, requireWord);
599 }
600
601
602 Rectangle2D bounds = measureParagraphFragment(measureIndex);
603
604
605 Rectangle2D measuredBounds = bounds;
606 if (bounds.getWidth() <= width)
607 {
608
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
617 done = true;
618 }
619 else
620 {
621
622 Rectangle2D nextBounds = measureParagraphFragment(nextBreakIndex);
623 if (nextBounds.getWidth() <= width)
624 {
625 measuredBounds = nextBounds;
626 measureIndex = nextBreakIndex;
627
628 }
629 else
630 {
631 done = true;
632 }
633 }
634 } while (!done);
635 }
636 else
637 {
638
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
649 measureIndex = paragraphPosition;
650 }
651 else
652 {
653
654
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
667 measuredBounds = prevBounds;
668 done = true;
669 }
670 }
671 } while (!done);
672 }
673
674 if (measureIndex <= paragraphPosition)
675 {
676
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
688 return endLimit;
689 }
690
691
692 int charCountEstimate = (int) Math.ceil(width / avgCharWidth);
693 int estimateFitPosition = paragraphPosition + charCountEstimate;
694 if (estimateFitPosition > endLimit)
695 {
696
697 return endLimit;
698 }
699
700
701 int breakAfterEstimatePosition = paragraphBreakIterator.following(estimateFitPosition);
702 if (breakAfterEstimatePosition == BreakIterator.DONE || breakAfterEstimatePosition > endLimit)
703 {
704 breakAfterEstimatePosition = endLimit;
705 }
706
707 int estimateIndex = breakAfterEstimatePosition;
708
709 if (breakAfterEstimatePosition > estimateFitPosition + NEXT_BREAK_INDEX_THRESHOLD)
710 {
711 int breakBeforeEstimatePosition = paragraphBreakIterator.previous();
712
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
730
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
745 Rectangle2D bounds = fontInfo.fontInfo.font.getStringBounds(paragraphText, paragraphPosition, endIndex,
746 context.getFontRenderContext());
747
748
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
771 paragraphPosition = measureIndex;
772
773 return textLine;
774 }
775
776 @Override
777 public TextLine baseTextLine(int index)
778 {
779
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