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.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 /**
57  * @author Teodor Danciu (teodord@users.sourceforge.net)
58  */

59 public class JRStyledTextUtil
60 {
61     //private final JasperReportsContext jasperReportsContext;
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     /**
70      *
71      */

72     private JRStyledTextUtil(JasperReportsContext jasperReportsContext)
73     {
74         //this.jasperReportsContext = jasperReportsContext;
75         this.allSelector = JRStyledTextAttributeSelector.getAllSelector(jasperReportsContext);
76         fontUtil = FontUtil.getInstance(jasperReportsContext);
77         //FIXME read from report/element
78         ignoreMissingFonts = JRPropertiesUtil.getInstance(jasperReportsContext).getBooleanProperty(
79                 JRStyledText.PROPERTY_AWT_IGNORE_MISSING_FONT);
80     }
81     
82     /**
83      *
84      */

85     public static JRStyledTextUtil getInstance(JasperReportsContext jasperReportsContext)
86     {
87         return new JRStyledTextUtil(jasperReportsContext);
88     }
89     
90     /**
91      *
92      */

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     /**
127      *
128      */

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         //TODO introduce an option to modify the existing object
181         //TODO lucianc trace logging
182         String text = styledText.getText();
183         List<Run> runs = styledText.getRuns();
184         List<Run> newRuns = null;
185         
186         if (runs.size() == 1)
187         {
188             //treating separately to avoid styledText.getAttributedString() because it's slow
189             Map<Attribute, Object> attributes = runs.get(0).attributes;
190             FamilyFonts families = getFamilyFonts(attributes, locale);
191             if (families.needsToResolveFonts(exporterKey))//TODO lucianc check for single family
192             {
193                 newRuns = new ArrayList<Run>(runs.size() + 2);
194                 matchFonts(text, 0, styledText.length(), attributes, families, newRuns);
195             }
196         }
197         else
198         {
199             //quick test to avoid styledText.getAttributedString() when not needed
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                         //not a font set, copying the run
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             //no changes
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                 //we have a font that matched a part of the text
306                 addFontRun(newRuns, attributes, index, fontMatch.endIndex, fontMatch.fontInfo);
307             }
308             else
309             {
310                 //we stopped at the first character
311                 hadUnmatched = true;
312             }
313             index = fontMatch.endIndex;
314         }
315         while(index < endIndex);
316         
317         if (hadUnmatched)
318         {
319             //we have unmatched characters, adding a run with the primary font for the entire chunk.
320             //we're relying on the JRStyledText to apply the runs in the reverse order.
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             //using the primary font as fallback for characters that are not found in any fonts
340             //TODO lucianc enhance AdditionalEntryMap to support overwriting an entry
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             //not a normal case, leaving the font family as is
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         //directly putting the FontInfo as an attribute
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                     //isolated high surrogate, not attempting to match fonts
387                     break;
388                 }
389                 
390                 char nextChar = text.charAt(charIndex + 1);
391                 if (!Character.isLowSurrogate(nextChar))
392                 {
393                     //unpaired high surrogate, not attempting to match fonts
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             //we found a font, not looking for font sets
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         //TODO share caches across fills/exports
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