Add classes

This commit is contained in:
bzotto 2018-05-21 15:04:40 -07:00
parent a41c0b4e14
commit 98754a90f5
10 changed files with 937 additions and 0 deletions

35
BitmapFont.h Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}