ClassicMacTypography/BitmapFont.m

284 lines
9.2 KiB
Objective-C

//
// BitmapFont.m
// Copyright © 2018 Ben Zotto. All rights reserved.
//
#import "BitmapFont.h"
//
// The FONT resource format is from the 1985 edition of Apple's Inside Macintosh
// documentation Volume I (Font Manager).
//
@interface BitmapFont ()
// Private accessors for public properties
@property (copy) NSString * name;
@property (assign) NSInteger size;
@property (assign) BOOL isProportional;
@property (assign) NSInteger firstChar;
@property (assign) NSInteger lastChar;
@property (assign) NSInteger widMax;
@property (assign) NSInteger kernMax;
@property (assign) NSInteger nDescent;
@property (assign) NSInteger fRectWidth;
@property (assign) NSInteger fRectHeight;
@property (assign) NSInteger ascent;
@property (assign) NSInteger descent;
@property (assign) NSInteger leading;
// Private data
@property (strong) NSDictionary * charImageMap;
@property (strong) CharacterImage * missingCharImage;
@end
// Declarations of utility functions at the end of this file.
static NSString * FontNameFromResourceId(uint16 resourceId);
static NSInteger FontSizeFromResourceId(uint16 resourceId);
@implementation BitmapFont
+ (instancetype)fontFromResourceData:(NSData *)data withResourceIdString:(NSString *)resourceIdStr
{
if (!data || !resourceIdStr) {
return nil;
}
BitmapFont * font = [[BitmapFont alloc] init];
// Extract the name and size from the resource ID.
NSInteger resourceId = [resourceIdStr integerValue];
if (resourceId == 0 || resourceId > 0xFFFF) {
// 0 would be invalid and is also the sentinel from -integerValue that it
// couldn't parse, so safe to bail.
NSLog(@"Failed to parse resource ID %@", resourceIdStr);
return nil;
}
uint16 resourceIdShort = (uint16)resourceId;
// If the high bit is nonzero, it's invalid.
if ((resourceIdShort & 0x8000) != 0) {
NSLog(@"Failed to parse resource ID %@", resourceIdStr);
return nil;
}
font.name = FontNameFromResourceId(resourceIdShort);
font.size = FontSizeFromResourceId(resourceIdShort);
if (font.size < 0) {
return nil;
}
// Parse the resource data
if (data.length < 26) {
return nil;
}
uint8 * resourceData = (uint8 *)data.bytes;
uint16 fontType = OSReadBigInt16(resourceData, 0);
if (fontType == 0x9000) {
font.isProportional = YES;
} else if (fontType == 0xB000) {
font.isProportional = NO;
} else if (fontType == 0xACB0) {
NSLog(@"FWID width-only resources not supported.");
return nil;
} else {
NSLog(@"Unknown fontType record value");
return nil;
}
font.firstChar = OSReadBigInt16(resourceData, 2);
font.lastChar = OSReadBigInt16(resourceData, 4);
if (font.lastChar > 0xFF) {
NSLog(@"lastChar is > 255");
return nil;
}
NSUInteger charactersInFont = font.lastChar - font.firstChar + 1;
// The 16-bit words values are generally signed. Make sure we don't drop the sign
// in the expansion to native intgeters.
#define ReadBigSignedInt16(base, byteOffset) ((NSInteger)(int16_t)OSReadBigInt16(base, byteOffset))
font.widMax = ReadBigSignedInt16(resourceData, 6);
font.kernMax = ReadBigSignedInt16(resourceData, 8);
if (font.kernMax > 0) {
NSLog(@"kernMax is not negative (or zero)");
return nil;
}
font.nDescent = ReadBigSignedInt16(resourceData, 10);
font.fRectWidth = ReadBigSignedInt16(resourceData, 12);
font.fRectHeight = ReadBigSignedInt16(resourceData, 14);
if (font.fRectHeight > 127 || font.fRectWidth > 254) {
NSLog(@"font rectangle exceeds max dimension");
return nil;
}
font.ascent = ReadBigSignedInt16(resourceData, 18);
font.descent = ReadBigSignedInt16(resourceData, 20);
font.leading = ReadBigSignedInt16(resourceData, 22);
NSUInteger rowWords = OSReadBigInt16(resourceData, 24);
// Sanity check that we're not going to run off the end of the data.
// if (data.length != 26 + (rowWords * 2 * font.fRectHeight) + ((charactersInFont + 2) * 2 * 2)) {
// return nil;
// }
uint8 * bitImagePtr = &resourceData[26];
uint16 * locTablePtr = ((uint16 *)bitImagePtr) + (rowWords * font.fRectHeight);
uint16 * owTablePtr = locTablePtr + (charactersInFont + 2);
NSUInteger locTable[255 + 2] = {0};
uint16 owTable[255 + 2] = {0};
for (int i = 0; i < (charactersInFont + 2); i++) {
locTable[i] = OSReadBigInt16(locTablePtr, i * 2);
owTable[i] = OSReadBigInt16(owTablePtr, i * 2);
}
#undef ReadBigSignedInt16
// Find and extract the special missing character image so we can use it when needed.
uint16 missingLoc = locTable[charactersInFont];
uint16 missingImageWidth = locTable[charactersInFont + 1] - missingLoc;
uint16 missingOffsetWidth = owTable[charactersInFont];
font.missingCharImage = [CharacterImage imageWithWidth:(missingOffsetWidth & 0x00FF)
offset:((missingOffsetWidth >> 8) & 0x00FF)
rectSize:UIntSizeMake(missingImageWidth, font.fRectHeight)
fromBitmap:bitImagePtr
bitmapStartLocation:missingLoc
bitmapStride:(rowWords * 2)];
NSMutableDictionary * charImageMap = [[NSMutableDictionary alloc] init];
for (NSInteger i = 0; i < 256; i++) {
// Are we out of range?
if (i < font.firstChar || i > font.lastChar) {
continue;
}
// Does the character not exist?
uint16 ow = owTable[i - font.firstChar];
if (ow == 0xFFFF) {
// Doesn't exist
continue;
}
NSUInteger offset = (ow >> 8) & 0x00FF;
NSUInteger width = ow & 0x00FF;
NSUInteger location = locTable[i - font.firstChar];
NSUInteger nextLocation = locTable[i - font.firstChar + 1];
CharacterImage * charImage = [CharacterImage imageWithWidth:width
offset:offset
rectSize:UIntSizeMake(nextLocation - location, font.fRectHeight)
fromBitmap:bitImagePtr
bitmapStartLocation:location
bitmapStride:(rowWords * 2)];
charImageMap[[NSNumber numberWithInteger:i]] = charImage;
}
font.charImageMap = charImageMap;
return font;
}
- (id)init
{
if ((self = [super init])) {
self.charImageMap = @{};
}
return self;
}
- (NSUInteger)countOfPresentCharacters
{
return self.charImageMap.count;
}
- (BOOL)characterIsPresent:(NSUInteger)character
{
return [self.charImageMap objectForKey:[NSNumber numberWithInteger:character]] != nil;
}
- (CharacterImage *)imageForCharacter:(NSUInteger)character
{
CharacterImage * charImage = [self.charImageMap objectForKey:[NSNumber numberWithInteger:character]];
if (!charImage) {
charImage = self.missingCharImage;
}
return charImage;
}
- (CharacterImage *)missingCharacterImage
{
return self.missingCharImage;
}
@end
//
// Utility functions
//
static NSString * FontNameFromResourceId(uint16 resourceId)
{
uint16 maskedNumber = (resourceId >> 7) & 0x00FF;
NSInteger fontNumber = (NSInteger)maskedNumber;
switch (fontNumber) {
case 0:
return @"Chicago";
case 1:
NSLog(@"Resource ID has unexpected font number 1 (application font)");
// Default to Geneva, which was the Mac's default for this.
return @"Geneva";
case 2:
return @"New York";
case 3:
return @"Geneva";
case 4:
return @"Monaco";
case 5:
return @"Venice";
case 6:
return @"London";
case 7:
return @"Athens";
case 8:
return @"San Francisco";
case 9:
return @"Toronto";
case 10:
// There was no apparent documented font number 10.
NSLog(@"Resource ID has unexpected font number 10");
return @"Unknown (Apple) #10";
case 11:
return @"Cairo";
case 12:
return @"Los Angeles";
case 20:
return @"Times";
case 21:
return @"Helvetica";
case 22:
return @"Courier";
case 23:
return @"Symbol";
case 24:
return @"Taliesin";
default:
if (fontNumber <= 127) {
return [NSString stringWithFormat:@"Unknown (Apple) #%ld", fontNumber];
} else {
return [NSString stringWithFormat:@"Unknown (3rd Party) #%ld", fontNumber];
}
}
}
static NSInteger FontSizeFromResourceId(uint16 resourceId)
{
uint16 maskedSize = resourceId & 0x007F;
if (maskedSize == 0) {
NSLog(@"Resource ID has invalid font size 0");
return -1;
}
return (NSInteger)maskedSize;
}