Files
server-configs/node_modules/canvas/src/FontManagerMacos.cc
2026-02-13 22:24:27 +08:00

641 lines
19 KiB
C++

// Copyright (c) 2025 Caleb Hearon <caleb@chearon.net>
//
// References:
// - https://github.com/foliojs/font-manager
// - https://searchfox.org/firefox-main/rev/30ea9a2fd7271e9c731df414bd80e46edc3190eb/gfx/thebes/CoreTextFontList.cpp
#include <CoreText/CoreText.h>
#include <vector>
#include <array>
#include <cstring>
#include <iostream>
#include <cmath>
#include "FontManagerMacos.h"
#include "Font.h"
#include "unicode.h"
// Forward declarations for Objective-C types we need
typedef void NSString;
typedef void NSURL;
typedef void NSArray;
const uint16_t MAX_STYLE_LENGTH = 128; // like "Bold Italic", so should never be big
inline double round(double aNum) {
return aNum >= 0.0 ? std::floor(aNum + 0.5) : std::ceil(aNum - 0.5);
}
// https://searchfox.org/firefox-main/rev/30ea9a2fd7271e9c731df414bd80e46edc3190eb/gfx/thebes/CoreTextFontList.cpp#770
static uint32_t convertWeight(float aCTWeight) {
constexpr std::pair<CGFloat, int32_t> kCoreTextToCSSWeights[] = {
{-1.0, 1},
{-0.8, 100},
{-0.6, 200},
{-0.4, 300},
{0.0, 400}, // standard 'regular' weight
{0.23, 500},
{0.3, 600},
{0.4, 700}, // standard 'bold' weight
{0.56, 800},
{0.62, 900}, // Core Text seems to return 0.62 for faces with both
// usWeightClass=800 and 900 in their OS/2 tables!
// We use 900 as there are also fonts that return 0.56,
// so we want an intermediate value for that.
{1.0, 1000}
};
const auto* begin = &kCoreTextToCSSWeights[0];
const auto* end = begin + std::size(kCoreTextToCSSWeights);
auto m = std::upper_bound(
begin,
end,
aCTWeight,
[](CGFloat aValue, const std::pair<CGFloat, int32_t>& aMapping) {
return aValue <= aMapping.first;
}
);
if (m == end) return 1000;
if (m->first == aCTWeight || m == begin) return m->second;
// Interpolate between the preceding and found entries:
const auto* prev = m - 1;
const auto t = (aCTWeight - prev->first) / (m->first - prev->first);
return round(prev->second * (1.0 - t) + m->second * t);
}
void
create_font_descriptor(
std::vector<FontDescriptor>& results,
CTFontDescriptorRef descriptor
) {
FontDescriptor desc;
// TODO: these all need null-checked...
NSURL *nsUrl = (NSURL *) CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);
CFStringRef nsPath = CFURLCopyFileSystemPath((CFURLRef)nsUrl, kCFURLPOSIXPathStyle);
NSString *nsFamily = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontFamilyNameAttribute);
NSString *nsStyle = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute);
CFDictionaryRef nsTraits = (CFDictionaryRef) CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute);
// weight
CFNumberRef weightVal = (CFNumberRef) CFDictionaryGetValue(nsTraits, kCTFontWeightTrait);
float weightValue;
CFNumberGetValue(weightVal, kCFNumberFloatType, &weightValue);
desc.weight = (uint32_t) convertWeight(weightValue);
// file path
CFIndex pathLength = CFStringGetLength(nsPath) * 2 + 1;
desc.url = std::make_unique<char[]>(pathLength);
CFStringGetCString(nsPath, desc.url.get(), pathLength, kCFStringEncodingUTF8);
// family name
CFIndex familyLength = CFStringGetLength((CFStringRef)nsFamily) * 2 + 1;
std::unique_ptr<char[]> family = std::make_unique<char[]>(familyLength);
CFStringGetCString((CFStringRef)nsFamily, family.get(), familyLength, kCFStringEncodingUTF8);
desc.family = std::move(family);
// style
CFNumberRef symbolicTraitsVal = (CFNumberRef)CFDictionaryGetValue(nsTraits, kCTFontSymbolicTrait);
unsigned int symbolicTraits;
CFNumberGetValue(symbolicTraitsVal, kCFNumberIntType, &symbolicTraits);
desc.style = FontStyle::Normal;
if (symbolicTraits & kCTFontItalicTrait) {
desc.style = FontStyle::Italic;
} else {
char styleBuffer[MAX_STYLE_LENGTH];
CFStringGetCString((CFStringRef)nsStyle, styleBuffer, MAX_STYLE_LENGTH, kCFStringEncodingUTF8);
if (strstr(styleBuffer, "Oblique") != NULL) desc.style = FontStyle::Oblique;
}
results.push_back(std::move(desc));
CFRelease(nsUrl);
CFRelease(nsPath);
CFRelease(nsFamily);
CFRelease(nsStyle);
CFRelease(nsTraits);
}
void FontManagerMacos::readSystemFonts(std::vector<FontDescriptor>& results) {
static CTFontCollectionRef collection = NULL;
if (collection == NULL) collection = CTFontCollectionCreateFromAvailableFonts(NULL);
NSArray *matches = (NSArray *) CTFontCollectionCreateMatchingFontDescriptors(collection);
CFIndex count = CFArrayGetCount((CFArrayRef) matches);
results.reserve(count);
for (CFIndex i = 0; i < count; i++) {
CTFontDescriptorRef match = (CTFontDescriptorRef)CFArrayGetValueAtIndex((CFArrayRef)matches, i);
create_font_descriptor(results, match);
}
CFRelease(matches);
}
void FontManagerMacos::populateFallbackFonts(
std::vector<std::string>& families,
script_t script
) {
switch (script) {
case SCRIPT_COMMON:
case SCRIPT_INHERITED:
// In most cases, COMMON and INHERITED characters will be merged into
// their context, but if they occur without any specific script context
// we'll just try common default fonts here.
case SCRIPT_LATIN:
case SCRIPT_CYRILLIC:
case SCRIPT_GREEK:
families.push_back("Lucida Grande");
break;
// CJK-related script codes are a bit troublesome because of unification;
// we'll probably just get HAN much of the time, so the choice of which
// language font to try for fallback is rather arbitrary. Usually, though,
// we hope that font prefs will have handled this earlier.
case SCRIPT_BOPOMOFO:
case SCRIPT_HAN:
families.push_back("Songti SC");
families.push_back("SimSun-ExtB");
break;
case SCRIPT_HIRAGANA:
case SCRIPT_KATAKANA:
families.push_back("Hiragino Sans");
families.push_back("Hiragino Kaku Gothic ProN");
break;
case SCRIPT_HANGUL:
families.push_back("Nanum Gothic");
families.push_back("Apple SD Gothic Neo");
break;
// For most other scripts, macOS comes with a default font we can use.
case SCRIPT_ARABIC:
families.push_back("Geeza Pro");
break;
case SCRIPT_ARMENIAN:
families.push_back("Mshtakan");
break;
case SCRIPT_BENGALI:
families.push_back("Bangla Sangam MN");
break;
case SCRIPT_CHEROKEE:
families.push_back("Plantagenet Cherokee");
break;
case SCRIPT_COPTIC:
families.push_back("Noto Sans Coptic");
break;
case SCRIPT_DESERET:
families.push_back("Baskerville");
break;
case SCRIPT_DEVANAGARI:
families.push_back("Devanagari Sangam MN");
break;
case SCRIPT_ETHIOPIC:
families.push_back("Kefa");
break;
case SCRIPT_GEORGIAN:
families.push_back("Helvetica");
break;
case SCRIPT_GOTHIC:
families.push_back("Noto Sans Gothic");
break;
case SCRIPT_GUJARATI:
families.push_back("Gujarati Sangam MN");
break;
case SCRIPT_GURMUKHI:
families.push_back("Gurmukhi MN");
break;
case SCRIPT_HEBREW:
families.push_back("Lucida Grande");
break;
case SCRIPT_KANNADA:
families.push_back("Kannada MN");
break;
case SCRIPT_KHMER:
families.push_back("Khmer MN");
break;
case SCRIPT_LAO:
families.push_back("Lao MN");
break;
case SCRIPT_MALAYALAM:
families.push_back("Malayalam Sangam MN");
break;
case SCRIPT_MONGOLIAN:
families.push_back("Noto Sans Mongolian");
break;
case SCRIPT_MYANMAR:
families.push_back("Myanmar MN");
break;
case SCRIPT_OGHAM:
families.push_back("Noto Sans Ogham");
break;
case SCRIPT_OLD_ITALIC:
families.push_back("Noto Sans Old Italic");
break;
case SCRIPT_ORIYA:
families.push_back("Oriya Sangam MN");
break;
case SCRIPT_RUNIC:
families.push_back("Noto Sans Runic");
break;
case SCRIPT_SINHALA:
families.push_back("Sinhala Sangam MN");
break;
case SCRIPT_SYRIAC:
families.push_back("Noto Sans Syriac");
break;
case SCRIPT_TAMIL:
families.push_back("Tamil MN");
break;
case SCRIPT_TELUGU:
families.push_back("Telugu MN");
break;
case SCRIPT_THAANA:
families.push_back("Noto Sans Thaana");
break;
case SCRIPT_THAI:
families.push_back("Thonburi");
break;
case SCRIPT_TIBETAN:
families.push_back("Kailasa");
break;
case SCRIPT_CANADIAN_ABORIGINAL:
families.push_back("Euphemia UCAS");
break;
case SCRIPT_YI:
families.push_back("Noto Sans Yi");
families.push_back("STHeiti");
break;
case SCRIPT_TAGALOG:
families.push_back("Noto Sans Tagalog");
break;
case SCRIPT_HANUNOO:
families.push_back("Noto Sans Hanunoo");
break;
case SCRIPT_BUHID:
families.push_back("Noto Sans Buhid");
break;
case SCRIPT_TAGBANWA:
families.push_back("Noto Sans Tagbanwa");
break;
case SCRIPT_BRAILLE:
families.push_back("Apple Braille");
break;
case SCRIPT_CYPRIOT:
families.push_back("Noto Sans Cypriot");
break;
case SCRIPT_LIMBU:
families.push_back("Noto Sans Limbu");
break;
case SCRIPT_LINEAR_B:
families.push_back("Noto Sans Linear B");
break;
case SCRIPT_OSMANYA:
families.push_back("Noto Sans Osmanya");
break;
case SCRIPT_SHAVIAN:
families.push_back("Noto Sans Shavian");
break;
case SCRIPT_TAI_LE:
families.push_back("Noto Sans Tai Le");
break;
case SCRIPT_UGARITIC:
families.push_back("Noto Sans Ugaritic");
break;
case SCRIPT_BUGINESE:
families.push_back("Noto Sans Buginese");
break;
case SCRIPT_GLAGOLITIC:
families.push_back("Noto Sans Glagolitic");
break;
case SCRIPT_KHAROSHTHI:
families.push_back("Noto Sans Kharoshthi");
break;
case SCRIPT_SYLOTI_NAGRI:
families.push_back("Noto Sans Syloti Nagri");
break;
case SCRIPT_NEW_TAI_LUE:
families.push_back("Noto Sans New Tai Lue");
break;
case SCRIPT_TIFINAGH:
families.push_back("Noto Sans Tifinagh");
break;
case SCRIPT_OLD_PERSIAN:
families.push_back("Noto Sans Old Persian");
break;
case SCRIPT_BALINESE:
families.push_back("Noto Sans Balinese");
break;
case SCRIPT_BATAK:
families.push_back("Noto Sans Batak");
break;
case SCRIPT_BRAHMI:
families.push_back("Noto Sans Brahmi");
break;
case SCRIPT_CHAM:
families.push_back("Noto Sans Cham");
break;
case SCRIPT_EGYPTIAN_HIEROGLYPHS:
families.push_back("Noto Sans Egyptian Hieroglyphs");
break;
case SCRIPT_PAHAWH_HMONG:
families.push_back("Noto Sans Pahawh Hmong");
break;
case SCRIPT_OLD_HUNGARIAN:
families.push_back("Noto Sans Old Hungarian");
break;
case SCRIPT_JAVANESE:
families.push_back("Noto Sans Javanese");
break;
case SCRIPT_KAYAH_LI:
families.push_back("Noto Sans Kayah Li");
break;
case SCRIPT_LEPCHA:
families.push_back("Noto Sans Lepcha");
break;
case SCRIPT_LINEAR_A:
families.push_back("Noto Sans Linear A");
break;
case SCRIPT_MANDAIC:
families.push_back("Noto Sans Mandaic");
break;
case SCRIPT_NKO:
families.push_back("Noto Sans NKo");
break;
case SCRIPT_OLD_TURKIC:
families.push_back("Noto Sans Old Turkic");
break;
case SCRIPT_OLD_PERMIC:
families.push_back("Noto Sans Old Permic");
break;
case SCRIPT_PHAGS_PA:
families.push_back("Noto Sans PhagsPa");
break;
case SCRIPT_PHOENICIAN:
families.push_back("Noto Sans Phoenician");
break;
case SCRIPT_MIAO:
families.push_back("Noto Sans Miao");
break;
case SCRIPT_VAI:
families.push_back("Noto Sans Vai");
break;
case SCRIPT_CUNEIFORM:
families.push_back("Noto Sans Cuneiform");
break;
case SCRIPT_CARIAN:
families.push_back("Noto Sans Carian");
break;
case SCRIPT_TAI_THAM:
families.push_back("Noto Sans Tai Tham");
break;
case SCRIPT_LYCIAN:
families.push_back("Noto Sans Lycian");
break;
case SCRIPT_LYDIAN:
families.push_back("Noto Sans Lydian");
break;
case SCRIPT_OL_CHIKI:
families.push_back("Noto Sans Ol Chiki");
break;
case SCRIPT_REJANG:
families.push_back("Noto Sans Rejang");
break;
case SCRIPT_SAURASHTRA:
families.push_back("Noto Sans Saurashtra");
break;
case SCRIPT_SUNDANESE:
families.push_back("Noto Sans Sundanese");
break;
case SCRIPT_MEETEI_MAYEK:
families.push_back("Noto Sans Meetei Mayek");
break;
case SCRIPT_IMPERIAL_ARAMAIC:
families.push_back("Noto Sans Imperial Aramaic");
break;
case SCRIPT_AVESTAN:
families.push_back("Noto Sans Avestan");
break;
case SCRIPT_CHAKMA:
families.push_back("Noto Sans Chakma");
break;
case SCRIPT_KAITHI:
families.push_back("Noto Sans Kaithi");
break;
case SCRIPT_MANICHAEAN:
families.push_back("Noto Sans Manichaean");
break;
case SCRIPT_INSCRIPTIONAL_PAHLAVI:
families.push_back("Noto Sans Inscriptional Pahlavi");
break;
case SCRIPT_PSALTER_PAHLAVI:
families.push_back("Noto Sans Psalter Pahlavi");
break;
case SCRIPT_INSCRIPTIONAL_PARTHIAN:
families.push_back("Noto Sans Inscriptional Parthian");
break;
case SCRIPT_SAMARITAN:
families.push_back("Noto Sans Samaritan");
break;
case SCRIPT_TAI_VIET:
families.push_back("Noto Sans Tai Viet");
break;
case SCRIPT_BAMUM:
families.push_back("Noto Sans Bamum");
break;
case SCRIPT_LISU:
families.push_back("Noto Sans Lisu");
break;
case SCRIPT_OLD_SOUTH_ARABIAN:
families.push_back("Noto Sans Old South Arabian");
break;
case SCRIPT_BASSA_VAH:
families.push_back("Noto Sans Bassa Vah");
break;
case SCRIPT_DUPLOYAN:
families.push_back("Noto Sans Duployan");
break;
case SCRIPT_ELBASAN:
families.push_back("Noto Sans Elbasan");
break;
case SCRIPT_GRANTHA:
families.push_back("Noto Sans Grantha");
break;
case SCRIPT_MENDE_KIKAKUI:
families.push_back("Noto Sans Mende Kikakui");
break;
case SCRIPT_MEROITIC_CURSIVE:
case SCRIPT_MEROITIC_HIEROGLYPHS:
families.push_back("Noto Sans Meroitic");
break;
case SCRIPT_OLD_NORTH_ARABIAN:
families.push_back("Noto Sans Old North Arabian");
break;
case SCRIPT_NABATAEAN:
families.push_back("Noto Sans Nabataean");
break;
case SCRIPT_PALMYRENE:
families.push_back("Noto Sans Palmyrene");
break;
case SCRIPT_KHUDAWADI:
families.push_back("Noto Sans Khudawadi");
break;
case SCRIPT_WARANG_CITI:
families.push_back("Noto Sans Warang Citi");
break;
case SCRIPT_MRO:
families.push_back("Noto Sans Mro");
break;
case SCRIPT_SHARADA:
families.push_back("Noto Sans Sharada");
break;
case SCRIPT_SORA_SOMPENG:
families.push_back("Noto Sans Sora Sompeng");
break;
case SCRIPT_TAKRI:
families.push_back("Noto Sans Takri");
break;
case SCRIPT_KHOJKI:
families.push_back("Noto Sans Khojki");
break;
case SCRIPT_TIRHUTA:
families.push_back("Noto Sans Tirhuta");
break;
case SCRIPT_CAUCASIAN_ALBANIAN:
families.push_back("Noto Sans Caucasian Albanian");
break;
case SCRIPT_MAHAJANI:
families.push_back("Noto Sans Mahajani");
break;
case SCRIPT_AHOM:
families.push_back("Noto Serif Ahom");
break;
case SCRIPT_HATRAN:
families.push_back("Noto Sans Hatran");
break;
case SCRIPT_MODI:
families.push_back("Noto Sans Modi");
break;
case SCRIPT_MULTANI:
families.push_back("Noto Sans Multani");
break;
case SCRIPT_PAU_CIN_HAU:
families.push_back("Noto Sans Pau Cin Hau");
break;
case SCRIPT_SIDDHAM:
families.push_back("Noto Sans Siddham");
break;
case SCRIPT_ADLAM:
families.push_back("Noto Sans Adlam");
break;
case SCRIPT_BHAIKSUKI:
families.push_back("Noto Sans Bhaiksuki");
break;
case SCRIPT_MARCHEN:
families.push_back("Noto Sans Marchen");
break;
case SCRIPT_NEWA:
families.push_back("Noto Sans Newa");
break;
case SCRIPT_OSAGE:
families.push_back("Noto Sans Osage");
break;
case SCRIPT_HANIFI_ROHINGYA:
families.push_back("Noto Sans Hanifi Rohingya");
break;
case SCRIPT_WANCHO:
families.push_back("Noto Sans Wancho");
break;
// Script codes for which no commonly-installed font is currently known.
// Probably future macOS versions will add Noto fonts for many of these,
// so we should watch for updates.
case SCRIPT_NONE:
case SCRIPT_NUSHU:
case SCRIPT_TANGUT:
case SCRIPT_ANATOLIAN_HIEROGLYPHS:
case SCRIPT_MASARAM_GONDI:
case SCRIPT_SOYOMBO:
case SCRIPT_ZANABAZAR_SQUARE:
case SCRIPT_DOGRA:
case SCRIPT_GUNJALA_GONDI:
case SCRIPT_MAKASAR:
case SCRIPT_MEDEFAIDRIN:
case SCRIPT_SOGDIAN:
case SCRIPT_OLD_SOGDIAN:
case SCRIPT_ELYMAIC:
case SCRIPT_NYIAKENG_PUACHUE_HMONG:
case SCRIPT_NANDINAGARI:
case SCRIPT_CHORASMIAN:
case SCRIPT_DIVES_AKURU:
case SCRIPT_KHITAN_SMALL_SCRIPT:
case SCRIPT_YEZIDI:
case SCRIPT_CYPRO_MINOAN:
case SCRIPT_OLD_UYGHUR:
case SCRIPT_TANGSA:
case SCRIPT_TOTO:
case SCRIPT_VITHKUQI:
case SCRIPT_KAWI:
case SCRIPT_NAG_MUNDARI:
case SCRIPT_GARAY:
case SCRIPT_GURUNG_KHEMA:
case SCRIPT_KIRAT_RAI:
case SCRIPT_OL_ONAL:
case SCRIPT_SIGNWRITING:
case SCRIPT_SUNUWAR:
case SCRIPT_TODHRI:
case SCRIPT_TULU_TIGALARI:
break;
}
// TODO: Color Emoji should depend on if the default presentation for the
// codepoint is color or if a VS16 selector is present.
families.push_back("Apple Color Emoji");
// TODO: Firefox makes the middle these 6 conditional on the codepoint.
// When users try to paint text that isn't in the first few families, this
// is going to be slower than it needs to be. Original Firefox comment next...
//
// Symbols/dingbats are generally Script=COMMON but may be resolved to any
// surrounding script run. So we'll always append a couple of likely fonts
// for such characters.
families.push_back("Zapf Dingbats");
families.push_back("Geneva");
families.push_back("STIXGeneral");
families.push_back("Apple Symbols");
// Japanese fonts also cover a lot of miscellaneous symbols
families.push_back("Hiragino Sans");
families.push_back("Hiragino Kaku Gothic ProN");
// Arial Unicode MS has lots of glyphs for obscure characters; try it as a
// last resort.
families.push_back("Arial Unicode MS");
}
// See the preferences font.name-list.*.x-western in Firefox
const std::vector<std::string> serif_fonts = {"Times", "Times New Roman"};
const std::vector<std::string> sans_serif_fonts = {"Helvetica", "Arial"};
const std::vector<std::string> monospace_fonts = {"Menlo"};
const std::vector<std::string> cursive_fonts = {"Apple Chancery"};
const std::vector<std::string> fantasy_fonts = {"Papyrus"};
std::optional<const std::vector<std::string>*>
FontManagerMacos::getGenericList(const std::string& generic) {
if (generic == "serif") {
return &serif_fonts;
} else if (generic == "sans-serif") {
return &sans_serif_fonts;
} else if (generic == "monospace") {
return &monospace_fonts;
} else if (generic == "cursive") {
return &cursive_fonts;
} else if (generic == "fantasy") {
return &fantasy_fonts;
} else {
return std::nullopt;
}
}