1
24 package net.sf.jasperreports.engine.util;
25
26 import java.awt.Font;
27 import java.awt.font.TextAttribute;
28 import java.text.AttributedCharacterIterator;
29 import java.text.AttributedCharacterIterator.Attribute;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.LinkedList;
34 import java.util.List;
35 import java.util.ListIterator;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.concurrent.ConcurrentHashMap;
39
40 import net.sf.jasperreports.engine.JRCommonText;
41 import net.sf.jasperreports.engine.JRPrintText;
42 import net.sf.jasperreports.engine.JRPropertiesUtil;
43 import net.sf.jasperreports.engine.JRStyledTextAttributeSelector;
44 import net.sf.jasperreports.engine.JasperReportsContext;
45 import net.sf.jasperreports.engine.fonts.FontFace;
46 import net.sf.jasperreports.engine.fonts.FontFamily;
47 import net.sf.jasperreports.engine.fonts.FontInfo;
48 import net.sf.jasperreports.engine.fonts.FontSetFamilyInfo;
49 import net.sf.jasperreports.engine.fonts.FontSetInfo;
50 import net.sf.jasperreports.engine.fonts.FontUtil;
51 import net.sf.jasperreports.engine.util.CharPredicateCache.Result;
52 import net.sf.jasperreports.engine.util.JRStyledText.Run;
53
54
55
56
59 public class JRStyledTextUtil
60 {
61
62 private final JRStyledTextAttributeSelector allSelector;
63 private final FontUtil fontUtil;
64 private final boolean ignoreMissingFonts;
65
66 private final Map<Pair<String, Locale>, FamilyFonts> familyFonts =
67 new ConcurrentHashMap<Pair<String, Locale>, FamilyFonts>();
68
69
72 private JRStyledTextUtil(JasperReportsContext jasperReportsContext)
73 {
74
75 this.allSelector = JRStyledTextAttributeSelector.getAllSelector(jasperReportsContext);
76 fontUtil = FontUtil.getInstance(jasperReportsContext);
77
78 ignoreMissingFonts = JRPropertiesUtil.getInstance(jasperReportsContext).getBooleanProperty(
79 JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT);
80 }
81
82
85 public static JRStyledTextUtil getInstance(JasperReportsContext jasperReportsContext)
86 {
87 return new JRStyledTextUtil(jasperReportsContext);
88 }
89
90
93 public String getTruncatedText(JRPrintText printText)
94 {
95 String truncatedText = null;
96 String originalText = printText.getOriginalText();
97 if (originalText != null)
98 {
99 if (printText.getTextTruncateIndex() == null)
100 {
101 truncatedText = originalText;
102 }
103 else
104 {
105 if (!JRCommonText.MARKUP_NONE.equals(printText.getMarkup()))
106 {
107 truncatedText = JRStyledTextParser.getInstance().write(
108 printText.getFullStyledText(allSelector),
109 0, printText.getTextTruncateIndex());
110 }
111 else
112 {
113 truncatedText = originalText.substring(0, printText.getTextTruncateIndex());
114 }
115 }
116
117 String textTruncateSuffix = printText.getTextTruncateSuffix();
118 if (textTruncateSuffix != null)
119 {
120 truncatedText += textTruncateSuffix;
121 }
122 }
123 return truncatedText;
124 }
125
126
129 public JRStyledText getStyledText(JRPrintText printText, JRStyledTextAttributeSelector attributeSelector)
130 {
131 String truncatedText = getTruncatedText(printText);
132 if (truncatedText == null)
133 {
134 return null;
135 }
136
137 Locale locale = JRStyledTextAttributeSelector.getTextLocale(printText);
138 JRStyledText styledText = getStyledText(printText, truncatedText, attributeSelector, locale);
139 return styledText;
140 }
141
142 protected JRStyledText getStyledText(JRPrintText printText, String text,
143 JRStyledTextAttributeSelector attributeSelector, Locale locale)
144 {
145 return JRStyledTextParser.getInstance().getStyledText(
146 attributeSelector.getStyledTextAttributes(printText),
147 text,
148 !JRCommonText.MARKUP_NONE.equals(printText.getMarkup()),
149 locale
150 );
151 }
152
153 public JRStyledText getProcessedStyledText(JRPrintText printText, JRStyledTextAttributeSelector attributeSelector,
154 String exporterKey)
155 {
156 String truncatedText = getTruncatedText(printText);
157 if (truncatedText == null)
158 {
159 return null;
160 }
161
162 Locale locale = JRStyledTextAttributeSelector.getTextLocale(printText);
163 JRStyledText styledText = getStyledText(printText, truncatedText, attributeSelector, locale);
164 JRStyledText processedStyledText = resolveFonts(styledText, locale, exporterKey);
165 return processedStyledText;
166 }
167
168 public JRStyledText resolveFonts(JRStyledText styledText, Locale locale)
169 {
170 return resolveFonts(styledText, locale, null);
171 }
172
173 protected JRStyledText resolveFonts(JRStyledText styledText, Locale locale, String exporterKey)
174 {
175 if (styledText == null || styledText.length() == 0)
176 {
177 return styledText;
178 }
179
180
181
182 String text = styledText.getText();
183 List<Run> runs = styledText.getRuns();
184 List<Run> newRuns = null;
185
186 if (runs.size() == 1)
187 {
188
189 Map<Attribute, Object> attributes = runs.get(0).attributes;
190 FamilyFonts families = getFamilyFonts(attributes, locale);
191 if (families.needsToResolveFonts(exporterKey))
192 {
193 newRuns = new ArrayList<Run>(runs.size() + 2);
194 matchFonts(text, 0, styledText.length(), attributes, families, newRuns);
195 }
196 }
197 else
198 {
199
200 boolean needsFontMatching = false;
201 for (Run run : runs)
202 {
203 FamilyFonts families = getFamilyFonts(run.attributes, locale);
204 if (families.needsToResolveFonts(exporterKey))
205 {
206 needsFontMatching = true;
207 break;
208 }
209 }
210
211 if (needsFontMatching)
212 {
213 newRuns = new ArrayList<Run>(runs.size() + 2);
214 AttributedCharacterIterator attributesIt = styledText.getAttributedString().getIterator();
215 int index = 0;
216 while (index < styledText.length())
217 {
218 int runEndIndex = attributesIt.getRunLimit();
219 Map<Attribute, Object> runAttributes = attributesIt.getAttributes();
220 FamilyFonts familyFonts = getFamilyFonts(runAttributes, locale);
221 if (familyFonts.needsToResolveFonts(exporterKey))
222 {
223 matchFonts(text, index, runEndIndex, runAttributes, familyFonts, newRuns);
224 }
225 else
226 {
227
228 copyRun(newRuns, runAttributes, index, runEndIndex);
229 }
230
231 index = runEndIndex;
232 attributesIt.setIndex(index);
233 }
234 }
235 }
236
237 if (newRuns == null)
238 {
239
240 return styledText;
241 }
242
243 JRStyledText processedText = createProcessedStyledText(styledText, text, newRuns);
244 return processedText;
245 }
246
247 protected JRStyledText createProcessedStyledText(JRStyledText styledText, String text, List<Run> newRuns)
248 {
249 Map<Attribute,Object> globalAttributes = null;
250 JRStyledText processedText = new JRStyledText(styledText.getLocale(), text);
251 for (Run newRun : newRuns)
252 {
253 if (newRun.startIndex == 0 && newRun.endIndex == text.length() && globalAttributes == null)
254 {
255 globalAttributes = newRun.attributes;
256 }
257 else
258 {
259 processedText.addRun(newRun);
260 }
261 }
262 processedText.setGlobalAttributes(globalAttributes == null ? styledText.getGlobalAttributes()
263 : globalAttributes);
264 return processedText;
265 }
266
267 protected void matchFonts(String text, int startIndex, int endIndex,
268 Map<Attribute, Object> attributes, FamilyFonts familyFonts,
269 List<Run> newRuns)
270 {
271 Number posture = (Number) attributes.get(TextAttribute.POSTURE);
272 boolean italic = posture != null && !TextAttribute.POSTURE_REGULAR.equals(posture);
273
274 Number weight = (Number) attributes.get(TextAttribute.WEIGHT);
275 boolean bold = weight != null && !TextAttribute.WEIGHT_REGULAR.equals(weight);
276
277 boolean hadUnmatched = false;
278 int index = startIndex;
279 do
280 {
281 FontMatch fontMatch = null;
282
283 if (bold && italic)
284 {
285 fontMatch = fontMatchRun(text, index, endIndex, familyFonts.boldItalicFonts);
286 }
287
288 if (bold && (fontMatch == null || fontMatch.fontInfo == null))
289 {
290 fontMatch = fontMatchRun(text, index, endIndex, familyFonts.boldFonts);
291 }
292
293 if (italic && (fontMatch == null || fontMatch.fontInfo == null))
294 {
295 fontMatch = fontMatchRun(text, index, endIndex, familyFonts.italicFonts);
296 }
297
298 if (fontMatch == null || fontMatch.fontInfo == null)
299 {
300 fontMatch = fontMatchRun(text, index, endIndex, familyFonts.normalFonts);
301 }
302
303 if (fontMatch.fontInfo != null)
304 {
305
306 addFontRun(newRuns, attributes, index, fontMatch.endIndex, fontMatch.fontInfo);
307 }
308 else
309 {
310
311 hadUnmatched = true;
312 }
313 index = fontMatch.endIndex;
314 }
315 while(index < endIndex);
316
317 if (hadUnmatched)
318 {
319
320
321 addFallbackRun(newRuns, attributes, startIndex, endIndex, familyFonts);
322 }
323 }
324
325 protected void copyRun(List<Run> newRuns, Map<Attribute, Object> attributes,
326 int startIndex, int endIndex)
327 {
328 Map<Attribute, Object> newAttributes = Collections.unmodifiableMap(attributes);
329 Run newRun = new Run(newAttributes, startIndex, endIndex);
330 newRuns.add(newRun);
331 }
332
333 protected void addFallbackRun(List<Run> newRuns, Map<Attribute, Object> attributes,
334 int startIndex, int endIndex, FamilyFonts familyFonts)
335 {
336 Map<Attribute, Object> newAttributes;
337 if (familyFonts.fontSet.getPrimaryFamily() != null)
338 {
339
340
341 newAttributes = new HashMap<Attribute, Object>(attributes);
342 String primaryFamilyName = familyFonts.fontSet.getPrimaryFamily().getFontFamily().getName();
343 newAttributes.put(TextAttribute.FAMILY, primaryFamilyName);
344 }
345 else
346 {
347
348 newAttributes = Collections.unmodifiableMap(attributes);
349 }
350 Run newRun = new Run(newAttributes, startIndex, endIndex);
351 newRuns.add(newRun);
352 }
353
354 protected void addFontRun(List<Run> newRuns, Map<Attribute, Object> attributes,
355 int startIndex, int endIndex, FontInfo fontInfo)
356 {
357
358 Map<Attribute, Object> newAttributes = new AdditionalEntryMap<Attribute, Object>(
359 attributes, JRTextAttribute.FONT_INFO, fontInfo);
360 Run newRun = new Run(newAttributes, startIndex, endIndex);
361 newRuns.add(newRun);
362 }
363
364 protected static class FontMatch
365 {
366 FontInfo fontInfo;
367 int endIndex;
368 }
369
370 protected FontMatch fontMatchRun(String text, int startIndex, int endIndex, List<Face> fonts)
371 {
372 LinkedList<Face> validFonts = new LinkedList<Face>(fonts);
373 Face lastValid = null;
374 int charIndex = startIndex;
375 int nextCharIndex = charIndex;
376 while (charIndex < endIndex)
377 {
378 char textChar = text.charAt(charIndex);
379 nextCharIndex = charIndex + 1;
380
381 int codePoint;
382 if (Character.isHighSurrogate(textChar))
383 {
384 if (charIndex + 1 >= endIndex)
385 {
386
387 break;
388 }
389
390 char nextChar = text.charAt(charIndex + 1);
391 if (!Character.isLowSurrogate(nextChar))
392 {
393
394 break;
395 }
396 codePoint = Character.toCodePoint(textChar, nextChar);
397 ++nextCharIndex;
398 }
399 else
400 {
401 codePoint = textChar;
402 }
403
404 for (ListIterator<Face> fontIt = validFonts.listIterator(); fontIt.hasNext();)
405 {
406 Face face = fontIt.next();
407
408 if (!face.supports(codePoint))
409 {
410 fontIt.remove();
411 }
412 }
413
414 if (validFonts.isEmpty())
415 {
416 break;
417 }
418
419 lastValid = validFonts.getFirst();
420 charIndex = nextCharIndex;
421 }
422
423 FontMatch fontMatch = new FontMatch();
424 fontMatch.endIndex = lastValid == null ? nextCharIndex : charIndex;
425 fontMatch.fontInfo = lastValid == null ? null : lastValid.fontInfo;
426 return fontMatch;
427 }
428
429 private FamilyFonts getFamilyFonts(Map<Attribute, Object> attributes, Locale locale)
430 {
431 String family = (String) attributes.get(TextAttribute.FAMILY);
432 return getFamilyFonts(family, locale);
433 }
434
435 protected FamilyFonts getFamilyFonts(String name, Locale locale)
436 {
437 Pair<String, Locale> key = new Pair<String, Locale>(name, locale);
438 FamilyFonts fonts = familyFonts.get(key);
439 if (fonts == null)
440 {
441 fonts = loadFamilyFonts(name, locale);
442 familyFonts.put(key, fonts);
443 }
444 return fonts;
445 }
446
447 protected FamilyFonts loadFamilyFonts(String name, Locale locale)
448 {
449 if (name == null)
450 {
451 return NULL_FAMILY_FONTS;
452 }
453
454 FontInfo fontInfo = fontUtil.getFontInfo(name, locale);
455 if (fontInfo != null)
456 {
457
458 return NULL_FAMILY_FONTS;
459 }
460
461 FontSetInfo fontSetInfo = fontUtil.getFontSetInfo(name, locale, ignoreMissingFonts);
462 if (fontSetInfo == null)
463 {
464 return NULL_FAMILY_FONTS;
465 }
466
467 return new FamilyFonts(fontSetInfo);
468 }
469
470 private static FamilyFonts NULL_FAMILY_FONTS = new FamilyFonts(null);
471
472 private static class FamilyFonts
473 {
474 FontSetInfo fontSet;
475 List<Face> normalFonts;
476 List<Face> boldFonts;
477 List<Face> italicFonts;
478 List<Face> boldItalicFonts;
479
480 public FamilyFonts(FontSetInfo fontSet)
481 {
482 this.fontSet = fontSet;
483
484 init();
485 }
486
487 private void init()
488 {
489 if (fontSet == null)
490 {
491 return;
492 }
493
494 List<FontSetFamilyInfo> families = fontSet.getFamilies();
495 this.normalFonts = new ArrayList<Face>(families.size());
496 this.boldFonts = new ArrayList<Face>(families.size());
497 this.italicFonts = new ArrayList<Face>(families.size());
498 this.boldItalicFonts = new ArrayList<Face>(families.size());
499
500 for (FontSetFamilyInfo fontSetFamily : families)
501 {
502 Family family = new Family(fontSetFamily);
503
504 FontFamily fontFamily = fontSetFamily.getFontFamily();
505 if (fontFamily.getNormalFace() != null && fontFamily.getNormalFace().getFont() != null)
506 {
507 normalFonts.add(new Face(family, fontFamily.getNormalFace(), Font.PLAIN));
508 }
509 if (fontFamily.getBoldFace() != null && fontFamily.getBoldFace().getFont() != null)
510 {
511 boldFonts.add(new Face(family, fontFamily.getBoldFace(), Font.BOLD));
512 }
513 if (fontFamily.getItalicFace() != null && fontFamily.getItalicFace().getFont() != null)
514 {
515 italicFonts.add(new Face(family, fontFamily.getItalicFace(), Font.ITALIC));
516 }
517 if (fontFamily.getBoldItalicFace() != null && fontFamily.getBoldItalicFace().getFont() != null)
518 {
519 boldItalicFonts.add(new Face(family, fontFamily.getBoldItalicFace(), Font.BOLD | Font.ITALIC));
520 }
521 }
522 }
523
524 public boolean needsToResolveFonts(String exporterKey)
525 {
526 return fontSet != null && (exporterKey == null
527 || fontSet.getFontSet().getExportFont(exporterKey) == null);
528 }
529 }
530
531 private static class Family
532 {
533 final FontSetFamilyInfo fontFamily;
534 CharScriptsSet scriptsSet;
535
536 public Family(FontSetFamilyInfo fontSetFamily)
537 {
538 this.fontFamily = fontSetFamily;
539 initScripts();
540 }
541
542 private void initScripts()
543 {
544 List<String> includedScripts = fontFamily.getFontSetFamily().getIncludedScripts();
545 List<String> excludedScripts = fontFamily.getFontSetFamily().getExcludedScripts();
546 if ((includedScripts != null && !includedScripts.isEmpty())
547 || (excludedScripts != null && !excludedScripts.isEmpty()))
548 {
549 scriptsSet = new CharScriptsSet(includedScripts, excludedScripts);
550 }
551 }
552
553 public boolean includesCharacter(int codePoint)
554 {
555 return scriptsSet == null || scriptsSet.includesCharacter(codePoint);
556 }
557 }
558
559 private static class Face
560 {
561 final Family family;
562 final FontInfo fontInfo;
563
564 final CharPredicateCache cache;
565
566 public Face(Family family, FontFace fontFace, int style)
567 {
568 this.family = family;
569 this.fontInfo = new FontInfo(family.fontFamily.getFontFamily(), fontFace, style);
570 this.cache = new CharPredicateCache();
571 }
572
573 public boolean supports(int code)
574 {
575 Result cacheResult = cache.getCached(code);
576 boolean supports;
577 switch (cacheResult)
578 {
579 case TRUE:
580 supports = true;
581 break;
582 case FALSE:
583 supports = false;
584 break;
585 case NOT_FOUND:
586 supports = supported(code);
587 cache.set(code, supports);
588 break;
589 case NOT_CACHEABLE:
590 default:
591 supports = supported(code);
592 break;
593 }
594 return supports;
595 }
596
597 protected boolean supported(int code)
598 {
599 return family.includesCharacter(code)
600 && fontInfo.getFontFace().getFont().canDisplay(code);
601 }
602 }
603 }
604