Add classes
This commit is contained in:
parent
a41c0b4e14
commit
98754a90f5
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// BitmapFont.h
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "CharacterImage.h"
|
||||||
|
|
||||||
|
@interface BitmapFont : NSObject
|
||||||
|
@property (readonly) NSString * name; // Text name of font if known, derived from ID.
|
||||||
|
@property (readonly) NSInteger size; // Point size (pixels), derived from ID.
|
||||||
|
@property (readonly) BOOL isProportional; // YES if proportional; NO if fixed-width.
|
||||||
|
@property (readonly) NSInteger firstChar; // ASCII code of first character
|
||||||
|
@property (readonly) NSInteger lastChar; // ASCII code of last character
|
||||||
|
@property (readonly) NSInteger widMax; // maximum character width
|
||||||
|
@property (readonly) NSInteger kernMax; // negative of maximum character kern
|
||||||
|
@property (readonly) NSInteger nDescent; // negative of descent
|
||||||
|
@property (readonly) NSInteger fRectWidth; // width of font rectangle
|
||||||
|
@property (readonly) NSInteger fRectHeight; // height of font rectangle
|
||||||
|
@property (readonly) NSInteger ascent; // ascent
|
||||||
|
@property (readonly) NSInteger descent; // descent
|
||||||
|
@property (readonly) NSInteger leading; // leading
|
||||||
|
|
||||||
|
// Data is assumed to be an original Macintosh FONT resource in its original packed,
|
||||||
|
// big-endian format, as documented in Inside Macintosh. The resource ID is assumed
|
||||||
|
// to be a string containing only numeric digits. This routine does basic sanity
|
||||||
|
// checking on input data but is NOT exhaustively hardened against e.g. corrupt or
|
||||||
|
// maliciously-crafted data.
|
||||||
|
+ (instancetype)fontFromResourceData:(NSData *)data withResourceIdString:(NSString *)resourceId;
|
||||||
|
|
||||||
|
- (NSUInteger)countOfPresentCharacters;
|
||||||
|
- (BOOL)characterIsPresent:(NSUInteger)character;
|
||||||
|
- (CharacterImage *)imageForCharacter:(NSUInteger)character;
|
||||||
|
- (CharacterImage *)missingCharacterImage;
|
||||||
|
@end
|
|
@ -0,0 +1,283 @@
|
||||||
|
//
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// CharacterImage.h
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "UIntTypes.h"
|
||||||
|
|
||||||
|
@interface CharacterImage : NSObject
|
||||||
|
@property (readonly) NSUInteger characterWidth;
|
||||||
|
@property (readonly) NSInteger characterOffset;
|
||||||
|
@property (readonly) UIntSize characterRectSize;
|
||||||
|
|
||||||
|
+ (instancetype)imageWithWidth:(NSUInteger)width
|
||||||
|
offset:(NSInteger)offset
|
||||||
|
rectSize:(UIntSize)size // size of character rect in bits (image width x font height)
|
||||||
|
fromBitmap:(uint8 *)bitmap // base pointer to image bitmap to copy from
|
||||||
|
bitmapStartLocation:(NSUInteger)startLocation // bit location from the start where this image begins
|
||||||
|
bitmapStride:(NSUInteger)stride; // stride of bitmap pointer (in bytes)
|
||||||
|
|
||||||
|
// This is a byte-sized bitmap array of dimension characterRectSize.
|
||||||
|
// A byte value is zero for an off (transparent) pixel and nonzero for an on (solid) one.
|
||||||
|
- (uint8 *)image;
|
||||||
|
- (BOOL)isWhitespace;
|
||||||
|
|
||||||
|
// Dumps out the image as a text figure with X's and .'s.
|
||||||
|
- (NSString *)imageAsDebugString;
|
||||||
|
@end
|
|
@ -0,0 +1,84 @@
|
||||||
|
//
|
||||||
|
// CharacterImage.m
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "CharacterImage.h"
|
||||||
|
|
||||||
|
@interface CharacterImage ()
|
||||||
|
{
|
||||||
|
uint8 * _image;
|
||||||
|
}
|
||||||
|
@property (assign) NSUInteger characterWidth;
|
||||||
|
@property (assign) NSInteger characterOffset;
|
||||||
|
@property (assign) UIntSize characterRectSize;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation CharacterImage
|
||||||
|
+ (instancetype)imageWithWidth:(NSUInteger)width
|
||||||
|
offset:(NSInteger)offset
|
||||||
|
rectSize:(UIntSize)size // size of character rect in bits (image width x font height)
|
||||||
|
fromBitmap:(uint8 *)bitmap // base pointer to image bitmap to copy from
|
||||||
|
bitmapStartLocation:(NSUInteger)startLocation // bit location from the start where this image begins
|
||||||
|
bitmapStride:(NSUInteger)stride // stride of bitmap pointer (in bytes)
|
||||||
|
{
|
||||||
|
CharacterImage * image = [[CharacterImage alloc] init];
|
||||||
|
image.characterWidth = width;
|
||||||
|
image.characterOffset = offset;
|
||||||
|
image.characterRectSize = size;
|
||||||
|
NSUInteger bitmapsize = size.width * size.height * sizeof(uint8);
|
||||||
|
if (bitmapsize == 0) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
image->_image = malloc(bitmapsize);
|
||||||
|
if (!image->_image) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSUInteger y = 0; y < size.height; y++) {
|
||||||
|
for (NSUInteger x = 0; x < size.width; x++) {
|
||||||
|
NSUInteger loc = startLocation + x;
|
||||||
|
uint8 byte = bitmap[(stride * y) + (loc / 8)];
|
||||||
|
uint8 value = (byte >> (7 - (loc % 8))) & 0x01;
|
||||||
|
image->_image[y * size.width + x] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
if (_image) {
|
||||||
|
free(_image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (uint8 *)image
|
||||||
|
{
|
||||||
|
return _image;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isWhitespace
|
||||||
|
{
|
||||||
|
return self.characterRectSize.width == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)imageAsDebugString
|
||||||
|
{
|
||||||
|
NSMutableString * str = [[NSMutableString alloc] init];
|
||||||
|
for (int y = 0; y < self.characterRectSize.height; y++) {
|
||||||
|
for (int x = 0; x < self.characterRectSize.width; x++) {
|
||||||
|
int value = self.image[y * self.characterRectSize.width + x];
|
||||||
|
if (value) {
|
||||||
|
[str appendString:@"X"];
|
||||||
|
} else {
|
||||||
|
[str appendString:@"."];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[str appendString:@"\n"];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
@end
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// MacRomanString.h
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@interface MacRomanString : NSObject
|
||||||
|
@property (readonly) NSUInteger length;
|
||||||
|
- (id)initWithCString:(const char *)cstring;
|
||||||
|
- (id)initWithCChars:(const char *)cchars length:(NSUInteger)length;
|
||||||
|
- (unsigned char)characterAtIndex:(NSUInteger)index;
|
||||||
|
- (MacRomanString *)substringWithRange:(NSRange)range;
|
||||||
|
@end
|
||||||
|
|
||||||
|
// Handy category for NSString
|
||||||
|
@interface NSString (MacRomanString)
|
||||||
|
- (MacRomanString *)macRomanString;
|
||||||
|
@end
|
|
@ -0,0 +1,78 @@
|
||||||
|
//
|
||||||
|
// MacRomanString.m
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MacRomanString.h"
|
||||||
|
|
||||||
|
@interface MacRomanString ()
|
||||||
|
{
|
||||||
|
unsigned char * _cstring;
|
||||||
|
NSUInteger _length;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MacRomanString
|
||||||
|
- (id)initWithCString:(const char *)cstring
|
||||||
|
{
|
||||||
|
if (!cstring) return nil;
|
||||||
|
if ((self = [super init])) {
|
||||||
|
self->_length = strlen(cstring);
|
||||||
|
self->_cstring = (unsigned char *)strdup(cstring);
|
||||||
|
if (!self->_cstring) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)initWithCChars:(const char *)cchars length:(NSUInteger)length
|
||||||
|
{
|
||||||
|
if (!cchars) return nil;
|
||||||
|
if ((self = [super init])) {
|
||||||
|
self->_length = length;
|
||||||
|
self->_cstring = malloc(length + 1);
|
||||||
|
if (!self->_cstring) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
memcpy(self->_cstring, cchars, length);
|
||||||
|
self->_cstring[length] = '\0';
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
if (_cstring) {
|
||||||
|
free(_cstring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- (unsigned char)characterAtIndex:(NSUInteger)index
|
||||||
|
{
|
||||||
|
assert(index < self->_length);
|
||||||
|
return _cstring[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MacRomanString *)substringWithRange:(NSRange)range
|
||||||
|
{
|
||||||
|
if (range.location + range.length > self->_length) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
return [[MacRomanString alloc] initWithCChars:(const char *)&_cstring[range.location] length:range.length];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
//
|
||||||
|
// Category
|
||||||
|
//
|
||||||
|
|
||||||
|
@implementation NSString (MacRomanString)
|
||||||
|
- (MacRomanString *)macRomanString
|
||||||
|
{
|
||||||
|
const char * cstr = [self cStringUsingEncoding:NSMacOSRomanStringEncoding];
|
||||||
|
if (cstr) {
|
||||||
|
return [[MacRomanString alloc] initWithCString:cstr];
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
@end
|
|
@ -0,0 +1,38 @@
|
||||||
|
//
|
||||||
|
// SimpleBitmapRenderer.h
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "UIntTypes.h"
|
||||||
|
#import "BitmapFont.h"
|
||||||
|
#import "MacRomanString.h"
|
||||||
|
|
||||||
|
@interface SimpleBitmapRenderer : NSObject
|
||||||
|
@property (readonly) UIntSize size;
|
||||||
|
@property (strong) BitmapFont * currentFont;
|
||||||
|
|
||||||
|
- (id)initWithSize:(UIntSize)size;
|
||||||
|
|
||||||
|
// Returns the width from the leftmost origin start to the rightmost pixel for given string
|
||||||
|
// in the current font. Will not trim printable whitespace which will be
|
||||||
|
// included in measurement.
|
||||||
|
- (NSUInteger)measureWidthForString:(MacRomanString *)str;
|
||||||
|
|
||||||
|
// Render the string onto the canvas using the the origin point (at baseline
|
||||||
|
// of first character) with the current font. No line-wrapping or any layout at all.
|
||||||
|
- (void)renderString:(MacRomanString *)str atOrigin:(UIntPoint)origin;
|
||||||
|
|
||||||
|
// Layout the string inside the rect and render onto the canvas. Does
|
||||||
|
// best effort simple word wrapping. Does not pixel-clip to the rect; glyphs that will
|
||||||
|
// not fall wholly within the rect will not be rendered. The word wrapping algorithm
|
||||||
|
// is naive and doesn't handle edge cases like double-spaces.
|
||||||
|
- (void)renderString:(MacRomanString *)str inRect:(UIntRect)rect;
|
||||||
|
|
||||||
|
// Render the complete set of present characters in the current font, into the rect as possible.
|
||||||
|
- (void)renderCharSetInRect:(UIntRect)rect;
|
||||||
|
|
||||||
|
// Retrieving images of the rendered "canvas".
|
||||||
|
- (NSString *)bitmapImageAsString;
|
||||||
|
- (NSData *)bitmapImageAsPNGDataWithScale:(NSUInteger)scale showingGrid:(BOOL)showGrid;
|
||||||
|
@end
|
|
@ -0,0 +1,322 @@
|
||||||
|
//
|
||||||
|
// BitmapTextRenderer.m
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import "SimpleBitmapRenderer.h"
|
||||||
|
|
||||||
|
@interface SimpleBitmapRenderer ()
|
||||||
|
{
|
||||||
|
uint8 * _canvas;
|
||||||
|
}
|
||||||
|
@property (assign) UIntSize size;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation SimpleBitmapRenderer
|
||||||
|
- (id)initWithSize:(UIntSize)size
|
||||||
|
{
|
||||||
|
if ((self = [super init])) {
|
||||||
|
_canvas = malloc(size.width * size.height);
|
||||||
|
if (!_canvas) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
memset(_canvas, 0, size.width * size.height);
|
||||||
|
self.size = size;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
if (_canvas) {
|
||||||
|
free(_canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)measureWidthForString:(MacRomanString *)str
|
||||||
|
{
|
||||||
|
if (!self.currentFont) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
NSUInteger width = 0;
|
||||||
|
for (int i = 0; i < str.length; i++) {
|
||||||
|
NSUInteger ch = [str characterAtIndex:i];
|
||||||
|
CharacterImage * charImage = [self.currentFont imageForCharacter:ch];
|
||||||
|
if (i == str.length-1) {
|
||||||
|
width += charImage.characterRectSize.width;
|
||||||
|
} else {
|
||||||
|
if (self.currentFont.isProportional) {
|
||||||
|
width += charImage.characterWidth;
|
||||||
|
} else {
|
||||||
|
width += self.currentFont.widMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIntPoint)renderCharacter:(CharacterImage *)image atPenLocation:(UIntPoint)location
|
||||||
|
{
|
||||||
|
// How many pixels does this character kern?
|
||||||
|
// NOTE: This little computation is given by Inside Macintosh docs. But based on
|
||||||
|
// my understanding, it doesn't seem like it would be correct to use the result of
|
||||||
|
// this to shift the image blit. However, since none of the Apple fonts seem to have
|
||||||
|
// anything but zero for kernMax, it's a no-op. :shrug:
|
||||||
|
NSInteger kern = image.characterOffset + self.currentFont.kernMax;
|
||||||
|
|
||||||
|
// Blit the character image at the pen's x plus net kern, and at the pen y.
|
||||||
|
for (int y = 0; y < image.characterRectSize.height; y++) {
|
||||||
|
for (int x = 0; x < image.characterRectSize.width; x++) {
|
||||||
|
uint8 val = [image image][y * image.characterRectSize.width + x];
|
||||||
|
if (val) {
|
||||||
|
UIntPoint dest = UIntPointMake(location.x + kern + x, location.y - self.currentFont.ascent + y);
|
||||||
|
[self setPixelAtLocation:dest];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance the pen by the width of the character.
|
||||||
|
if (self.currentFont.isProportional) {
|
||||||
|
location.x += image.characterWidth;
|
||||||
|
} else {
|
||||||
|
location.x += self.currentFont.widMax;
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)renderString:(MacRomanString *)str atOrigin:(UIntPoint)origin
|
||||||
|
{
|
||||||
|
if (!self.currentFont) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UIntPoint pen = origin;
|
||||||
|
for (int i = 0; i < str.length; i++) {
|
||||||
|
NSUInteger ch = [str characterAtIndex:i];
|
||||||
|
CharacterImage * charImage = [self.currentFont imageForCharacter:ch];
|
||||||
|
pen = [self renderCharacter:charImage atPenLocation:pen];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)renderString:(MacRomanString *)str inRect:(UIntRect)rect
|
||||||
|
{
|
||||||
|
if (!self.currentFont) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the rect isn't big enough to hold even a single character of text, bail.
|
||||||
|
if (rect.size.height < self.currentFont.fRectHeight ||
|
||||||
|
rect.size.width < self.currentFont.fRectWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (str.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Always start the pen with an "ascent" offset from the top of rect.
|
||||||
|
NSUInteger leftEdge = rect.origin.x + self.currentFont.kernMax;
|
||||||
|
UIntPoint pen = UIntPointMake(leftEdge, rect.origin.y + self.currentFont.ascent);
|
||||||
|
|
||||||
|
int wordStartIndex = 0;
|
||||||
|
while (wordStartIndex < str.length) {
|
||||||
|
// Find the next nonprinting (whitespace) character.
|
||||||
|
int nextWhitespaceIndex = wordStartIndex + 1;
|
||||||
|
for (; nextWhitespaceIndex < str.length; nextWhitespaceIndex++) {
|
||||||
|
NSUInteger ch = [str characterAtIndex:nextWhitespaceIndex];
|
||||||
|
if (ch == '\n' || ch == '\r' || [self.currentFont imageForCharacter:ch].isWhitespace) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MacRomanString * substring = [str substringWithRange:NSMakeRange(wordStartIndex, nextWhitespaceIndex - wordStartIndex)];
|
||||||
|
NSUInteger substringWidth = [self measureWidthForString:substring];
|
||||||
|
// Check for the special case of the substring being not just too big for current remaining.
|
||||||
|
// width but for the full rect width, which will require a forced break in the word.
|
||||||
|
while (substringWidth > rect.size.width && nextWhitespaceIndex > wordStartIndex) {
|
||||||
|
nextWhitespaceIndex--;
|
||||||
|
substring = [str substringWithRange:NSMakeRange(wordStartIndex, nextWhitespaceIndex - wordStartIndex)];
|
||||||
|
substringWidth = [self measureWidthForString:substring];
|
||||||
|
}
|
||||||
|
// Does substring fit in remaining space on current line?
|
||||||
|
if (pen.x + substringWidth > rect.origin.x + rect.size.width) {
|
||||||
|
// CRLF.
|
||||||
|
pen.x = leftEdge;
|
||||||
|
pen.y += (self.currentFont.descent + self.currentFont.leading + self.currentFont.ascent);
|
||||||
|
if (pen.y + self.currentFont.descent > rect.origin.y + rect.size.height) {
|
||||||
|
// next line too short so we're done early!
|
||||||
|
goto Done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Render the subtring.
|
||||||
|
for (int i = wordStartIndex; i < nextWhitespaceIndex; i++) {
|
||||||
|
CharacterImage * charImage = [self.currentFont imageForCharacter:[str characterAtIndex:i]];
|
||||||
|
pen = [self renderCharacter:charImage atPenLocation:pen];
|
||||||
|
}
|
||||||
|
// Render the sequence of whitespace, if any, after the word.
|
||||||
|
for(; nextWhitespaceIndex < str.length; nextWhitespaceIndex++) {
|
||||||
|
NSUInteger ch = [str characterAtIndex:nextWhitespaceIndex];
|
||||||
|
CharacterImage * charImage = [self.currentFont imageForCharacter:ch];
|
||||||
|
// Handle special cases CR/LF.
|
||||||
|
if (ch == '\n' || ch == '\r') {
|
||||||
|
pen.x = leftEdge;
|
||||||
|
pen.y += (self.currentFont.descent + self.currentFont.leading + self.currentFont.ascent);
|
||||||
|
if (pen.y + self.currentFont.descent > rect.origin.y + rect.size.height) {
|
||||||
|
// next line too short so we're done
|
||||||
|
goto Done;
|
||||||
|
}
|
||||||
|
} else if (!charImage.isWhitespace) {
|
||||||
|
wordStartIndex = nextWhitespaceIndex;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
pen = [self renderCharacter:charImage atPenLocation:pen];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextWhitespaceIndex >= str.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Done:
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)renderCharSetInRect:(UIntRect)rect
|
||||||
|
{
|
||||||
|
if (!self.currentFont) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the rect isn't big enough to hold even a single character of text, bail.
|
||||||
|
if (rect.size.height < self.currentFont.fRectHeight ||
|
||||||
|
rect.size.width < self.currentFont.fRectWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always start the pen with an "ascent" offset from the top of rect.
|
||||||
|
NSUInteger leftEdge = rect.origin.x + self.currentFont.kernMax;
|
||||||
|
UIntPoint pen = UIntPointMake(leftEdge, rect.origin.y + self.currentFont.ascent);
|
||||||
|
for (int ch = 1; ch < 256; ch++) {
|
||||||
|
CharacterImage * charImage = [self.currentFont imageForCharacter:ch];
|
||||||
|
if (ch == 255) {
|
||||||
|
charImage = self.currentFont.missingCharacterImage;
|
||||||
|
} else if (![self.currentFont characterIsPresent:ch] || charImage.characterRectSize.width == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NSInteger kern = charImage.characterOffset + self.currentFont.kernMax;
|
||||||
|
// Is there enough room on the canvas left to draw this character?
|
||||||
|
if (pen.x + kern + charImage.characterRectSize.width >= rect.size.width) {
|
||||||
|
pen.x = leftEdge;
|
||||||
|
pen.y += (self.currentFont.descent + self.currentFont.leading + self.currentFont.ascent);
|
||||||
|
// If we went past the edge, STOP.
|
||||||
|
if (pen.y + self.currentFont.descent > rect.origin.y + rect.size.height) {
|
||||||
|
// next line too short so we're done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pen = [self renderCharacter:charImage atPenLocation:pen];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Retrieval
|
||||||
|
//
|
||||||
|
|
||||||
|
- (NSString *)bitmapImageAsString
|
||||||
|
{
|
||||||
|
NSMutableString * str = [[NSMutableString alloc] init];
|
||||||
|
for (int y = 0; y < self.size.height; y++) {
|
||||||
|
for (int x = 0; x < self.size.width; x++) {
|
||||||
|
uint8 val = _canvas[y * self.size.width + x];
|
||||||
|
switch (val) {
|
||||||
|
case 0:
|
||||||
|
[str appendString:@" "];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
[str appendString:@"X"];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[str appendString:@"\n"];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)bitmapImageAsPNGDataWithScale:(NSUInteger)scale showingGrid:(BOOL)showGrid
|
||||||
|
{
|
||||||
|
if (scale == 0) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (scale == 1) {
|
||||||
|
showGrid = NO;
|
||||||
|
}
|
||||||
|
CGSize contextSize = CGSizeMake(self.size.width * scale, self.size.height * scale);
|
||||||
|
int bytesPerRow = contextSize.width * 4;
|
||||||
|
int bytesTotal = bytesPerRow * contextSize.height;
|
||||||
|
void * bitmapData = calloc(bytesTotal, sizeof(uint8));
|
||||||
|
if (!bitmapData) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
|
||||||
|
CGContextRef context = CGBitmapContextCreate (bitmapData,
|
||||||
|
contextSize.width,
|
||||||
|
contextSize.height,
|
||||||
|
8,
|
||||||
|
bytesPerRow,
|
||||||
|
colorSpace,
|
||||||
|
kCGImageAlphaPremultipliedLast);
|
||||||
|
CGColorSpaceRelease(colorSpace);
|
||||||
|
if (!context) {
|
||||||
|
free(bitmapData);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip the context
|
||||||
|
CGContextTranslateCTM(context, 0, contextSize.height);
|
||||||
|
CGContextScaleCTM(context, 1, -1);
|
||||||
|
|
||||||
|
// Fill with white background
|
||||||
|
CGContextSetRGBFillColor(context, 1, 1, 1, 1);
|
||||||
|
CGContextFillRect(context, CGRectMake(0, 0, contextSize.width, contextSize.height));
|
||||||
|
|
||||||
|
for (int y = 0; y < self.size.height; y++) {
|
||||||
|
for (int x = 0; x < self.size.width; x++) {
|
||||||
|
uint8 val = _canvas[y * self.size.width + x];
|
||||||
|
// Set color
|
||||||
|
if (val) {
|
||||||
|
CGContextSetRGBFillColor(context, 0, 0, 0, 1);
|
||||||
|
} else {
|
||||||
|
if (showGrid) {
|
||||||
|
CGContextSetRGBFillColor(context, 0.9, 0.9, 0.9, 1.0);
|
||||||
|
} else {
|
||||||
|
CGContextSetRGBFillColor(context, 1, 1, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Draw
|
||||||
|
if (showGrid) {
|
||||||
|
CGRect pixel = CGRectMake(x * scale + 1, y * scale + 1, scale-1, scale-1);
|
||||||
|
CGContextFillRect(context, pixel);
|
||||||
|
} else {
|
||||||
|
CGRect pixel = CGRectMake(x * scale, y * scale, scale, scale);
|
||||||
|
CGContextFillRect(context, pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CGImageRef imageRef = CGBitmapContextCreateImage(context);
|
||||||
|
NSBitmapImageRep * newRep = [[NSBitmapImageRep alloc] initWithCGImage:imageRef];
|
||||||
|
[newRep setSize:contextSize];
|
||||||
|
NSData * pngData = [newRep representationUsingType:NSPNGFileType properties:@{}];
|
||||||
|
CGImageRelease(imageRef);
|
||||||
|
free(bitmapData);
|
||||||
|
return pngData;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private routines
|
||||||
|
//
|
||||||
|
- (void)setPixelAtLocation:(UIntPoint)pt
|
||||||
|
{
|
||||||
|
if (pt.x >= self.size.width) {
|
||||||
|
return;
|
||||||
|
} else if (pt.y >= self.size.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_canvas[pt.y * self.size.width + pt.x] = 1;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
//
|
||||||
|
// UIntTypes.h
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef UIntTypes_h
|
||||||
|
#define UIntTypes_h
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
typedef struct _UIntPoint {
|
||||||
|
NSUInteger x;
|
||||||
|
NSUInteger y;
|
||||||
|
} UIntPoint;
|
||||||
|
UIntPoint UIntPointMake(NSUInteger x, NSUInteger y);
|
||||||
|
|
||||||
|
typedef struct _UIntSize {
|
||||||
|
NSUInteger width;
|
||||||
|
NSUInteger height;
|
||||||
|
} UIntSize;
|
||||||
|
UIntSize UIntSizeMake(NSUInteger width, NSUInteger height);
|
||||||
|
|
||||||
|
typedef struct _UIntRect {
|
||||||
|
UIntPoint origin;
|
||||||
|
UIntSize size;
|
||||||
|
} UIntRect;
|
||||||
|
UIntRect UIntRectMake(NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height);
|
||||||
|
|
||||||
|
#endif /* UIntTypes_h */
|
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// UIntTypes.m
|
||||||
|
// Copyright © 2018 Ben Zotto. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "UIntTypes.h"
|
||||||
|
|
||||||
|
UIntPoint UIntPointMake(NSUInteger x, NSUInteger y)
|
||||||
|
{
|
||||||
|
UIntPoint point; point.x = x; point.y = y; return point;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIntSize UIntSizeMake(NSUInteger width, NSUInteger height)
|
||||||
|
{
|
||||||
|
UIntSize size; size.width = width; size.height = height; return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIntRect UIntRectMake(NSUInteger x, NSUInteger y, NSUInteger width, NSUInteger height)
|
||||||
|
{
|
||||||
|
UIntRect rect; rect.origin = UIntPointMake(x, y); rect.size = UIntSizeMake(width, height); return rect;
|
||||||
|
}
|
Loading…
Reference in New Issue