diff --git a/BitmapFont.h b/BitmapFont.h new file mode 100644 index 0000000..544b393 --- /dev/null +++ b/BitmapFont.h @@ -0,0 +1,35 @@ +// +// BitmapFont.h +// Copyright © 2018 Ben Zotto. All rights reserved. +// + +#import +#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 diff --git a/BitmapFont.m b/BitmapFont.m new file mode 100644 index 0000000..ca04b5c --- /dev/null +++ b/BitmapFont.m @@ -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; +} + diff --git a/CharacterImage.h b/CharacterImage.h new file mode 100644 index 0000000..ae2e311 --- /dev/null +++ b/CharacterImage.h @@ -0,0 +1,28 @@ +// +// CharacterImage.h +// Copyright © 2018 Ben Zotto. All rights reserved. +// + +#import +#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 diff --git a/CharacterImage.m b/CharacterImage.m new file mode 100644 index 0000000..3475ff4 --- /dev/null +++ b/CharacterImage.m @@ -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 diff --git a/MacRomanString.h b/MacRomanString.h new file mode 100644 index 0000000..1306e23 --- /dev/null +++ b/MacRomanString.h @@ -0,0 +1,19 @@ +// +// MacRomanString.h +// Copyright © 2018 Ben Zotto. All rights reserved. +// + +#import + +@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 diff --git a/MacRomanString.m b/MacRomanString.m new file mode 100644 index 0000000..dc23bee --- /dev/null +++ b/MacRomanString.m @@ -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 diff --git a/SimpleBitmapRenderer.h b/SimpleBitmapRenderer.h new file mode 100644 index 0000000..76f0bb0 --- /dev/null +++ b/SimpleBitmapRenderer.h @@ -0,0 +1,38 @@ +// +// SimpleBitmapRenderer.h +// Copyright © 2018 Ben Zotto. All rights reserved. +// + +#import +#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 diff --git a/SimpleBitmapRenderer.m b/SimpleBitmapRenderer.m new file mode 100644 index 0000000..e48129b --- /dev/null +++ b/SimpleBitmapRenderer.m @@ -0,0 +1,322 @@ +// +// BitmapTextRenderer.m +// Copyright © 2018 Ben Zotto. All rights reserved. +// + +#import +#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 + diff --git a/UIntTypes.h b/UIntTypes.h new file mode 100644 index 0000000..fef363e --- /dev/null +++ b/UIntTypes.h @@ -0,0 +1,29 @@ +// +// UIntTypes.h +// Copyright © 2018 Ben Zotto. All rights reserved. +// + +#ifndef UIntTypes_h +#define UIntTypes_h + +#import + +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 */ diff --git a/UIntTypes.m b/UIntTypes.m new file mode 100644 index 0000000..022fb5e --- /dev/null +++ b/UIntTypes.m @@ -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; +}