From b1cb16c7f8806b7d69898fe5b29f5e0b999943fe Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 2 Dec 2015 11:44:55 -0800 Subject: [PATCH] Do a better job of picking the best match from CoreText --- src/font_file_lister_coretext.mm | 152 ++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 41 deletions(-) diff --git a/src/font_file_lister_coretext.mm b/src/font_file_lister_coretext.mm index d71ec3a6e..8eef4edeb 100644 --- a/src/font_file_lister_coretext.mm +++ b/src/font_file_lister_coretext.mm @@ -20,8 +20,15 @@ #include namespace { -void process_font(CollectionResult& ret, NSFontDescriptor *font, int bold, bool italic, - std::vector const& characters) { +struct FontMatch { + NSURL *url; + NSCharacterSet *codepoints; + double weight; + double width; + bool italic; +}; + +void process_descriptor(NSFontDescriptor *font, std::vector& out) { // For whatever reason there is no NSFontURLAttribute NSURL *url = [font objectForKey:(__bridge NSString *)kCTFontURLAttribute]; if (!url) @@ -29,23 +36,90 @@ void process_font(CollectionResult& ret, NSFontDescriptor *font, int bold, bool NSDictionary *attributes = [font objectForKey:NSFontTraitsAttribute]; double weight = [attributes[NSFontWeightTrait] doubleValue]; - double slant = [attributes[NSFontSlantTrait] doubleValue]; + double width = [attributes[NSFontWidthTrait] doubleValue]; + bool italic = [attributes[NSFontSlantTrait] doubleValue] > 0.03; - if (italic != (slant > 0.03)) - return; - if (bold == 0 && weight > 0) - return; - if (bold == 1 && weight < 0.4) - return; - - NSCharacterSet *codepoints = [font objectForKey:NSFontCharacterSetAttribute]; - for (int chr : characters) { - if (![codepoints longCharacterIsMember:chr]) { - ret.missing += chr; + // CoreText sometimes reports a slant of zero (or a very small slant) + // despite the macStyle field indicating that the font is italic, so + // double check by reading that field from the ttf + if (!italic) @autoreleasepool { + NSFont *nsfont = [NSFont fontWithDescriptor:font size:10]; + auto data = (__bridge_transfer NSData *)CTFontCopyTable((__bridge CTFontRef)nsfont, kCTFontTableHead, 0); + if (data.length > 45) { + italic = static_cast(data.bytes)[45] & 2; } } - ret.paths.push_back(url.fileSystemRepresentation); + out.push_back({url, [font objectForKey:NSFontCharacterSetAttribute], weight, width, italic}); +} + +void select_best(CollectionResult& ret, std::vector& options, double bold, + bool italic, std::vector const& characters) { + if (options.empty()) + return; + + // Prioritize fonts with the width closest to normal + sort(begin(options), end(options), [](FontMatch const& lft, FontMatch const& rgt) { + return std::abs(lft.width) < std::abs(rgt.width); + }); + + const FontMatch *match = nullptr; + + auto check_weight = [&](FontMatch const& m) { + if (m.weight < bold) + return false; + if (match && m.weight >= match->weight) + return false; + return true; + }; + auto check_slant = [&](FontMatch const& m) { + return (italic && m.italic) || (!italic && !m.italic); + }; + + // First look for the best valid match + for (auto const& m : options) { + if (check_slant(m) && check_weight(m)) + match = &m; + } + + if (!match) { + // Either none are italic, or none are heavy enough + // Just pick the heaviest one (italic if applicable) + for (auto const& m : options) { + if (!check_slant(m) || (match && m.weight >= match->weight)) + continue; + match = &m; + ret.fake_bold = bold > 0; + } + } + + if (!match) { + // We want italic but there is none, or we want regular and there is + // only italic. Don't complain about fake italic in the latter case. + ret.fake_italic = italic; + for (auto const& m : options) { + if (check_weight(m)) + match = &m; + } + } + + if (!match) { + // No correct weight match even in non-italic, so just pick the heaviest + ret.fake_bold = bold > 0; + for (auto const& m : options) { + if (match && m.weight >= match->weight) + continue; + match = &m; + } + } + + auto& best = *match; + ret.paths.push_back(best.url.fileSystemRepresentation); + for (int chr : characters) { + if (![best.codepoints longCharacterIsMember:chr]) { + ret.missing += chr; + } + } } } @@ -54,8 +128,28 @@ CollectionResult CoreTextFontFileLister::GetFontPaths(std::string const& facenam std::vector const& characters) { CollectionResult ret; + double desired_weight; + if (bold == 0) + desired_weight = 0; + else if (bold == 1) + desired_weight = 0.4; + else if (bold < 400) + desired_weight = -0.4; + else if (bold < 500) + desired_weight = 0; + else if (bold < 600) + desired_weight = 0.23; + else if (bold < 700) + desired_weight = 0.3; + else if (bold < 800) + desired_weight = 0.4; + else + desired_weight = 0.62; + + std::vector fonts; @autoreleasepool { NSString *name = @(facename.c_str() + (facename[0] == '@')); + NSArray *attrs = @[ [NSFontDescriptor fontDescriptorWithFontAttributes:@{NSFontFamilyAttribute: name}], [NSFontDescriptor fontDescriptorWithFontAttributes:@{NSFontFaceAttribute: name}], @@ -64,34 +158,10 @@ CollectionResult CoreTextFontFileLister::GetFontPaths(std::string const& facenam auto font_collection = [NSFontCollection fontCollectionWithDescriptors:attrs]; for (NSFontDescriptor *desc in font_collection.matchingDescriptors) { - process_font(ret, desc, bold, italic, characters); - - // If we didn't get any results, check for non-bold/italic variants - // and collect them with a warning - if (ret.paths.empty() && bold) { - process_font(ret, desc, 0, italic, characters); - if (!ret.paths.empty()) - ret.fake_bold = true; - } - - if (ret.paths.empty() && italic) { - process_font(ret, desc, bold, false, characters); - if (!ret.paths.empty()) - ret.fake_italic = true; - } - - if (ret.paths.empty() && bold && italic) { - process_font(ret, desc, 0, false, characters); - if (!ret.paths.empty()) { - ret.fake_bold = true; - ret.fake_italic = true; - } - } + process_descriptor(desc, fonts); } + select_best(ret, fonts, desired_weight, italic, characters); } - sort(begin(ret.paths), end(ret.paths)); - ret.paths.erase(unique(begin(ret.paths), end(ret.paths)), end(ret.paths)); - return ret; }