Skip to content

Instantly share code, notes, and snippets.

@jordiboehme
Created July 26, 2012 10:04
Show Gist options
  • Save jordiboehme/3181324 to your computer and use it in GitHub Desktop.
Save jordiboehme/3181324 to your computer and use it in GitHub Desktop.
UIFont from a CSS definition
//
// FontRegistry.h
// Tabris
//
// Created by Jordi Böhme López on 25.07.12.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import <Foundation/Foundation.h>
@interface FontRegistry : NSObject {
NSMutableDictionary *fonts;
}
+ (FontRegistry *)instance;
-(NSString *)fontForFamily:(NSString *)fontFamily forBold:(BOOL)isBold andItalic:(BOOL)isItalic;
@end
//
// FontRegistry.m
// Tabris
//
// Created by Jordi Böhme López on 25.07.12.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import "FontRegistry.h"
@implementation FontRegistry
- (id)init {
self = [super init];
if (self) {
fonts = [[NSMutableDictionary alloc] init];
[self initFonts];
}
return self;
}
#pragma mark - API Methods
+(FontRegistry *)instance {
static FontRegistry *instance;
@synchronized( self ) {
if( !instance ) {
instance = [[FontRegistry alloc] init];
}
return instance;
}
}
-(NSString *)fontForFamily:(NSString *)fontFamily forBold:(BOOL)isBold andItalic:(BOOL)isItalic {
NSString *key = [self computeKey:fontFamily forBold:isBold andItalic:isItalic];
return [fonts objectForKey:key];
}
#pragma mark - Internal Methods
-(void)initFonts {
NSArray *fontFamilies = [UIFont familyNames];
for (NSString *fontFamily in fontFamilies) {
[fonts setObject:[self computeFont:fontFamily forBold:NO andItalic:NO] forKey:[self computeKey:fontFamily forBold:NO andItalic:NO]];
[fonts setObject:[self computeFont:fontFamily forBold:YES andItalic:NO] forKey:[self computeKey:fontFamily forBold:YES andItalic:NO]];
[fonts setObject:[self computeFont:fontFamily forBold:NO andItalic:YES] forKey:[self computeKey:fontFamily forBold:NO andItalic:YES]];
[fonts setObject:[self computeFont:fontFamily forBold:YES andItalic:YES] forKey:[self computeKey:fontFamily forBold:YES andItalic:YES]];
}
}
-(NSString *)computeKey:(NSString *)fontFamily forBold:(BOOL)isBold andItalic:(BOOL)isItalic {
return [NSString stringWithFormat:@"%@;%d;%d", fontFamily, isBold, isItalic];
}
-(NSString *)computeFont:(NSString *)family forBold:(BOOL)isBold andItalic:(BOOL)isItalic {
NSString *result = nil;
if( isBold && isItalic ) {
result = [self findBoldItalicFontForFamily:family];
}
if( !result && isItalic ) {
result = [self findItalicFontForFamily:family];
}
if( !result && isBold ) {
result = [self findBoldFontForFamily:family];
}
if( !result ) {
result = [self findRegularFontForFamily:family];
}
return result;
}
-(NSString *)findRegularFontForFamily:(NSString *)family {
NSArray *fontNames = [UIFont fontNamesForFamilyName:family];
NSArray *sortedFontNames = [self sortFontNames:fontNames];
for (NSString *fontName in sortedFontNames) {
if( ![self isItalic:fontName] && ![self isBold:fontName] ) {
return fontName;
}
}
return [sortedFontNames objectAtIndex:0];
}
-(NSString *)findBoldFontForFamily:(NSString *)family {
NSString *result = nil;
NSArray *fontNames = [UIFont fontNamesForFamilyName:family];
NSArray *sortedFontNames = [self sortFontNames:fontNames];
for (NSString *fontName in sortedFontNames) {
if( [self isBold:fontName] && ![self isItalic:fontName] ) {
return fontName;
}
}
return result;
}
-(NSString *)findItalicFontForFamily:(NSString *)family {
NSString *result = nil;
NSArray *fontNames = [UIFont fontNamesForFamilyName:family];
NSArray *sortedFontNames = [self sortFontNames:fontNames];
for (NSString *fontName in sortedFontNames) {
if( [self isItalic:fontName] && ![self isBold:fontName] ) {
return fontName;
}
}
return result;
}
-(NSString *)findBoldItalicFontForFamily:(NSString *)family {
NSString *result = nil;
NSArray *fontNames = [UIFont fontNamesForFamilyName:family];
NSArray *sortedFontNames = [self sortFontNames:fontNames];
for (NSString *fontName in sortedFontNames) {
if( [self isItalic:fontName] && [self isBold:fontName] ) {
return fontName;
}
}
return result;
}
-(NSArray *)sortFontNames:(NSArray *)fontNames {
NSArray *sortedFontNames = [fontNames sortedArrayUsingComparator:(NSComparator)^(id obj1, id obj2) {
NSString *str1 = (NSString *)obj1;
NSString *str2 = (NSString *)obj2;
if( [str1 rangeOfString:@"Bold"].location != NSNotFound && [str2 rangeOfString:@"Black"].location != NSNotFound ) {
return NSOrderedAscending;
}
if( [str1 rangeOfString:@"Light"].location != NSNotFound || [str1 rangeOfString:@"Condensed"].location != NSNotFound ) {
return NSOrderedDescending;
}
if( [str2 rangeOfString:@"Light"].location != NSNotFound || [str2 rangeOfString:@"Condensed"].location != NSNotFound ) {
return NSOrderedAscending;
}
return (NSInteger)[str1 caseInsensitiveCompare:str2];
}];
return sortedFontNames;
}
-(BOOL)isItalic:(NSString *)fontName {
return ( [fontName rangeOfString:@"Italic"].location != NSNotFound
|| [fontName rangeOfString:@"Oblique"].location != NSNotFound
|| [fontName hasSuffix:@"It"]
|| [fontName hasSuffix:@"Ita"] );
}
-(BOOL)isBold:(NSString *)fontName {
return ( [fontName rangeOfString:@"Bold"].location != NSNotFound
|| [fontName rangeOfString:@"Black"].location != NSNotFound
|| [fontName rangeOfString:@"Wide"].location != NSNotFound
|| [fontName hasSuffix:@"-W6"]
|| ([fontName hasSuffix:@"-Medium"] && [fontName rangeOfString:@"Heiti"].location != NSNotFound ));
}
- (void)dealloc {
[fonts release];
[super dealloc];
}
@end
//
// FontRegistry_Test.h
// Tabris
//
// Created by Jordi Böhme López on 25.07.2012.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import <SenTestingKit/SenTestingKit.h>
@interface FontRegistry_Test : SenTestCase
@end
//
// FontRegistry_Test.m
// Tabris
//
// Created by Jordi Böhme López on 25.07.2012.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import "FontRegistry_Test.h"
#import "FontRegistry.h"
@implementation FontRegistry_Test
-(void)testIsSingleton {
FontRegistry *instance1 = [FontRegistry instance];
FontRegistry *instance2 = [FontRegistry instance];
STAssertEqualObjects(instance1, instance2, nil);
}
-(void)testInstance {
FontRegistry *instance = [FontRegistry instance];
STAssertNotNil(instance, nil);
}
-(void)testComputesFonts {
NSArray *fontFamilies = [UIFont familyNames];
FontRegistry *registry = [FontRegistry instance];
for (NSString *fontFamily in fontFamilies) {
STAssertNotNil([registry fontForFamily:fontFamily forBold:NO andItalic:NO], nil);
STAssertNotNil([registry fontForFamily:fontFamily forBold:YES andItalic:NO], nil);
STAssertNotNil([registry fontForFamily:fontFamily forBold:NO andItalic:YES], nil);
STAssertNotNil([registry fontForFamily:fontFamily forBold:YES andItalic:YES], nil);
}
}
@end
//
// FontResolver.h
// Tabris
//
// Created by Jordi Böhme López on 23.07.12.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import <Foundation/Foundation.h>
@interface FontResolver : NSObject
+(UIFont*)fontWithDescription:(NSString*)description;
@end
//
// FontResolver.m
// Tabris
//
// Created by Jordi Böhme López on 23.07.12.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import "FontResolver.h"
#import "FontRegistry.h"
@implementation FontResolver
NSString *const FONT_FAMILY = @"font-family";
NSString *const FONT_SIZE = @"font-size";
NSString *const FONT_WEIGHT = @"font-weight";
NSString *const FONT_STYLE = @"font-style";
NSString *const FAMILY_SANS_SERIF = @"Helvetica";
NSString *const FAMILY_SERIF = @"Times New Roman";
NSString *const FAMILY_MONOSPACE = @"Courier New";
CGFloat const DEFAULT_FONT_SIZE = 12;
#pragma mark - API Methods
+(UIFont*)fontWithDescription:(NSString*)description {
UIFont *result = nil;
CGFloat fontSize = [[self extractSize:description] floatValue];
BOOL isBold = [[self extractWeight:description] boolValue];
BOOL isItalic = [[self extractStyle:description] boolValue];
result = [self computeFont:description forSize:fontSize andBold:isBold andItalic:isItalic];
if( !result ) {
result = [self computeFallbackFontForSize:fontSize andBold:isBold andItalic:isItalic];
}
return result;
}
#pragma mark - Internal Methods
+(UIFont *)computeFont:(NSString *)description forSize:(CGFloat)size andBold:(BOOL)isBold andItalic:(BOOL)isItalic {
UIFont *result = nil;
NSString *fontName = [self extractFontName:description forBold:isBold andItalic:isItalic];
if( fontName ) {
UIFont *foundFont = [UIFont fontWithName:fontName size:size];
if( foundFont ) {
result = foundFont;
}
}
return result;
}
+(UIFont *)computeFallbackFontForSize:(CGFloat)size andBold:(BOOL)isBold andItalic:(BOOL)isItalic {
UIFont *result = nil;
if( isBold ) {
result = [UIFont boldSystemFontOfSize:size];
} else if( isItalic ) {
result = [UIFont italicSystemFontOfSize:size];
} else {
result = [UIFont systemFontOfSize:size];
}
return result;
}
+(NSString *)extractFontName:(NSString *)description forBold:(BOOL)isBold andItalic:(BOOL)isItalic {
NSString *result = nil;
NSString *familyString = [self extractValue:FONT_FAMILY from:description];
NSArray *families = [self splitFamilies:familyString];
for (NSString *family in families) {
NSString *foundFamily = [self findFamily:family];
result = [[FontRegistry instance] fontForFamily:foundFamily forBold:isBold andItalic:isItalic]; //replace this with [UIFont fontWithMarkupDescription:] when it's public
if( result ) {
return result;
}
}
return result;
}
+(NSArray *)splitFamilies:(NSString *)familiesString {
NSMutableArray *result = [NSMutableArray array];
NSArray *families = [familiesString componentsSeparatedByString:@","];
for (NSString *family in families) {
[result addObject:[self normalizeFamily:family]];
}
return result;
}
+(NSString *)normalizeFamily:(NSString *)family {
NSString *result = trim(family);
if( [[result lowercaseString] isEqualToString:@"sans-serif"] ) {
result = FAMILY_SANS_SERIF;
} else if( [[result lowercaseString] isEqualToString:@"serif"] ) {
result = FAMILY_SERIF;
} else if( [[result lowercaseString] isEqualToString:@"monospace"] ) {
result = FAMILY_MONOSPACE;
}
return result;
}
+(NSString *)findFamily:(NSString *)definedFont {
NSArray *familyNames = [UIFont familyNames];
for (NSString *familyName in familyNames) {
if( [familyName isEqualToString:definedFont] ) {
return familyName;
}
}
return nil;
}
+(NSNumber *)extractSize:(NSString *)description {
CGFloat result = DEFAULT_FONT_SIZE;
NSString *pixel = [self extractValue:FONT_SIZE from:description];
if( pixel ) {
CGFloat px = [pixel floatValue];
result = px;
}
return [NSNumber numberWithFloat:result];
}
+(NSNumber *)extractWeight:(NSString *)description {
BOOL result = NO;
NSString *foundValue = [self extractValue:FONT_WEIGHT from:description];
if( foundValue && [foundValue isEqualToString:@"bold"] ) {
result = YES;
}
return [NSNumber numberWithBool:result];
}
+(NSNumber *)extractStyle:(NSString *)description {
BOOL result = NO;
NSString *foundValue = [self extractValue:FONT_STYLE from:description];
if( foundValue && [foundValue isEqualToString:@"italic"] ) {
result = YES;
}
return [NSNumber numberWithFloat:result];
}
+(NSString *)extractValue:(NSString *)key from:(NSString *)description {
NSString *result = nil;
NSArray *components = [description componentsSeparatedByString:@";"];
for (NSString *component in components) {
NSString *keyValue = trim(component);
if( [keyValue hasPrefix:key] ) {
result = [keyValue substringFromIndex:[key length]+1];
result = trim(result);
}
}
return result;
}
NSString * trim(NSString *string) {
return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
@end
//
// FontResolver_Test.h
// Tabris
//
// Created by Jordi Böhme López on 23.07.2012.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import <SenTestingKit/SenTestingKit.h>
@interface FontResolver_Test : SenTestCase
@end
//
// FontResolver_Test.m
// Tabris Client
//
// Created by Jordi Böhme López on 23.07.2012.
// Copyright (c) 2012 EclipseSource.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
#import "FontResolver_Test.h"
#import "FontResolver.h"
@implementation FontResolver_Test
-(void)assertFont:(UIFont *)actual equals:(UIFont *)expected for:(NSString *)description {
STAssertEqualObjects([actual fontName], [expected fontName], description);
STAssertEqualObjects([NSNumber numberWithFloat:[actual pointSize]], [NSNumber numberWithFloat:[expected pointSize]], description);
}
- (void)compareWithDefault:(NSString *)description {
UIFont *expected = [UIFont performSelector:@selector(fontWithMarkupDescription:) withObject:description];
UIFont *actual = [FontResolver fontWithDescription:description];
[self assertFont:actual equals:expected for:description];
}
-(void)testExtractSize {
NSString *description = @"font-size: 24px;";
NSNumber *result = [FontResolver performSelector:@selector(extractSize:) withObject:description];
STAssertEqualObjects([NSNumber numberWithFloat:24], result, nil);
}
-(void)testExtractWeight {
NSString *description = @"font-weight: bold;";
NSNumber *result = [FontResolver performSelector:@selector(extractWeight:) withObject:description];
STAssertTrue([result boolValue], nil);
}
-(void)testExtractNoBoldWeight {
NSString *description = @"font-weight: bla;";
NSNumber *result = [FontResolver performSelector:@selector(extractWeight:) withObject:description];
STAssertFalse([result boolValue], nil);
}
-(void)testExtractNoWeight {
NSString *description = @"font-family: Arial; font-size: 24px;";
NSNumber *result = [FontResolver performSelector:@selector(extractWeight:) withObject:description];
STAssertFalse([result boolValue], nil);
}
-(void)testExtractStyle {
NSString *description = @"font-style: italic;";
NSNumber *result = [FontResolver performSelector:@selector(extractStyle:) withObject:description];
STAssertTrue([result boolValue], nil);
}
-(void)testExtractNoItalicStyle {
NSString *description = @"font-style: bla;";
NSNumber *result = [FontResolver performSelector:@selector(extractStyle:) withObject:description];
STAssertFalse([result boolValue], nil);
}
-(void)testExtractNoStyle {
NSString *description = @"font-family: Arial; font-size: 24px;";
NSNumber *result = [FontResolver performSelector:@selector(extractStyle:) withObject:description];
STAssertFalse([result boolValue], nil);
}
-(void)testRegularFallbackFontWithDescription {
NSString *description = @"font-family: NonExistent; font-size: 24px;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont systemFontOfSize:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont systemFontOfSize:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testItalicFallbackFontWithDescription {
NSString *description = @"font-family: NonExistent; font-size: 24px; font-style: italic;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont italicSystemFontOfSize:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont italicSystemFontOfSize:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testBoldFallbackFontWithDescription {
NSString *description = @"font-family: NonExistent; font-size: 24px; font-weight: bold;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont boldSystemFontOfSize:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont boldSystemFontOfSize:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testBoldItalicFallbackFontWithDescription {
NSString *description = @"font-family: NonExistent; font-size: 24px; font-weight: bold; font-style: italic;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont boldSystemFontOfSize:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont boldSystemFontOfSize:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testSansSerifFontWithDescription {
NSString *description = @"font-family: sans-serif; font-size: 24px;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont systemFontOfSize:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont systemFontOfSize:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testSerifFontWithDescription {
NSString *description = @"font-family: serif; font-size: 24px;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont fontWithName:@"Times New Roman" size:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont fontWithName:@"Times New Roman" size:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testMonospaceFontWithDescription {
NSString *description = @"font-family: monospace; font-size: 24px;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont fontWithName:@"Courier New" size:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont fontWithName:@"Courier New" size:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testMultipleFontsWithDescription {
NSString *description = @"font-family: blubb, monospace; font-size: 24px;";
UIFont *actual = [FontResolver fontWithDescription:description];
STAssertEqualObjects( [[UIFont fontWithName:@"Courier New" size:24] fontName], [actual fontName], nil);
STAssertEqualObjects( [NSNumber numberWithFloat:[[UIFont fontWithName:@"Courier New" size:24] pointSize]], [NSNumber numberWithFloat:[actual pointSize]], nil);
}
-(void)testRegularFontWithDescription {
[self compareWithDefault:@"font-family: Arial; font-size: 24px;"];
}
-(void)testBoldFontWithDescription {
[self compareWithDefault:@"font-family: Arial; font-size: 24px; font-weight: bold;"];
}
-(void)testItalicFontWithDescription {
[self compareWithDefault:@"font-family: Arial; font-size: 24px; font-style: italic;"];
}
-(void)testBoldItalicFontWithDescription {
[self compareWithDefault:@"font-family: Arial; font-size: 24px; font-weight: bold; font-style: italic;"];
}
-(void)testFontThonburi {
[self compareWithDefault:@"font-family: Thonburi; font-style: italic;"];
}
-(void)testFontAcademyEngravedLET {
[self compareWithDefault:@"font-family: Academy Engraved LET; font-style: italic;"];
}
-(void)testFontMarkerFelt {
[self compareWithDefault:@"font-family: Marker Felt;"];
}
-(void)testFontMarkerFeltBold {
[self compareWithDefault:@"font-family: Marker Felt; font-weight: bold;"];
}
-(void)testFontBodoni {
[self compareWithDefault:@"font-family: Bodoni 72 Oldstyle;"];
[self compareWithDefault:@"font-family: Bodoni 72 Oldstyle; font-weight: bold;"];
[self compareWithDefault:@"font-family: Bodoni 72 Oldstyle; font-style: italic;"];
[self compareWithDefault:@"font-family: Bodoni 72 Oldstyle; font-weight: bold; font-style: italic;"];
}
-(void)testFontWithDescriptionComparedToFontWithMarkupDescription {
NSArray *familyNames = [UIFont familyNames];
for (NSString *familyName in familyNames) {
if( [[UIFont fontNamesForFamilyName:familyName] count] == 1 ) {
[self compareWithDefault:[NSString stringWithFormat:@"font-family: %@;", familyName]];
} else {
[self compareWithDefault:[NSString stringWithFormat:@"font-family: %@;", familyName]];
[self compareWithDefault:[NSString stringWithFormat:@"font-family: %@; font-weight: bold;", familyName]];
[self compareWithDefault:[NSString stringWithFormat:@"font-family: %@; font-style: italic;", familyName]];
[self compareWithDefault:[NSString stringWithFormat:@"font-family: %@; font-weight: bold; font-style: italic;", familyName]];
}
}
}
@end
@icanzilb
Copy link

I had to cast return [str1 caseInsensitiveCompare:str2]; to NSInteger to clear a compile error

@jordiboehme
Copy link
Author

I updated the code. Better cast to NSComparisonResult which is an NSInteger (typedef NSInteger NSComparisonResult;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment