blob: 7144caba9efe1836931f401ca6d0a3a4fc333acf [file] [log] [blame]
/*
* Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
* Copyright (C) 2007 Nicholas Shanks <webkit@nickshanks.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.h"
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#import <math.h>
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/stl_util.h"
#include "third_party/blink/renderer/platform/fonts/font_cache.h"
#import "third_party/blink/renderer/platform/wtf/hash_set.h"
#import "third_party/blink/renderer/platform/wtf/text/atomic_string_hash.h"
namespace blink {
namespace {
const NSFontTraitMask SYNTHESIZED_FONT_TRAITS =
(NSBoldFontMask | NSItalicFontMask);
const NSFontTraitMask IMPORTANT_FONT_TRAITS =
(NSCompressedFontMask | NSCondensedFontMask | NSExpandedFontMask |
NSItalicFontMask | NSNarrowFontMask | NSPosterFontMask |
NSSmallCapsFontMask);
BOOL AcceptableChoice(NSFontTraitMask desired_traits,
NSFontTraitMask candidate_traits) {
desired_traits &= ~SYNTHESIZED_FONT_TRAITS;
return (candidate_traits & desired_traits) == desired_traits;
}
BOOL BetterChoice(NSFontTraitMask desired_traits,
int desired_weight,
NSFontTraitMask chosen_traits,
int chosen_weight,
NSFontTraitMask candidate_traits,
int candidate_weight) {
if (!AcceptableChoice(desired_traits, candidate_traits))
return NO;
// A list of the traits we care about.
// The top item in the list is the worst trait to mismatch; if a font has this
// and we didn't ask for it, we'd prefer any other font in the family.
const NSFontTraitMask kMasks[] = {NSPosterFontMask, NSSmallCapsFontMask,
NSItalicFontMask, NSCompressedFontMask,
NSCondensedFontMask, NSExpandedFontMask,
NSNarrowFontMask, 0};
int i = 0;
NSFontTraitMask mask;
while ((mask = kMasks[i++])) {
BOOL desired = (desired_traits & mask) != 0;
BOOL chosen_has_unwanted_trait = desired != ((chosen_traits & mask) != 0);
BOOL candidate_has_unwanted_trait =
desired != ((candidate_traits & mask) != 0);
if (!candidate_has_unwanted_trait && chosen_has_unwanted_trait)
return YES;
if (!chosen_has_unwanted_trait && candidate_has_unwanted_trait)
return NO;
}
int chosen_weight_delta_magnitude = abs(chosen_weight - desired_weight);
int candidate_weight_delta_magnitude = abs(candidate_weight - desired_weight);
// If both are the same distance from the desired weight, prefer the candidate
// if it is further from medium.
if (chosen_weight_delta_magnitude == candidate_weight_delta_magnitude)
return abs(candidate_weight - 6) > abs(chosen_weight - 6);
// Otherwise, prefer the one closer to the desired weight.
return candidate_weight_delta_magnitude < chosen_weight_delta_magnitude;
}
NSFontWeight ToFontWeight(blink::FontSelectionValue font_weight) {
if (font_weight <= 50 || font_weight >= 950)
return NSFontWeightRegular;
const NSFontWeight ns_font_weights[] = {
NSFontWeightUltraLight, NSFontWeightThin, NSFontWeightLight,
NSFontWeightRegular, NSFontWeightMedium, NSFontWeightSemibold,
NSFontWeightBold, NSFontWeightHeavy, NSFontWeightBlack,
};
size_t select_weight = roundf(font_weight / 100) - 1;
DCHECK_GE(select_weight, 0ul);
DCHECK_LE(select_weight, base::size(ns_font_weights));
return ns_font_weights[select_weight];
}
} // namespace
NSFont* MatchUniqueFont(const AtomicString& unique_font_name, float size) {
// Testing with a large list of fonts available on Mac OS shows that matching
// for kCTFontNameAttribute matches postscript name as well as full font name.
NSString* desired_name = unique_font_name;
NSDictionary* attributes = @{
(NSString*)kCTFontNameAttribute : desired_name,
(NSString*)kCTFontSizeAttribute : @(size)
};
base::ScopedCFTypeRef<CTFontDescriptorRef> descriptor(
CTFontDescriptorCreateWithAttributes(base::mac::NSToCFCast(attributes)));
base::ScopedCFTypeRef<CTFontRef> matched_font(
CTFontCreateWithFontDescriptor(descriptor, 0, nullptr));
// CoreText will usually give us *something* but not always an exactly
// matched font.
DCHECK(matched_font);
base::ScopedCFTypeRef<CFStringRef> matched_font_ps_name(
CTFontCopyName(matched_font, kCTFontPostScriptNameKey));
base::ScopedCFTypeRef<CFStringRef> matched_font_full_font_name(
CTFontCopyName(matched_font, kCTFontFullNameKey));
// If the found font does not match in postscript name or full font name, it's
// not the exact match that is required, so return nullptr.
if (kCFCompareEqualTo != CFStringCompare(matched_font_ps_name,
base::mac::NSToCFCast(desired_name),
kCFCompareCaseInsensitive) &&
kCFCompareEqualTo != CFStringCompare(matched_font_full_font_name,
base::mac::NSToCFCast(desired_name),
kCFCompareCaseInsensitive)) {
return nullptr;
}
return [base::mac::CFToNSCast(matched_font.release()) autorelease];
}
// Family name is somewhat of a misnomer here. We first attempt to find an
// exact match comparing the desiredFamily to the PostScript name of the
// installed fonts. If that fails we then do a search based on the family
// names of the installed fonts.
NSFont* MatchNSFontFamily(const AtomicString& desired_family_string,
NSFontTraitMask desired_traits,
FontSelectionValue desired_weight,
float size) {
DCHECK_NE(desired_family_string, FontCache::LegacySystemFontFamily());
if (desired_family_string == font_family_names::kSystemUi) {
NSFont* font = [NSFont systemFontOfSize:size
weight:ToFontWeight(desired_weight)];
if (desired_traits & IMPORTANT_FONT_TRAITS)
font = [[NSFontManager sharedFontManager] convertFont:font
toHaveTrait:desired_traits];
return font;
}
NSString* desired_family = desired_family_string;
NSFontManager* font_manager = [NSFontManager sharedFontManager];
// From Mac OS 10.15 [NSFontManager availableFonts] does not list certain
// fonts that availableMembersOfFontFamily actually shows results for, for
// example "Hiragino Kaku Gothic ProN" is not listed, only Hiragino Sans is
// listed. We previously enumerated availableFontFamilies and looked for a
// case-insensitive string match here, but instead, we can rely on
// availableMembersOfFontFamily here to do a case-insensitive comparison, then
// set available_family to desired_family if the result was not empty.
// See https://crbug.com/1000542
NSString* available_family = nil;
NSArray* fonts_in_family =
[font_manager availableMembersOfFontFamily:desired_family];
if (fonts_in_family && [fonts_in_family count]) {
available_family = desired_family;
}
int app_kit_font_weight = ToAppKitFontWeight(desired_weight);
if (!available_family) {
// Match by PostScript name.
NSEnumerator* available_fonts =
[[font_manager availableFonts] objectEnumerator];
NSString* available_font;
NSFont* name_matched_font = nil;
NSFontTraitMask desired_traits_for_name_match =
desired_traits | (app_kit_font_weight >= 7 ? NSBoldFontMask : 0);
while ((available_font = [available_fonts nextObject])) {
if ([desired_family caseInsensitiveCompare:available_font] ==
NSOrderedSame) {
name_matched_font = [NSFont fontWithName:available_font size:size];
// Special case Osaka-Mono. According to <rdar://problem/3999467>, we
// need to treat Osaka-Mono as fixed pitch.
if ([desired_family caseInsensitiveCompare:@"Osaka-Mono"] ==
NSOrderedSame &&
desired_traits_for_name_match == 0)
return name_matched_font;
NSFontTraitMask traits = [font_manager traitsOfFont:name_matched_font];
if ((traits & desired_traits_for_name_match) ==
desired_traits_for_name_match)
return [font_manager convertFont:name_matched_font
toHaveTrait:desired_traits_for_name_match];
available_family = [name_matched_font familyName];
break;
}
}
}
// Found a family, now figure out what weight and traits to use.
BOOL chose_font = false;
int chosen_weight = 0;
NSFontTraitMask chosen_traits = 0;
NSString* chosen_full_name = 0;
NSArray* fonts = [font_manager availableMembersOfFontFamily:available_family];
unsigned n = [fonts count];
unsigned i;
for (i = 0; i < n; i++) {
NSArray* font_info = [fonts objectAtIndex:i];
// Array indices must be hard coded because of lame AppKit API.
NSString* font_full_name = [font_info objectAtIndex:0];
NSInteger font_weight = [[font_info objectAtIndex:2] intValue];
NSFontTraitMask font_traits =
[[font_info objectAtIndex:3] unsignedIntValue];
BOOL new_winner;
if (!chose_font)
new_winner = AcceptableChoice(desired_traits, font_traits);
else
new_winner =
BetterChoice(desired_traits, app_kit_font_weight, chosen_traits,
chosen_weight, font_traits, font_weight);
if (new_winner) {
chose_font = YES;
chosen_weight = font_weight;
chosen_traits = font_traits;
chosen_full_name = font_full_name;
if (chosen_weight == app_kit_font_weight &&
(chosen_traits & IMPORTANT_FONT_TRAITS) ==
(desired_traits & IMPORTANT_FONT_TRAITS))
break;
}
}
if (!chose_font)
return nil;
NSFont* font = [NSFont fontWithName:chosen_full_name size:size];
if (!font)
return nil;
NSFontTraitMask actual_traits = 0;
if (desired_traits & NSFontItalicTrait)
actual_traits = [font_manager traitsOfFont:font];
int actual_weight = [font_manager weightOfFont:font];
bool synthetic_bold = app_kit_font_weight >= 7 && actual_weight < 7;
bool synthetic_italic = (desired_traits & NSFontItalicTrait) &&
!(actual_traits & NSFontItalicTrait);
// There are some malformed fonts that will be correctly returned by
// -fontWithFamily:traits:weight:size: as a match for a particular trait,
// though -[NSFontManager traitsOfFont:] incorrectly claims the font does not
// have the specified trait. This could result in applying
// synthetic bold on top of an already-bold font, as reported in
// <http://bugs.webkit.org/show_bug.cgi?id=6146>. To work around this
// problem, if we got an apparent exact match, but the requested traits
// aren't present in the matched font, we'll try to get a font from the same
// family without those traits (to apply the synthetic traits to later).
NSFontTraitMask non_synthetic_traits = desired_traits;
if (synthetic_bold)
non_synthetic_traits &= ~NSBoldFontMask;
if (synthetic_italic)
non_synthetic_traits &= ~NSItalicFontMask;
if (non_synthetic_traits != desired_traits) {
NSFont* font_without_synthetic_traits =
[font_manager fontWithFamily:available_family
traits:non_synthetic_traits
weight:chosen_weight
size:size];
if (font_without_synthetic_traits)
font = font_without_synthetic_traits;
}
return font;
}
int ToAppKitFontWeight(FontSelectionValue font_weight) {
float weight = font_weight;
if (weight <= 50 || weight >= 950)
return 5;
size_t select_weight = roundf(weight / 100) - 1;
static int app_kit_font_weights[] = {
2, // FontWeight100
3, // FontWeight200
4, // FontWeight300
5, // FontWeight400
6, // FontWeight500
8, // FontWeight600
9, // FontWeight700
10, // FontWeight800
12, // FontWeight900
};
DCHECK_GE(select_weight, 0ul);
DCHECK_LE(select_weight, base::size(app_kit_font_weights));
return app_kit_font_weights[select_weight];
}
} // namespace blink