1/*
2 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3 * Copyright (C) 2007 Nicholas Shanks <webkit@nickshanks.com>
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1.  Redistributions of source code must retain the above copyright
10 *     notice, this list of conditions and the following disclaimer.
11 * 2.  Redistributions in binary form must reproduce the above copyright
12 *     notice, this list of conditions and the following disclaimer in the
13 *     documentation and/or other materials provided with the distribution.
14 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 *     its contributors may be used to endorse or promote products derived
16 *     from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30#import "config.h"
31#import "WebFontCache.h"
32
33#import "FontTraitsMask.h"
34#import <AppKit/AppKit.h>
35#import <Foundation/Foundation.h>
36#import <math.h>
37#import <wtf/UnusedParam.h>
38
39using namespace WebCore;
40
41
42#define SYNTHESIZED_FONT_TRAITS (NSBoldFontMask | NSItalicFontMask)
43
44#define IMPORTANT_FONT_TRAITS (0 \
45    | NSCompressedFontMask \
46    | NSCondensedFontMask \
47    | NSExpandedFontMask \
48    | NSItalicFontMask \
49    | NSNarrowFontMask \
50    | NSPosterFontMask \
51    | NSSmallCapsFontMask \
52)
53
54static BOOL acceptableChoice(NSFontTraitMask desiredTraits, NSFontTraitMask candidateTraits)
55{
56    desiredTraits &= ~SYNTHESIZED_FONT_TRAITS;
57    return (candidateTraits & desiredTraits) == desiredTraits;
58}
59
60static BOOL betterChoice(NSFontTraitMask desiredTraits, int desiredWeight,
61    NSFontTraitMask chosenTraits, int chosenWeight,
62    NSFontTraitMask candidateTraits, int candidateWeight)
63{
64    if (!acceptableChoice(desiredTraits, candidateTraits))
65        return NO;
66
67    // A list of the traits we care about.
68    // The top item in the list is the worst trait to mismatch; if a font has this
69    // and we didn't ask for it, we'd prefer any other font in the family.
70    const NSFontTraitMask masks[] = {
71        NSPosterFontMask,
72        NSSmallCapsFontMask,
73        NSItalicFontMask,
74        NSCompressedFontMask,
75        NSCondensedFontMask,
76        NSExpandedFontMask,
77        NSNarrowFontMask,
78        0
79    };
80
81    int i = 0;
82    NSFontTraitMask mask;
83    while ((mask = masks[i++])) {
84        BOOL desired = (desiredTraits & mask) != 0;
85        BOOL chosenHasUnwantedTrait = desired != ((chosenTraits & mask) != 0);
86        BOOL candidateHasUnwantedTrait = desired != ((candidateTraits & mask) != 0);
87        if (!candidateHasUnwantedTrait && chosenHasUnwantedTrait)
88            return YES;
89        if (!chosenHasUnwantedTrait && candidateHasUnwantedTrait)
90            return NO;
91    }
92
93    int chosenWeightDeltaMagnitude = abs(chosenWeight - desiredWeight);
94    int candidateWeightDeltaMagnitude = abs(candidateWeight - desiredWeight);
95
96    // If both are the same distance from the desired weight, prefer the candidate if it is further from medium.
97    if (chosenWeightDeltaMagnitude == candidateWeightDeltaMagnitude)
98        return abs(candidateWeight - 6) > abs(chosenWeight - 6);
99
100    // Otherwise, prefer the one closer to the desired weight.
101    return candidateWeightDeltaMagnitude < chosenWeightDeltaMagnitude;
102}
103
104// Workaround for <rdar://problem/5781372>.
105static inline void fixUpWeight(NSInteger& weight, NSString *fontName)
106{
107#ifndef BUILDING_ON_LEOPARD
108    UNUSED_PARAM(weight);
109    UNUSED_PARAM(fontName);
110#else
111    if (weight == 3 && [fontName rangeOfString:@"ultralight" options:NSCaseInsensitiveSearch | NSBackwardsSearch | NSLiteralSearch].location != NSNotFound)
112        weight = 2;
113#endif
114}
115
116static inline FontTraitsMask toTraitsMask(NSFontTraitMask appKitTraits, NSInteger appKitWeight)
117{
118    return static_cast<FontTraitsMask>(((appKitTraits & NSFontItalicTrait) ? FontStyleItalicMask : FontStyleNormalMask)
119        | FontVariantNormalMask
120        | (appKitWeight == 1 ? FontWeight100Mask :
121              appKitWeight == 2 ? FontWeight200Mask :
122              appKitWeight <= 4 ? FontWeight300Mask :
123              appKitWeight == 5 ? FontWeight400Mask :
124              appKitWeight == 6 ? FontWeight500Mask :
125              appKitWeight <= 8 ? FontWeight600Mask :
126              appKitWeight == 9 ? FontWeight700Mask :
127              appKitWeight <= 11 ? FontWeight800Mask :
128                                   FontWeight900Mask));
129}
130
131@implementation WebFontCache
132
133+ (void)getTraits:(Vector<unsigned>&)traitsMasks inFamily:(NSString *)desiredFamily
134{
135    NSFontManager *fontManager = [NSFontManager sharedFontManager];
136
137    NSEnumerator *e = [[fontManager availableFontFamilies] objectEnumerator];
138    NSString *availableFamily;
139    while ((availableFamily = [e nextObject])) {
140        if ([desiredFamily caseInsensitiveCompare:availableFamily] == NSOrderedSame)
141            break;
142    }
143
144    if (!availableFamily) {
145        // Match by PostScript name.
146        NSEnumerator *availableFonts = [[fontManager availableFonts] objectEnumerator];
147        NSString *availableFont;
148        while ((availableFont = [availableFonts nextObject])) {
149            if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
150                NSFont *font = [NSFont fontWithName:availableFont size:10];
151                NSInteger weight = [fontManager weightOfFont:font];
152                fixUpWeight(weight, desiredFamily);
153                traitsMasks.append(toTraitsMask([fontManager traitsOfFont:font], weight));
154                break;
155            }
156        }
157        return;
158    }
159
160    NSArray *fonts = [fontManager availableMembersOfFontFamily:availableFamily];
161    unsigned n = [fonts count];
162    unsigned i;
163    for (i = 0; i < n; i++) {
164        NSArray *fontInfo = [fonts objectAtIndex:i];
165        // Array indices must be hard coded because of lame AppKit API.
166        NSString *fontFullName = [fontInfo objectAtIndex:0];
167        NSInteger fontWeight = [[fontInfo objectAtIndex:2] intValue];
168        fixUpWeight(fontWeight, fontFullName);
169
170        NSFontTraitMask fontTraits = [[fontInfo objectAtIndex:3] unsignedIntValue];
171        traitsMasks.append(toTraitsMask(fontTraits, fontWeight));
172    }
173}
174
175// Family name is somewhat of a misnomer here.  We first attempt to find an exact match
176// comparing the desiredFamily to the PostScript name of the installed fonts.  If that fails
177// we then do a search based on the family names of the installed fonts.
178+ (NSFont *)internalFontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size
179{
180    NSFontManager *fontManager = [NSFontManager sharedFontManager];
181
182    // Do a simple case insensitive search for a matching font family.
183    // NSFontManager requires exact name matches.
184    // This addresses the problem of matching arial to Arial, etc., but perhaps not all the issues.
185    NSEnumerator *e = [[fontManager availableFontFamilies] objectEnumerator];
186    NSString *availableFamily;
187    while ((availableFamily = [e nextObject])) {
188        if ([desiredFamily caseInsensitiveCompare:availableFamily] == NSOrderedSame)
189            break;
190    }
191
192    if (!availableFamily) {
193        // Match by PostScript name.
194        NSEnumerator *availableFonts = [[fontManager availableFonts] objectEnumerator];
195        NSString *availableFont;
196        NSFont *nameMatchedFont = nil;
197        NSFontTraitMask desiredTraitsForNameMatch = desiredTraits | (desiredWeight >= 7 ? NSBoldFontMask : 0);
198        while ((availableFont = [availableFonts nextObject])) {
199            if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
200                nameMatchedFont = [NSFont fontWithName:availableFont size:size];
201
202                // Special case Osaka-Mono.  According to <rdar://problem/3999467>, we need to
203                // treat Osaka-Mono as fixed pitch.
204                if ([desiredFamily caseInsensitiveCompare:@"Osaka-Mono"] == NSOrderedSame && desiredTraitsForNameMatch == 0)
205                    return nameMatchedFont;
206
207                NSFontTraitMask traits = [fontManager traitsOfFont:nameMatchedFont];
208                if ((traits & desiredTraitsForNameMatch) == desiredTraitsForNameMatch)
209                    return [fontManager convertFont:nameMatchedFont toHaveTrait:desiredTraitsForNameMatch];
210
211                availableFamily = [nameMatchedFont familyName];
212                break;
213            }
214        }
215    }
216
217    // Found a family, now figure out what weight and traits to use.
218    BOOL choseFont = false;
219    int chosenWeight = 0;
220    NSFontTraitMask chosenTraits = 0;
221    NSString *chosenFullName = 0;
222
223    NSArray *fonts = [fontManager availableMembersOfFontFamily:availableFamily];
224    unsigned n = [fonts count];
225    unsigned i;
226    for (i = 0; i < n; i++) {
227        NSArray *fontInfo = [fonts objectAtIndex:i];
228
229        // Array indices must be hard coded because of lame AppKit API.
230        NSString *fontFullName = [fontInfo objectAtIndex:0];
231        NSInteger fontWeight = [[fontInfo objectAtIndex:2] intValue];
232        fixUpWeight(fontWeight, fontFullName);
233
234        NSFontTraitMask fontTraits = [[fontInfo objectAtIndex:3] unsignedIntValue];
235
236        BOOL newWinner;
237        if (!choseFont)
238            newWinner = acceptableChoice(desiredTraits, fontTraits);
239        else
240            newWinner = betterChoice(desiredTraits, desiredWeight, chosenTraits, chosenWeight, fontTraits, fontWeight);
241
242        if (newWinner) {
243            choseFont = YES;
244            chosenWeight = fontWeight;
245            chosenTraits = fontTraits;
246            chosenFullName = fontFullName;
247
248            if (chosenWeight == desiredWeight && (chosenTraits & IMPORTANT_FONT_TRAITS) == (desiredTraits & IMPORTANT_FONT_TRAITS))
249                break;
250        }
251    }
252
253    if (!choseFont)
254        return nil;
255
256    NSFont *font = [NSFont fontWithName:chosenFullName size:size];
257
258    if (!font)
259        return nil;
260
261    NSFontTraitMask actualTraits = 0;
262    if (desiredTraits & NSFontItalicTrait)
263        actualTraits = [fontManager traitsOfFont:font];
264    int actualWeight = [fontManager weightOfFont:font];
265
266    bool syntheticBold = desiredWeight >= 7 && actualWeight < 7;
267    bool syntheticOblique = (desiredTraits & NSFontItalicTrait) && !(actualTraits & NSFontItalicTrait);
268
269    // There are some malformed fonts that will be correctly returned by -fontWithFamily:traits:weight:size: as a match for a particular trait,
270    // though -[NSFontManager traitsOfFont:] incorrectly claims the font does not have the specified trait. This could result in applying
271    // 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
272    // 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
273    // the same family without those traits (to apply the synthetic traits to later).
274    NSFontTraitMask nonSyntheticTraits = desiredTraits;
275
276    if (syntheticBold)
277        nonSyntheticTraits &= ~NSBoldFontMask;
278
279    if (syntheticOblique)
280        nonSyntheticTraits &= ~NSItalicFontMask;
281
282    if (nonSyntheticTraits != desiredTraits) {
283        NSFont *fontWithoutSyntheticTraits = [fontManager fontWithFamily:availableFamily traits:nonSyntheticTraits weight:chosenWeight size:size];
284        if (fontWithoutSyntheticTraits)
285            font = fontWithoutSyntheticTraits;
286    }
287
288    return font;
289}
290
291+ (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits weight:(int)desiredWeight size:(float)size
292{
293    NSFont *font = [self internalFontWithFamily:desiredFamily traits:desiredTraits weight:desiredWeight size:size];
294    if (font)
295        return font;
296
297    // Auto activate the font before looking for it a second time.
298    // Ignore the result because we want to use our own algorithm to actually find the font.
299    [NSFont fontWithName:desiredFamily size:size];
300
301    return [self internalFontWithFamily:desiredFamily traits:desiredTraits weight:desiredWeight size:size];
302}
303
304+ (NSFont *)fontWithFamily:(NSString *)desiredFamily traits:(NSFontTraitMask)desiredTraits size:(float)size
305{
306    int desiredWeight = (desiredTraits & NSBoldFontMask) ? 9 : 5;
307    return [self fontWithFamily:desiredFamily traits:desiredTraits weight:desiredWeight size:size];
308}
309
310@end
311