mirror of
https://github.com/bzotto/ClassicMacTypography.git
synced 2025-02-17 07:31:24 +00:00
Add classes
This commit is contained in:
parent
a41c0b4e14
commit
98754a90f5
35
BitmapFont.h
Normal file
35
BitmapFont.h
Normal file
@ -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
|
283
BitmapFont.m
Normal file
283
BitmapFont.m
Normal file
@ -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;
|
||||
}
|
||||
|
28
CharacterImage.h
Normal file
28
CharacterImage.h
Normal file
@ -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
|
84
CharacterImage.m
Normal file
84
CharacterImage.m
Normal file
@ -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
|
19
MacRomanString.h
Normal file
19
MacRomanString.h
Normal file
@ -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
|
78
MacRomanString.m
Normal file
78
MacRomanString.m
Normal file
@ -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
|
38
SimpleBitmapRenderer.h
Normal file
38
SimpleBitmapRenderer.h
Normal file
@ -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
|
322
SimpleBitmapRenderer.m
Normal file
322
SimpleBitmapRenderer.m
Normal file
@ -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
|
||||
|
29
UIntTypes.h
Normal file
29
UIntTypes.h
Normal file
@ -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 */
|
21
UIntTypes.m
Normal file
21
UIntTypes.m
Normal file
@ -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…
x
Reference in New Issue
Block a user