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.fonts;
25
26 import java.awt.Font;
27 import java.awt.font.TextAttribute;
28 import java.text.AttributedCharacterIterator.Attribute;
29 import java.util.ArrayList;
30 import java.util.Collection;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.TreeSet;
40
41 import org.apache.commons.logging.Log;
42 import org.apache.commons.logging.LogFactory;
43
44 import net.sf.jasperreports.engine.JRFont;
45 import net.sf.jasperreports.engine.JRRuntimeException;
46 import net.sf.jasperreports.engine.JasperReportsContext;
47 import net.sf.jasperreports.engine.util.JRFontNotFoundException;
48 import net.sf.jasperreports.engine.util.JRGraphEnvInitializer;
49 import net.sf.jasperreports.engine.util.JRTextAttribute;
50
51
52 /**
53  * @author Teodor Danciu (teodord@users.sourceforge.net)
54  */

55 public final class FontUtil
56 {
57     private static final Log log = LogFactory.getLog(FontUtil.class);
58     public static final String EXCEPTION_MESSAGE_KEY_NULL_FONT = "engine.fonts.null.font";
59     public static final String EXCEPTION_MESSAGE_KEY_FONT_SET_FAMILY_NOT_FOUND = "util.font.set.family.not.found";
60
61     private JasperReportsContext jasperReportsContext;
62
63
64     /**
65      *
66      */

67     private FontUtil(JasperReportsContext jasperReportsContext)
68     {
69         this.jasperReportsContext = jasperReportsContext;
70     }
71     
72     
73     /**
74      *
75      */

76     public static FontUtil getInstance(JasperReportsContext jasperReportsContext)
77     {
78         return new FontUtil(jasperReportsContext);
79     }
80     
81     
82     /**
83      *.
84      */
 //FIXMECONTEXT this should no longer be a thread local
85     private static final InheritableThreadLocal<Set<String>> threadMissingFontsCache = new InheritableThreadLocal<Set<String>>()
86     {
87         @Override
88         protected Set<String> initialValue() {
89             return new HashSet<String>();
90         }
91     };
92     
93     /**
94      *
95      */

96     public static void copyNonNullOwnProperties(JRFont srcFont, JRFont destFont)
97     {
98         if(srcFont != null && destFont != null)
99         {
100             if (srcFont.getOwnFontName() != null)
101             {
102                 destFont.setFontName(srcFont.getOwnFontName());
103             }
104             if (srcFont.isOwnBold() != null)
105             {
106                 destFont.setBold(srcFont.isOwnBold());
107             }
108             if (srcFont.isOwnItalic() != null)
109             {
110                 destFont.setItalic(srcFont.isOwnItalic());
111             }
112             if (srcFont.isOwnUnderline() != null)
113             {
114                 destFont.setUnderline(srcFont.isOwnUnderline());
115             }
116             if (srcFont.isOwnStrikeThrough() != null)
117             {
118                 destFont.setStrikeThrough(srcFont.isOwnStrikeThrough());
119             }
120             if (srcFont.getOwnFontsize() != null)
121             {
122                 destFont.setFontSize(srcFont.getOwnFontsize());
123             }
124             if (srcFont.getOwnPdfFontName() != null)
125             {
126                 destFont.setPdfFontName(srcFont.getOwnPdfFontName());
127             }
128             if (srcFont.getOwnPdfEncoding() != null)
129             {
130                 destFont.setPdfEncoding(srcFont.getOwnPdfEncoding());
131             }
132             if (srcFont.isOwnPdfEmbedded() != null)
133             {
134                 destFont.setPdfEmbedded(srcFont.isOwnPdfEmbedded());
135             }
136         }
137     }
138     
139
140     /**
141      *
142      */

143     public Map<Attribute,Object> getAttributesWithoutAwtFont(Map<Attribute,Object> attributes, JRFont font)
144     {
145         attributes.put(TextAttribute.FAMILY, font.getFontName());
146
147         attributes.put(TextAttribute.SIZE, font.getFontsize());
148
149         if (font.isBold())
150         {
151             attributes.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
152         }
153         if (font.isItalic())
154         {
155             attributes.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
156         }
157         if (font.isUnderline())
158         {
159             attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
160         }
161         if (font.isStrikeThrough())
162         {
163             attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
164         }
165         
166         attributes.put(JRTextAttribute.PDF_FONT_NAME, font.getPdfFontName());
167         attributes.put(JRTextAttribute.PDF_ENCODING, font.getPdfEncoding());
168
169         if (font.isPdfEmbedded())
170         {
171             attributes.put(JRTextAttribute.IS_PDF_EMBEDDED, Boolean.TRUE);
172         }
173
174         return attributes;
175     }
176
177
178     /**
179      * Returns font information containing the font family, font face and font style.
180      * 
181      * @param name the font family or font face name
182      * @param ignoreCase the flag to specify if family names or face names are searched by ignoring case or not
183      * @param locale the locale
184      * @return a font info object
185      */

186     public FontInfo getFontInfo(String name, boolean ignoreCase, Locale locale)
187     {
188         FontInfo awtFamilyMatchFontInfo = null;
189
190         //FIXMEFONT do some cache
191         List<FontFamily> families = jasperReportsContext.getExtensions(FontFamily.class);
192         for (Iterator<FontFamily> itf = families.iterator(); itf.hasNext();)
193         {
194             FontFamily family = itf.next();
195             if (locale == null || family.supportsLocale(locale))
196             {
197                 if (equals(name, family.getName(), ignoreCase))
198                 {
199                     return new FontInfo(family, null, Font.PLAIN);
200                 }
201                 
202                 FontFace face = family.getNormalFace();
203                 if (face != null)
204                 {
205                     if (equals(name, face.getName(), ignoreCase))
206                     {
207                         return new FontInfo(family, face, Font.PLAIN);
208                     }
209                     else if (
210                         awtFamilyMatchFontInfo == null
211                         && face.getFont() != null
212                         && equals(name, face.getFont().getFamily(), ignoreCase)
213                         )
214                     {
215                         awtFamilyMatchFontInfo = new FontInfo(family, face, Font.PLAIN);
216                     }
217                 }
218
219                 face = family.getBoldFace();
220                 if (face != null)
221                 {
222                     if (equals(name, face.getName(), ignoreCase))
223                     {
224                         return new FontInfo(family, face, Font.BOLD);
225                     }
226                     else if (
227                         awtFamilyMatchFontInfo == null
228                         && face.getFont() != null
229                         && equals(name, face.getFont().getFamily(), ignoreCase)
230                         )
231                     {
232                         awtFamilyMatchFontInfo = new FontInfo(family, face, Font.BOLD);
233                     }
234                 }
235
236                 face = family.getItalicFace();
237                 if (face != null)
238                 {
239                     if (equals(name, face.getName(), ignoreCase))
240                     {
241                         return new FontInfo(family, face, Font.ITALIC);
242                     }
243                     else if (
244                         awtFamilyMatchFontInfo == null
245                         && face.getFont() != null
246                         && equals(name, face.getFont().getFamily(), ignoreCase)
247                         )
248                     {
249                         awtFamilyMatchFontInfo = new FontInfo(family, face, Font.ITALIC);
250                     }
251                 }
252
253                 face = family.getBoldItalicFace();
254                 if (face != null)
255                 {
256                     if (equals(name, face.getName(), ignoreCase))
257                     {
258                         return new FontInfo(family, face, Font.BOLD | Font.ITALIC);
259                     }
260                     else if (
261                         awtFamilyMatchFontInfo == null
262                         && face.getFont() != null
263                         && equals(name, face.getFont().getFamily(), ignoreCase)
264                         )
265                     {
266                         awtFamilyMatchFontInfo = new FontInfo(family, face, Font.BOLD | Font.ITALIC);
267                     }
268                 }
269             }
270         }
271         
272         return awtFamilyMatchFontInfo;
273     }
274
275
276     private static boolean equals(String value1, String value2, boolean ignoreCase)
277     {
278         return ignoreCase ? value1.equalsIgnoreCase(value2) : value1.equals(value2);
279     }
280     
281     
282     /**
283      * Returns font information containing the font family, font face and font style, searching for names case sensitive.
284      * 
285      * @param name the font family or font face name
286      * @param locale the locale
287      * @return a font info object
288      */

289     public FontInfo getFontInfo(String name, Locale locale)
290     {
291         return getFontInfo(name, false, locale);
292     }
293
294
295     /**
296      * Returns font information containing the font family, font face and font style, searching for names case insensitive.
297      * 
298      * @param name the font family or font face name
299      * @param locale the locale
300      * @return a font info object
301      * @deprecated Replaced by {@link #getFontInfo(String, boolean, Locale)}.
302      */

303     public FontInfo getFontInfoIgnoreCase(String name, Locale locale)
304     {
305         return getFontInfo(name, true, locale);
306     }
307
308
309     public FontSetInfo getFontSetInfo(String name, Locale locale, boolean ignoreMissingFonts)
310     {
311         //FIXMEFONT do some cache
312         List<FontFamily> allFontFamilies = jasperReportsContext.getExtensions(FontFamily.class);
313         HashMap<String, FontFamily> fontFamilies = new HashMap<String, FontFamily>(allFontFamilies.size() * 4 / 3, .75f);
314         for (FontFamily family : allFontFamilies)
315         {
316             if (family.getName() != null
317                     && (locale == null || family.supportsLocale(locale)))
318             {
319                 fontFamilies.put(family.getName(), family);
320             }
321         }
322         
323         Map<String, FontSetFamilyInfo> setFamilyInfos = new LinkedHashMap<String, FontSetFamilyInfo>();
324         List<FontSet> allSets = jasperReportsContext.getExtensions(FontSet.class);
325         FontSet foundFontSet = null;
326         FontSetFamilyInfo primaryFamily = null;
327         for (FontSet fontSet : allSets)
328         {
329             if (name.equals(fontSet.getName()))
330             {
331                 foundFontSet = fontSet;
332                 
333                 List<FontSetFamily> setFamilies = fontSet.getFamilies();
334                 for (FontSetFamily fontSetFamily : setFamilies)
335                 {
336                     FontFamily fontFamily = fontFamilies.get(fontSetFamily.getFamilyName());
337                     if (fontFamily != null)
338                     {
339                         FontSetFamilyInfo familyInfo = new FontSetFamilyInfo(fontSetFamily, fontFamily);
340                         setFamilyInfos.put(fontSetFamily.getFamilyName(), familyInfo);
341                         
342                         if (fontSetFamily.isPrimary())
343                         {
344                             primaryFamily = familyInfo;
345                         }
346                     }
347                     else 
348                     {
349                         if (ignoreMissingFonts)
350                         {
351                             if (log.isWarnEnabled())
352                             {
353                                 log.warn("Font family " + fontSetFamily.getFamilyName()
354                                         + " was not found for font set " + name);
355                             }
356                         }
357                         else
358                         {
359                             throw new JRRuntimeException(EXCEPTION_MESSAGE_KEY_FONT_SET_FAMILY_NOT_FOUND,
360                                     new Object[]{fontSetFamily.getFamilyName(), name});
361                         }
362                     }
363                 }
364             }
365         }
366         
367         if (foundFontSet == null)
368         {
369             return null;
370         }
371         
372         //TODO lucianc handle sets with no families
373         List<FontSetFamilyInfo> familyInfoList = new ArrayList<FontSetFamilyInfo>(setFamilyInfos.values());
374         if (primaryFamily == null && !familyInfoList.isEmpty())
375         {
376             primaryFamily = familyInfoList.get(0);
377         }
378         return new FontSetInfo(foundFontSet, primaryFamily, familyInfoList);
379     }
380     
381     public String getExportFontFamily(String name, Locale locale, String exporterKey)
382     {
383         //FIXMEFONT do some cache
384         FontInfo fontInfo = getFontInfo(name, locale);
385         if (fontInfo != null)
386         {
387             FontFamily family = fontInfo.getFontFamily();
388             String exportFont = family.getExportFont(exporterKey);
389             return exportFont == null ? name : exportFont;
390         }
391         
392         FontSetInfo fontSetInfo = getFontSetInfo(name, locale, true);
393         if (fontSetInfo != null)
394         {
395             String exportFont = fontSetInfo.getFontSet().getExportFont(exporterKey);
396             //TODO also look at the primary family?
397             return exportFont == null ? name : exportFont;
398         }
399         
400         return name;
401     }
402
403     /**
404      * Returns the font family names available through extensions, in alphabetical order.
405      */

406     public Collection<String> getFontFamilyNames()
407     {
408         TreeSet<String> familyNames = new TreeSet<String>();//FIXMEFONT use collator for order?
409         //FIXMEFONT do some cache
410         collectFontFamilyNames(familyNames);
411         return familyNames;
412     }
413
414     protected void collectFontFamilyNames(Collection<String> names)
415     {
416         List<FontFamily> families = jasperReportsContext.getExtensions(FontFamily.class);
417         for (Iterator<FontFamily> itf = families.iterator(); itf.hasNext();)
418         {
419             FontFamily family = itf.next();
420             if (family.isVisible())
421             {
422                 names.add(family.getName());
423             }
424         }
425     }
426
427     /**
428      * Returns the font names available through extensions, in alphabetical order.
429      * 
430      * @return the list of font names provided by extensions
431      */

432     public Collection<String> getFontNames()
433     {
434         TreeSet<String> fontNames = new TreeSet<String>();//FIXMEFONT use collator for order?
435         //FIXMEFONT do some cache
436         collectFontFamilyNames(fontNames);
437         collectFontSetNames(fontNames);
438         return fontNames;
439     }
440
441     protected void collectFontSetNames(Collection<String> names)
442     {
443         List<FontSet> fontSets = jasperReportsContext.getExtensions(FontSet.class);
444         for (Iterator<FontSet> itf = fontSets.iterator(); itf.hasNext();)
445         {
446             FontSet fontSet = itf.next();
447             names.add(fontSet.getName());
448         }
449     }
450
451     /**
452      * @deprecated Replaced by {@link #getAwtFontFromBundles(String, intfloat, Locale, boolean)}.
453      */

454     public Font getAwtFontFromBundles(String name, int style, int size, Locale locale, boolean ignoreMissingFont)
455     {
456         return getAwtFontFromBundles(name, style, (float)size, locale, ignoreMissingFont);
457     }
458
459
460     /**
461      * Calls {@link #getAwtFontFromBundles(boolean, String, intfloat, Locale, boolean)} with the ignoreCase parameter set to false.
462      */

463     public Font getAwtFontFromBundles(String name, int style, float size, Locale locale, boolean ignoreMissingFont)
464     {
465         return getAwtFontFromBundles(false, name, style, size, locale, ignoreMissingFont);
466     }
467
468
469     /**
470      *
471      */

472     public Font getAwtFontFromBundles(boolean ignoreCase, String name, int style, float size, Locale locale, boolean ignoreMissingFont)
473     {
474         Font awtFont = null;
475         FontInfo fontInfo = ignoreCase ? getFontInfoIgnoreCase(name, locale) : getFontInfo(name, locale);
476         
477         if (fontInfo != null)
478         {
479             awtFont = getAwtFont(fontInfo, style, size, ignoreMissingFont);
480         }
481         
482         return awtFont;
483     }
484
485
486     protected Font getAwtFont(FontInfo fontInfo, int style, float size, boolean ignoreMissingFont)
487     {
488         @SuppressWarnings("unused")
489         int faceStyle = Font.PLAIN;
490         FontFamily family = fontInfo.getFontFamily();
491         FontFace face = fontInfo.getFontFace();
492         if (face == null)
493         {
494             if (((style & Font.BOLD) > 0) && ((style & Font.ITALIC) > 0))
495             {
496                 face = family.getBoldItalicFace();
497                 faceStyle = Font.BOLD | Font.ITALIC;
498             }
499             
500             if ((face == null || face.getFont() == null) && ((style & Font.BOLD) > 0))
501             {
502                 face = family.getBoldFace();
503                 faceStyle = Font.BOLD;
504             }
505             
506             if ((face == null || face.getFont() == null) && ((style & Font.ITALIC) > 0))
507             {
508                 face = family.getItalicFace();
509                 faceStyle = Font.ITALIC;
510             }
511             
512             if (face == null || face.getFont() == null)
513             {
514                 face = family.getNormalFace();
515                 faceStyle = Font.PLAIN;
516             }
517                 
518 //            if (face == null)
519 //            {
520 //                throw new JRRuntimeException("Font family '" + family.getName() + "' does not have the normal font face.");
521 //            }
522         }
523         else
524         {
525             faceStyle = fontInfo.getStyle();
526         }
527
528         Font awtFont;
529         if (face == null || face.getFont() == null)
530         {
531             // None of the family's font faces was found to match, neither by name, nor by style and the font family does not even specify a normal face font.
532             // In such case, we take the family name and consider it as JVM available font name.
533             checkAwtFont(family.getName(), ignoreMissingFont);
534             
535             awtFont = new Font(family.getName(), style, (int)size);
536             awtFont = awtFont.deriveFont(size);
537         }
538         else
539         {
540             awtFont = face.getFont();
541             if (awtFont == null)
542             {
543                 throw 
544                     new JRRuntimeException(
545                         EXCEPTION_MESSAGE_KEY_NULL_FONT,
546                         new Object[]{face.getName(), family.getName()});
547             }
548
549             //deriving with style and size in one call, because deriving with size and then style loses the float size
550             awtFont = awtFont.deriveFont(style, size);// & ~faceStyle);
551         }
552         return awtFont;
553     }
554
555
556     public Font getAwtFontFromBundles(AwtFontAttribute fontAttribute, int style, float size, Locale locale, boolean ignoreMissingFont)
557     {
558         FontInfo fontInfo = fontAttribute.getFontInfo();
559         if (fontInfo == null)
560         {
561             fontInfo = getFontInfo(fontAttribute.getFamily(), locale);
562         }
563         
564         Font awtFont = null;
565         if (fontInfo != null)
566         {
567             awtFont = getAwtFont(fontInfo, style, size, ignoreMissingFont);
568         }
569         return awtFont;
570     }
571
572     
573     /**
574      *
575      */
 //FIXMECONTEXT check how to make this cache effective again
576     public void resetThreadMissingFontsCache()
577     {
578         threadMissingFontsCache.set(new HashSet<String>());
579     }
580     
581     
582     /**
583      *
584      */

585     public void checkAwtFont(String name, boolean ignoreMissingFont)
586     {
587         if (!JRGraphEnvInitializer.isAwtFontAvailable(name))
588         {
589             if (ignoreMissingFont)
590             {
591                 Set<String> missingFontNames = threadMissingFontsCache.get();
592                 if (!missingFontNames.contains(name))
593                 {
594                     missingFontNames.add(name);
595                     if (log.isWarnEnabled())
596                     {
597                         log.warn("Font '" + name + "' is not available to the JVM. For more details, see http://jasperreports.sourceforge.net/api/net/sf/jasperreports/engine/util/JRFontNotFoundException.html");
598                     }
599                 }
600             }
601             else
602             {
603                 throw new JRFontNotFoundException(name);
604             }
605         }
606     }
607
608     
609     /**
610      * Returns a java.awt.Font instance by converting a JRFont instance.
611      * Mostly used in combination with third-party visualization packages such as JFreeChart (for chart themes).
612      * Unless the font parameter is nullthis method always returns a non-null AWT font, regardless whether it was
613      * found in the font extensions or not. This is because we do need a font to draw with and there is no point
614      * in raising a font missing exception here, as it is not JasperReports who does the drawing. 
615      */

616     public Font getAwtFont(JRFont font, Locale locale)
617     {
618         if (font == null)
619         {
620             return null;
621         }
622         
623         // ignoring missing font as explained in the Javadoc
624         Font awtFont = 
625             getAwtFontFromBundles(
626                 font.getFontName(), 
627                 ((font.isBold()?Font.BOLD:Font.PLAIN)|(font.isItalic()?Font.ITALIC:Font.PLAIN)), 
628                 font.getFontsize(),
629                 locale,
630                 true
631                 );
632         
633         if (awtFont == null)
634         {
635             awtFont = new Font(getAttributesWithoutAwtFont(new HashMap<Attribute,Object>(), font));
636         }
637         else
638         {
639             // add underline and strikethrough attributes since these are set at
640             // style/font level
641             Map<Attribute, Object> attributes = new HashMap<Attribute, Object>();
642             if (font.isUnderline())
643             {
644                 attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
645             }
646             if (font.isStrikeThrough())
647             {
648                 attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
649             }
650             
651             if (!attributes.isEmpty())
652             {
653                 awtFont = awtFont.deriveFont(attributes);
654             }
655         }
656         
657         return awtFont;
658     }
659     
660     
661     private FontUtil()
662     {
663     }
664 }
665