/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.editor.impl; import com.intellij.Patches; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.impl.view.FontLayoutService; import com.intellij.openapi.util.registry.Registry; import com.intellij.util.containers.ContainerUtil; import gnu.trove.TIntHashSet; import org.intellij.lang.annotations.JdkConstants; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import sun.font.FontDesignMetrics; import java.awt.*; import java.awt.font.FontRenderContext; import java.awt.font.TextAttribute; import java.io.File; import java.io.FilenameFilter; import java.util.*; import java.util.List; /** * @author max */ public class FontInfo { private static final Logger LOG = Logger.getInstance(FontInfo.class); private static final boolean USE_ALTERNATIVE_CAN_DISPLAY_PROCEDURE = Registry.is("ide.mac.fix.font.fallback"); private static final FontRenderContext DEFAULT_CONTEXT = new FontRenderContext(null, false, false); private static final Font DUMMY_FONT = new Font(null); private final Font myFont; private final int mySize; @JdkConstants.FontStyle private final int myStyle; private final boolean myUseLigatures; private final TIntHashSet mySafeCharacters = new TIntHashSet(); private final FontRenderContext myContext; private FontMetrics myFontMetrics = null; /** * @deprecated Use {@link #FontInfo(String, int, int, boolean, FontRenderContext)} instead. */ public FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style) { this(familyName, size, style, style, false, null); } /** * @deprecated Use {@link #FontInfo(String, int, int, boolean, FontRenderContext)} instead. */ public FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style, boolean useLigatures) { this(familyName, size, style, useLigatures, null); } /** * To get valid font metrics from this {@link FontInfo} instance, pass valid {@link FontRenderContext} here as a parameter. */ public FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style, boolean useLigatures, FontRenderContext fontRenderContext) { this(familyName, size, style, style, useLigatures, fontRenderContext); } FontInfo(final String familyName, final int size, @JdkConstants.FontStyle int style, @JdkConstants.FontStyle int realStyle, boolean useLigatures, FontRenderContext context) { mySize = size; myStyle = style; myUseLigatures = useLigatures; Font font = new Font(familyName, style, size); myFont = useLigatures ? getFontWithLigaturesEnabled(font, realStyle) : font; myContext = context; } @NotNull private static Font getFontWithLigaturesEnabled(Font font, @JdkConstants.FontStyle int fontStyle) { if (Patches.JDK_BUG_ID_7162125) { // Ligatures don't work on Mac for fonts loaded natively, so we need to locate and load font manually String familyName = font.getFamily(); File fontFile = findFileForFont(familyName, fontStyle); if (fontFile == null) { LOG.info(font + "(style=" + fontStyle + ") not located"); return font; } LOG.info(font + "(style=" + fontStyle + ") located at " + fontFile); try { font = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(fontStyle, font.getSize()); } catch (Exception e) { LOG.warn("Couldn't load font", e); return font; } } return font.deriveFont(Collections.singletonMap(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON)); } private static final Comparator<File> BY_NAME = Comparator.comparing(File::getName); @Nullable private static File findFileForFont(@NotNull String familyName, int style) { File fontFile = doFindFileForFont(familyName, style); if (fontFile == null && style != Font.PLAIN) fontFile = doFindFileForFont(familyName, Font.PLAIN); if (fontFile == null) fontFile = doFindFileForFont(familyName, -1); return fontFile; } @Nullable private static File doFindFileForFont(@NotNull String familyName, final int style) { final String normalizedFamilyName = familyName.toLowerCase(Locale.getDefault()).replace(" ", ""); FilenameFilter filter = (file, name) -> { String normalizedName = name.toLowerCase(Locale.getDefault()); return normalizedName.startsWith(normalizedFamilyName) && (normalizedName.endsWith(".otf") || normalizedName.endsWith(".ttf")) && (style == -1 || style == getFontStyle(normalizedName)); }; List<File> files = new ArrayList<>(); File[] userFiles = new File(System.getProperty("user.home"), "Library/Fonts").listFiles(filter); if (userFiles != null) files.addAll(Arrays.asList(userFiles)); File[] localFiles = new File("/Library/Fonts").listFiles(filter); if (localFiles != null) files.addAll(Arrays.asList(localFiles)); if (files.isEmpty()) return null; if (style == Font.PLAIN) { // prefer font containing 'regular' in its name List<File> regulars = ContainerUtil.filter(files, file -> file.getName().toLowerCase(Locale.getDefault()).contains("regular")); if (!regulars.isEmpty()) return Collections.min(regulars, BY_NAME); } return Collections.min(files, BY_NAME); } private static int getFontStyle(@NotNull String fontFileNameLowercase) { String baseName = fontFileNameLowercase.substring(0, fontFileNameLowercase.length() - 4); if (baseName.endsWith("-it")) return Font.ITALIC; else if (baseName.endsWith("-boldit")) return Font.BOLD | Font.ITALIC; else return ComplementaryFontsRegistry.getFontStyle(fontFileNameLowercase); } public boolean canDisplay(int codePoint) { try { if (codePoint < 128) return true; if (mySafeCharacters.contains(codePoint)) return true; if (canDisplayImpl(codePoint)) { mySafeCharacters.add(codePoint); return true; } return false; } catch (Exception e) { // JRE has problems working with the font. Just skip. return false; } } private boolean canDisplayImpl(int codePoint) { if (!Character.isValidCodePoint(codePoint)) return false; if (USE_ALTERNATIVE_CAN_DISPLAY_PROCEDURE) { return myFont.createGlyphVector(DEFAULT_CONTEXT, new String(new int[]{codePoint}, 0, 1)).getGlyphCode(0) > 0; } else { return myFont.canDisplay(codePoint); } } public Font getFont() { return myFont; } public int charWidth(int codePoint) { final FontMetrics metrics = fontMetrics(); return FontLayoutService.getInstance().charWidth(metrics, codePoint); } public float charWidth2D(int codePoint) { FontMetrics metrics = fontMetrics(); return FontLayoutService.getInstance().charWidth2D(metrics, codePoint); } public synchronized FontMetrics fontMetrics() { if (myFontMetrics == null) { myFontMetrics = FontDesignMetrics.getMetrics(myFont, myContext == null ? getFontRenderContext(null) : myContext); } return myFontMetrics; } public static FontRenderContext getFontRenderContext(Component component) { if (component == null) { return DEFAULT_CONTEXT; } return component.getFontMetrics(DUMMY_FONT).getFontRenderContext(); } public int getSize() { return mySize; } @JdkConstants.FontStyle public int getStyle() { return myStyle; } public boolean areLigaturesEnabled() { return myUseLigatures; } public FontRenderContext getFontRenderContext() { return myContext; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FontInfo fontInfo = (FontInfo)o; if (!myFont.equals(fontInfo.myFont)) return false; return true; } @Override public int hashCode() { return myFont.hashCode(); } }