|
| 1 | +/* eslint-disable no-bitwise */ |
| 2 | + |
| 3 | +import hashStyle from '../../utils/hashStyle'; |
| 4 | +import { TextStyle } from '../../types'; |
| 5 | +import { FONT_STYLES } from '../../jsonUtils/textLayers'; |
| 6 | + |
| 7 | +// this borrows heavily from react-native's RCTFont class |
| 8 | +// thanks y'all |
| 9 | +// https://github.com/facebook/react-native/blob/master/React/Views/RCTFont.mm |
| 10 | + |
| 11 | +const FONT_WEIGHTS = { |
| 12 | + ultralight: -0.8, |
| 13 | + '100': -0.8, |
| 14 | + thin: -0.6, |
| 15 | + '200': -0.6, |
| 16 | + light: -0.4, |
| 17 | + '300': -0.4, |
| 18 | + normal: 0, |
| 19 | + regular: 0, |
| 20 | + '400': 0, |
| 21 | + semibold: 0.23, |
| 22 | + demibold: 0.23, |
| 23 | + '500': 0.23, |
| 24 | + '600': 0.3, |
| 25 | + bold: 0.4, |
| 26 | + '700': 0.4, |
| 27 | + extrabold: 0.56, |
| 28 | + ultrabold: 0.56, |
| 29 | + heavy: 0.56, |
| 30 | + '800': 0.56, |
| 31 | + black: 0.62, |
| 32 | + '900': 0.62, |
| 33 | +}; |
| 34 | + |
| 35 | +type NSFont = any; |
| 36 | + |
| 37 | +const isItalicFont = (font: NSFont): boolean => { |
| 38 | + const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute); |
| 39 | + const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue(); |
| 40 | + |
| 41 | + return (symbolicTraits & NSFontItalicTrait) !== 0; |
| 42 | +}; |
| 43 | + |
| 44 | +const isCondensedFont = (font: NSFont): boolean => { |
| 45 | + const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute); |
| 46 | + const symbolicTraits = traits[NSFontSymbolicTrait].unsignedIntValue(); |
| 47 | + |
| 48 | + return (symbolicTraits & NSFontCondensedTrait) !== 0; |
| 49 | +}; |
| 50 | + |
| 51 | +const weightOfFont = (font: NSFont): number => { |
| 52 | + const traits = font.fontDescriptor().objectForKey(NSFontTraitsAttribute); |
| 53 | + |
| 54 | + const weight = traits[NSFontWeightTrait].doubleValue(); |
| 55 | + if (weight === 0.0) { |
| 56 | + const weights = Object.keys(FONT_WEIGHTS); |
| 57 | + const fontName = String(font.fontName()).toLowerCase(); |
| 58 | + const matchingWeight = weights.find(w => fontName.endsWith(w)); |
| 59 | + if (matchingWeight) { |
| 60 | + return FONT_WEIGHTS[matchingWeight]; |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + return weight; |
| 65 | +}; |
| 66 | + |
| 67 | +const fontNamesForFamilyName = (familyName: string): Array<string> => { |
| 68 | + const manager = NSFontManager.sharedFontManager(); |
| 69 | + const members = NSArray.arrayWithArray(manager.availableMembersOfFontFamily(familyName)); |
| 70 | + |
| 71 | + const results = []; |
| 72 | + for (let i = 0; i < members.length; i += 1) { |
| 73 | + results.push(members[i][0]); |
| 74 | + } |
| 75 | + |
| 76 | + return results; |
| 77 | +}; |
| 78 | + |
| 79 | +const useCache = true; |
| 80 | +const _cache: Map<string, NSFont> = new Map(); |
| 81 | + |
| 82 | +const getCached = (key: string): NSFont => { |
| 83 | + if (!useCache) return undefined; |
| 84 | + return _cache.get(key); |
| 85 | +}; |
| 86 | + |
| 87 | +export const findFont = (style: TextStyle): NSFont => { |
| 88 | + const cacheKey = hashStyle(style); |
| 89 | + |
| 90 | + let font = getCached(cacheKey); |
| 91 | + if (font) { |
| 92 | + return font; |
| 93 | + } |
| 94 | + const defaultFontFamily = NSFont.systemFontOfSize(14).familyName(); |
| 95 | + const defaultFontWeight = NSFontWeightRegular; |
| 96 | + const defaultFontSize = 14; |
| 97 | + |
| 98 | + const fontSize = style.fontSize ? style.fontSize : defaultFontSize; |
| 99 | + let fontWeight = style.fontWeight |
| 100 | + ? FONT_WEIGHTS[style.fontWeight.toLowerCase()] |
| 101 | + : defaultFontWeight; |
| 102 | + |
| 103 | + let familyName = defaultFontFamily; |
| 104 | + let isItalic = false; |
| 105 | + let isCondensed = false; |
| 106 | + |
| 107 | + if (style.fontFamily) { |
| 108 | + familyName = style.fontFamily; |
| 109 | + } |
| 110 | + |
| 111 | + if (style.fontStyle) { |
| 112 | + isItalic = FONT_STYLES[style.fontStyle] || false; |
| 113 | + } |
| 114 | + |
| 115 | + let didFindFont = false; |
| 116 | + |
| 117 | + // Handle system font as special case. This ensures that we preserve |
| 118 | + // the specific metrics of the standard system font as closely as possible. |
| 119 | + if (familyName === defaultFontFamily || familyName === 'System') { |
| 120 | + font = NSFont.systemFontOfSize_weight(fontSize, fontWeight); |
| 121 | + |
| 122 | + if (font) { |
| 123 | + didFindFont = true; |
| 124 | + |
| 125 | + if (isItalic || isCondensed) { |
| 126 | + let fontDescriptor = font.fontDescriptor(); |
| 127 | + let symbolicTraits = fontDescriptor.symbolicTraits(); |
| 128 | + if (isItalic) { |
| 129 | + symbolicTraits |= NSFontItalicTrait; |
| 130 | + } |
| 131 | + |
| 132 | + if (isCondensed) { |
| 133 | + symbolicTraits |= NSFontCondensedTrait; |
| 134 | + } |
| 135 | + |
| 136 | + fontDescriptor = fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits); |
| 137 | + font = NSFont.fontWithDescriptor_size(fontDescriptor, fontSize); |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + const fontNames = fontNamesForFamilyName(familyName); |
| 143 | + |
| 144 | + // Gracefully handle being given a font name rather than font family, for |
| 145 | + // example: "Helvetica Light Oblique" rather than just "Helvetica". |
| 146 | + if (!didFindFont && fontNames.length === 0) { |
| 147 | + font = NSFont.fontWithName_size(familyName, fontSize); |
| 148 | + if (font) { |
| 149 | + // It's actually a font name, not a font family name, |
| 150 | + // but we'll do what was meant, not what was said. |
| 151 | + familyName = font.familyName(); |
| 152 | + fontWeight = style.fontWeight ? fontWeight : weightOfFont(font); |
| 153 | + isItalic = style.fontStyle ? isItalic : isItalicFont(font); |
| 154 | + isCondensed = isCondensedFont(font); |
| 155 | + } else { |
| 156 | + font = NSFont.systemFontOfSize_weight(fontSize, fontWeight); |
| 157 | + } |
| 158 | + |
| 159 | + didFindFont = true; |
| 160 | + } |
| 161 | + |
| 162 | + if (!didFindFont) { |
| 163 | + // Get the closest font that matches the given weight for the fontFamily |
| 164 | + let closestWeight = Infinity; |
| 165 | + for (let i = 0; i < fontNames.length; i += 1) { |
| 166 | + const match = NSFont.fontWithName_size(fontNames[i], fontSize); |
| 167 | + |
| 168 | + if (isItalic === isItalicFont(match) && isCondensed === isCondensedFont(match)) { |
| 169 | + const testWeight = weightOfFont(match); |
| 170 | + |
| 171 | + if (Math.abs(testWeight - fontWeight) < Math.abs(closestWeight - fontWeight)) { |
| 172 | + font = match; |
| 173 | + |
| 174 | + closestWeight = testWeight; |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + // If we still don't have a match at least return the first font in the fontFamily |
| 181 | + // This is to support built-in font Zapfino and other custom single font families like Impact |
| 182 | + if (!font) { |
| 183 | + if (fontNames.length > 0) { |
| 184 | + font = NSFont.fontWithName_size(fontNames[0], fontSize); |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + // TODO(gold): support opentype features: small-caps & number types |
| 189 | + |
| 190 | + if (font) { |
| 191 | + _cache.set(cacheKey, font); |
| 192 | + } |
| 193 | + |
| 194 | + return font; |
| 195 | +}; |
| 196 | + |
| 197 | +export default function findFontName(style: TextStyle): string { |
| 198 | + const font = findFont(style); |
| 199 | + return font.fontDescriptor().postscriptName(); |
| 200 | +} |
0 commit comments