// 1 november 2017 #import "uipriv_darwin.h" #import "attrstr.h" // This is the part of the font style matching and normalization code // that handles fonts that use a traits dictionary. // // Matching stupidity: Core Text requires an **exact match for the // entire traits dictionary**, otherwise it will **drop ALL the traits**. // // Normalization stupidity: Core Text uses its own scaled values for // weight and width, but the values are different if the font is not // registered and if said font is TrueType or OpenType. The values // for all other cases do have some named constants starting with // OS X 10.11, but even these aren't very consistent in practice. // // Of course, none of this is documented anywhere, so I had to do // both trial-and-error AND reverse engineering to figure out what's // what. We'll just convert Core Text's values into libui constants // and use those for matching. static BOOL fontRegistered(uiprivFontStyleData *d) { if (![d hasRegistrationScope]) // header says this should be treated as the same as not registered return NO; // examination of Core Text shows this is accurate return [d registrationScope] != kCTFontManagerScopeNone; } // Core Text does (usWidthClass / 10) - 0.5 here. // This roughly maps to our values with increments of 0.1, except for the fact 0 and 10 are allowed by Core Text, despite being banned by TrueType and OpenType themselves. // We'll just treat them as identical to 1 and 9, respectively. static const uiTextStretch os2WidthsToStretches[] = { uiTextStretchUltraCondensed, uiTextStretchUltraCondensed, uiTextStretchExtraCondensed, uiTextStretchCondensed, uiTextStretchSemiCondensed, uiTextStretchNormal, uiTextStretchSemiExpanded, uiTextStretchExpanded, uiTextStretchExtraExpanded, uiTextStretchUltraExpanded, uiTextStretchUltraExpanded, }; static const CFStringRef exceptions[] = { CFSTR("LucidaGrande"), CFSTR(".LucidaGrandeUI"), CFSTR("STHeiti"), CFSTR("STXihei"), CFSTR("TimesNewRomanPSMT"), NULL, }; static void trySecondaryOS2Values(uiprivFontStyleData *d, uiFontDescriptor *out, BOOL *hasWeight, BOOL *hasWidth) { CFDataRef os2; uint16_t usWeightClass, usWidthClass; CFStringRef psname; const CFStringRef *ex; *hasWeight = NO; *hasWidth = NO; // only applies to unregistered fonts if (fontRegistered(d)) return; os2 = [d table:kCTFontTableOS2]; if (os2 == NULL) // no OS2 table, so no secondary values return; if (CFDataGetLength(os2) > 77) { const UInt8 *b; b = CFDataGetBytePtr(os2); usWeightClass = ((uint16_t) (b[4])) << 8; usWeightClass |= (uint16_t) (b[5]); if (usWeightClass <= 1000) { if (usWeightClass < 11) usWeightClass *= 100; *hasWeight = YES; } usWidthClass = ((uint16_t) (b[6])) << 8; usWidthClass |= (uint16_t) (b[7]); if (usWidthClass <= 10) *hasWidth = YES; } else { usWeightClass = 0; *hasWeight = YES; usWidthClass = 0; *hasWidth = YES; } if (*hasWeight) // we can just use this directly out->Weight = usWeightClass; if (*hasWidth) out->Stretch = os2WidthsToStretches[usWidthClass]; CFRelease(os2); // don't use secondary weights in the event of special predefined names psname = [d postScriptName]; for (ex = exceptions; *ex != NULL; ex++) if (CFEqual(psname, *ex)) { *hasWeight = NO; break; } } static BOOL testTTFOTFSubfamilyName(CFStringRef name, CFStringRef want) { CFRange range; if (name == NULL) return NO; range.location = 0; range.length = CFStringGetLength(name); return CFStringFindWithOptions(name, want, range, (kCFCompareCaseInsensitive | kCFCompareBackwards | kCFCompareNonliteral), NULL) != false; } static BOOL testTTFOTFSubfamilyNames(uiprivFontStyleData *d, CFStringRef want) { switch ([d fontFormat]) { case kCTFontFormatOpenTypePostScript: case kCTFontFormatOpenTypeTrueType: case kCTFontFormatTrueType: break; default: return NO; } if (testTTFOTFSubfamilyName([d preferredSubFamilyName], want)) return YES; if (testTTFOTFSubfamilyName([d subFamilyName], want)) return YES; if (testTTFOTFSubfamilyName([d fullName], want)) return YES; if (testTTFOTFSubfamilyName([d preferredFamilyName], want)) return YES; return testTTFOTFSubfamilyName([d familyName], want); } // work around a bug in libFontRegistry.dylib static BOOL shouldReallyBeThin(uiprivFontStyleData *d) { return testTTFOTFSubfamilyNames(d, CFSTR("W1")); } // work around a bug in libFontRegistry.dylib static BOOL shouldReallyBeSemiCondensed(uiprivFontStyleData *d) { return testTTFOTFSubfamilyNames(d, CFSTR("Semi Condensed")); } void uiprivProcessFontTraits(uiprivFontStyleData *d, uiFontDescriptor *out) { double weight, width; BOOL hasWeight, hasWidth; hasWeight = NO; hasWidth = NO; trySecondaryOS2Values(d, out, &hasWeight, &hasWidth); weight = [d weight]; width = [d width]; if (!hasWeight) // TODO this scale is a bit lopsided if (weight <= -0.7) out->Weight = uiTextWeightThin; else if (weight <= -0.5) out->Weight = uiTextWeightUltraLight; else if (weight <= -0.3) out->Weight = uiTextWeightLight; else if (weight <= -0.23) { out->Weight = uiTextWeightBook; if (shouldReallyBeThin(d)) out->Weight = uiTextWeightThin; } else if (weight <= 0.0) out->Weight = uiTextWeightNormal; else if (weight <= 0.23) out->Weight = uiTextWeightMedium; else if (weight <= 0.3) out->Weight = uiTextWeightSemiBold; else if (weight <= 0.4) out->Weight = uiTextWeightBold; else if (weight <= 0.5) out->Weight = uiTextWeightUltraBold; else if (weight <= 0.7) out->Weight = uiTextWeightHeavy; else out->Weight = uiTextWeightUltraHeavy; if (!hasWidth) // TODO this scale is a bit lopsided if (width <= -0.7) { out->Stretch = uiTextStretchUltraCondensed; if (shouldReallyBeSemiCondensed(d)) out->Stretch = uiTextStretchSemiCondensed; } else if (width <= -0.5) out->Stretch = uiTextStretchExtraCondensed; else if (width <= -0.2) out->Stretch = uiTextStretchCondensed; else if (width <= -0.1) out->Stretch = uiTextStretchSemiCondensed; else if (width <= 0.0) out->Stretch = uiTextStretchNormal; else if (width <= 0.1) out->Stretch = uiTextStretchSemiExpanded; else if (width <= 0.2) out->Stretch = uiTextStretchExpanded; else if (width <= 0.6) out->Stretch = uiTextStretchExtraExpanded; else out->Stretch = uiTextStretchUltraExpanded; }