/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ /* * Created on Apr 17, 2005 */ package org.lobobrowser.util.gui; import java.awt.Font; import java.awt.GraphicsEnvironment; import java.awt.font.TextAttribute; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.text.StyleContext; import org.lobobrowser.util.Strings; /** Note: Undocumented class? */ //import sun.font.FontManager; /** * @author J. H. S. */ public class FontFactory { private static final Logger logger = Logger.getLogger(FontFactory.class.getName()); private static final boolean loggableFine = logger.isLoggable(Level.FINE); private static final FontFactory instance = new FontFactory(); private final Set<String> fontFamilies = new HashSet<>(40); private final Map<FontKey, Font> fontMap = new HashMap<>(50); /** * */ private FontFactory() { final boolean liflag = loggableFine; final String[] ffns = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); final Set<String> fontFamilies = this.fontFamilies; synchronized (this) { for (final String ffn : ffns) { if (liflag) { logger.fine("FontFactory(): family=" + ffn); } fontFamilies.add(ffn.toLowerCase()); } } } public static final FontFactory getInstance() { return instance; } private final Map<String, Font> registeredFonts = new HashMap<>(0); /** * Registers a font family. It does not close the stream provided. Fonts * should be registered before the renderer has a chance to cache document * font specifications. * * @param fontName * The name of a font as it would appear in a font-family * specification. * @param fontFormat * Should be {@link Font#TRUETYPE_FONT}. */ public void registerFont(final String fontName, final int fontFormat, final java.io.InputStream fontStream) throws java.awt.FontFormatException, java.io.IOException { final Font f = Font.createFont(fontFormat, fontStream); synchronized (this) { this.registeredFonts.put(fontName.toLowerCase(), f); } } /** * Unregisters a font previously registered with * {@link #registerFont(String, int, java.io.InputStream)}. * * @param fontName * The font name to be removed. */ public void unregisterFont(final String fontName) { synchronized (this) { this.registeredFonts.remove(fontName.toLowerCase()); } } public Font getFont(final String fontFamily, final String fontStyle, final String fontVariant, final String fontWeight, final float fontSize, final Set<Locale> locales, final Integer superscript) { final FontKey key = new FontKey(fontFamily, fontStyle, fontVariant, fontWeight, fontSize, locales, superscript); synchronized (this) { Font font = this.fontMap.get(key); if (font == null) { font = this.createFont(key); this.fontMap.put(key, font); } return font; } } private String defaultFontName = "SansSerif"; public String getDefaultFontName() { return defaultFontName; } /** * Sets the default font name to be used when a name is unrecognized or when a * font is determined not to be capable of diplaying characters from a given * language. This should be the name of a font that can display unicode text * across all or most languages. * * @param defaultFontName * The name of a font. */ public void setDefaultFontName(final String defaultFontName) { if (defaultFontName == null) { throw new IllegalArgumentException("defaultFontName cannot be null"); } this.defaultFontName = defaultFontName; } private final Font createFont(final FontKey key) { final Font font = createFont_Impl(key); return superscriptFont(font, key.superscript); } public static Font superscriptFont(final Font baseFont, final Integer newSuperscript) { if (newSuperscript == null) { return baseFont; } Integer fontSuperScript = (Integer) baseFont.getAttributes().get(TextAttribute.SUPERSCRIPT); if (fontSuperScript == null) { fontSuperScript = new Integer(0); } if (fontSuperScript.equals(newSuperscript)) { return baseFont; } else { final Map<TextAttribute, Integer> additionalAttributes = new HashMap<>(); additionalAttributes.put(TextAttribute.SUPERSCRIPT, newSuperscript); return baseFont.deriveFont(additionalAttributes); } } private final Font createFont_Impl(final FontKey key) { final String fontNames = key.fontFamily; String matchingFace = null; final Set<String> fontFamilies = this.fontFamilies; final Map<String, Font> registeredFonts = this.registeredFonts; Font baseFont = null; if (fontNames != null) { final StringTokenizer tok = new StringTokenizer(fontNames, ","); while (tok.hasMoreTokens()) { final String face = Strings.unquoteSingle(tok.nextToken().trim()); final String faceTL = face.toLowerCase(); if (registeredFonts.containsKey(faceTL)) { baseFont = registeredFonts.get(faceTL); break; } else if (fontFamilies.contains(faceTL)) { matchingFace = faceTL; break; } else if ("monospace".equals(faceTL)){ baseFont = Font.decode("monospaced"); } } } int fontStyle = Font.PLAIN; if ("italic".equalsIgnoreCase(key.fontStyle)) { fontStyle |= Font.ITALIC; } if ("bold".equalsIgnoreCase(key.fontWeight) || "bolder".equalsIgnoreCase(key.fontWeight)) { fontStyle |= Font.BOLD; } if (baseFont != null) { return baseFont.deriveFont(fontStyle, key.fontSize); } else if (matchingFace != null) { final Font font = createFont(matchingFace, fontStyle, Math.round(key.fontSize)); final Set<Locale> locales = key.locales; if (locales == null) { final Locale locale = Locale.getDefault(); if (font.canDisplayUpTo(locale.getDisplayLanguage(locale)) == -1) { return font; } } else { final Iterator<Locale> i = locales.iterator(); boolean allMatch = true; while (i.hasNext()) { final Locale locale = i.next(); if (font.canDisplayUpTo(locale.getDisplayLanguage(locale)) != -1) { allMatch = false; break; } } if (allMatch) { return font; } } // Otherwise, fall through. } // Last resort: return createFont(this.defaultFontName, fontStyle, Math.round(key.fontSize)); } private static Font createFont(final String name, final int style, final int size) { return StyleContext.getDefaultStyleContext().getFont(name, style, size); // Proprietary Sun API. Maybe shouldn't use it. Works well for Chinese. // return FontManager.getCompositeFontUIResource(new Font(name, style, // size)); } private static class FontKey { public final String fontFamily; public final String fontStyle; public final String fontVariant; public final String fontWeight; public final float fontSize; public final Set<Locale> locales; public final Integer superscript; /** * @param fontFamily * @param fontStyle * @param fontVariant * @param fontWeight * @param fontSize */ public FontKey(final String fontFamily, final String fontStyle, final String fontVariant, final String fontWeight, final float fontSize, final Set<Locale> locales, final Integer superscript) { this.fontFamily = fontFamily == null ? null : fontFamily.intern(); this.fontStyle = fontStyle == null ? null : fontStyle.intern(); this.fontVariant = fontVariant == null ? null : fontVariant.intern(); this.fontWeight = fontWeight == null ? null : fontWeight.intern(); this.fontSize = fontSize; this.locales = locales; this.superscript = superscript; } @Override public boolean equals(final Object other) { if (other == this) { // Quick check. return true; } FontKey ors; try { ors = (FontKey) other; } catch (final ClassCastException cce) { // Not expected return false; } // Note that we use String.intern() for all string fields, // so we can do instance comparisons. return (this.fontSize == ors.fontSize) && (this.fontFamily == ors.fontFamily) && (this.fontStyle == ors.fontStyle) && (this.fontWeight == ors.fontWeight) && (this.fontVariant == ors.fontVariant) && (this.superscript == ors.superscript) && java.util.Objects.equals(this.locales, ors.locales); } private int cachedHash = -1; @Override public int hashCode() { int ch = this.cachedHash; if (ch != -1) { // Object is immutable - caching is ok. return ch; } String ff = this.fontFamily; if (ff == null) { ff = ""; } String fw = this.fontWeight; if (fw == null) { fw = ""; } String fs = this.fontStyle; if (fs == null) { fs = ""; } final Integer ss = this.superscript; ch = ff.hashCode() ^ fw.hashCode() ^ fs.hashCode() ^ (int) this.fontSize ^ (ss == null ? 0 : ss.intValue()); this.cachedHash = ch; return ch; } @Override public String toString() { return "FontKey[family=" + this.fontFamily + ",size=" + this.fontSize + ",style=" + this.fontStyle + ",weight=" + this.fontWeight + ",variant=" + this.fontVariant + ",superscript=" + this.superscript + "]"; } } }