From 98754a90f584d25ae895bf6ce5d53412ed76551a Mon Sep 17 00:00:00 2001 From: bzotto Date: Mon, 21 May 2018 15:04:40 -0700 Subject: [PATCH] Add classes --- BitmapFont.h | 35 +++++ BitmapFont.m | 283 ++++++++++++++++++++++++++++++++++++ CharacterImage.h | 28 ++++ CharacterImage.m | 84 +++++++++++ MacRomanString.h | 19 +++ MacRomanString.m | 78 ++++++++++ SimpleBitmapRenderer.h | 38 +++++ SimpleBitmapRenderer.m | 322 +++++++++++++++++++++++++++++++++++++++++ UIntTypes.h | 29 ++++ UIntTypes.m | 21 +++ 10 files changed, 937 insertions(+) create mode 100644 BitmapFont.h create mode 100644 BitmapFont.m create mode 100644 CharacterImage.h create mode 100644 CharacterImage.m create mode 100644 MacRomanString.h create mode 100644 MacRomanString.m create mode 100644 SimpleBitmapRenderer.h create mode 100644 SimpleBitmapRenderer.m create mode 100644 UIntTypes.h create mode 100644 UIntTypes.m 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; +}