SameBoy/HexFiend/HFRepresenterStringEncodingTextView.m

541 lines
21 KiB
Objective-C

//
// HFRepresenterStringEncodingTextView.m
// HexFiend_2
//
// Copyright 2007 ridiculous_fish. All rights reserved.
//
#import <HexFiend/HFRepresenterStringEncodingTextView.h>
#import <HexFiend/HFRepresenterTextView_Internal.h>
#include <malloc/malloc.h>
@implementation HFRepresenterStringEncodingTextView
static NSString *copy1CharStringForByteValue(unsigned long long byteValue, NSUInteger bytesPerChar, NSStringEncoding encoding) {
NSString *result = nil;
unsigned char bytes[sizeof byteValue];
/* If we are little endian, then the bytesPerChar doesn't matter, because it will all come out the same. If we are big endian, then it does matter. */
#if ! __BIG_ENDIAN__
*(unsigned long long *)bytes = byteValue;
#else
if (bytesPerChar == sizeof(uint8_t)) {
*(uint8_t *)bytes = (uint8_t)byteValue;
} else if (bytesPerChar == sizeof(uint16_t)) {
*(uint16_t *)bytes = (uint16_t)byteValue;
} else if (bytesPerChar == sizeof(uint32_t)) {
*(uint32_t *)bytes = (uint32_t)byteValue;
} else if (bytesPerChar == sizeof(uint64_t)) {
*(uint64_t *)bytes = (uint64_t)byteValue;
} else {
[NSException raise:NSInvalidArgumentException format:@"Unsupported bytesPerChar of %u", bytesPerChar];
}
#endif
/* ASCII is mishandled :( */
BOOL encodingOK = YES;
if (encoding == NSASCIIStringEncoding && bytesPerChar == 1 && bytes[0] > 0x7F) {
encodingOK = NO;
}
/* Now create a string from these bytes */
if (encodingOK) {
result = [[NSString alloc] initWithBytes:bytes length:bytesPerChar encoding:encoding];
if ([result length] > 1) {
/* Try precomposing it */
NSString *temp = [[result precomposedStringWithCompatibilityMapping] copy];
[result release];
result = temp;
}
/* Ensure it has exactly one character */
if ([result length] != 1) {
[result release];
result = nil;
}
}
/* All done */
return result;
}
static BOOL getGlyphs(CGGlyph *glyphs, NSString *string, NSFont *inputFont) {
NSUInteger length = [string length];
HFASSERT(inputFont != nil);
NEW_ARRAY(UniChar, chars, length);
[string getCharacters:chars range:NSMakeRange(0, length)];
bool result = CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars, glyphs, length);
/* A NO return means some or all characters were not mapped. This is OK. We'll use the replacement glyph. Unless we're calculating the replacement glyph! Hmm...maybe we should have a series of replacement glyphs that we try? */
////////////////////////
// Workaround for a Mavericks bug. Still present as of 10.9.5
// TODO: Hmm, still? Should look into this again, either it's not a bug or Apple needs a poke.
if(!result) for(NSUInteger i = 0; i < length; i+=15) {
CFIndex x = length-i;
if(x > 15) x = 15;
result = CTFontGetGlyphsForCharacters((CTFontRef)inputFont, chars+i, glyphs+i, x);
if(!result) break;
}
////////////////////////
FREE_ARRAY(chars);
return result;
}
static void generateGlyphs(NSFont *baseFont, NSMutableArray *fonts, struct HFGlyph_t *outGlyphs, NSInteger bytesPerChar, NSStringEncoding encoding, const NSUInteger *charactersToLoad, NSUInteger charactersToLoadCount, CGFloat *outMaxAdvance) {
/* If the caller wants the advance, initialize it to 0 */
if (outMaxAdvance) *outMaxAdvance = 0;
/* Invalid glyph marker */
const struct HFGlyph_t invalidGlyph = {.fontIndex = kHFGlyphFontIndexInvalid, .glyph = -1};
NSCharacterSet *coveredSet = [baseFont coveredCharacterSet];
NSMutableString *coveredGlyphFetchingString = [[NSMutableString alloc] init];
NSMutableIndexSet *coveredGlyphIndexes = [[NSMutableIndexSet alloc] init];
NSMutableString *substitutionFontsGlyphFetchingString = [[NSMutableString alloc] init];
NSMutableIndexSet *substitutionGlyphIndexes = [[NSMutableIndexSet alloc] init];
/* Loop over all the characters, appending them to our glyph fetching string */
NSUInteger idx;
for (idx = 0; idx < charactersToLoadCount; idx++) {
NSString *string = copy1CharStringForByteValue(charactersToLoad[idx], bytesPerChar, encoding);
if (string == nil) {
/* This byte value is not represented in this char set (e.g. upper 128 in ASCII) */
outGlyphs[idx] = invalidGlyph;
} else {
if ([coveredSet characterIsMember:[string characterAtIndex:0]]) {
/* It's covered by our base font */
[coveredGlyphFetchingString appendString:string];
[coveredGlyphIndexes addIndex:idx];
} else {
/* Maybe there's a substitution font */
[substitutionFontsGlyphFetchingString appendString:string];
[substitutionGlyphIndexes addIndex:idx];
}
}
[string release];
}
/* Fetch the non-substitute glyphs */
{
NEW_ARRAY(CGGlyph, cgglyphs, [coveredGlyphFetchingString length]);
BOOL success = getGlyphs(cgglyphs, coveredGlyphFetchingString, baseFont);
HFASSERT(success == YES);
NSUInteger numGlyphs = [coveredGlyphFetchingString length];
/* Fill in our glyphs array */
NSUInteger coveredGlyphIdx = [coveredGlyphIndexes firstIndex];
for (NSUInteger i=0; i < numGlyphs; i++) {
outGlyphs[coveredGlyphIdx] = (struct HFGlyph_t){.fontIndex = 0, .glyph = cgglyphs[i]};
coveredGlyphIdx = [coveredGlyphIndexes indexGreaterThanIndex:coveredGlyphIdx];
/* Record the advancement. Note that this may be more efficient to do in bulk. */
if (outMaxAdvance) *outMaxAdvance = HFMax(*outMaxAdvance, [baseFont advancementForGlyph:cgglyphs[i]].width);
}
HFASSERT(coveredGlyphIdx == NSNotFound); //we must have exhausted the table
FREE_ARRAY(cgglyphs);
}
/* Now do substitution glyphs. */
{
NSUInteger substitutionGlyphIndex = [substitutionGlyphIndexes firstIndex], numSubstitutionChars = [substitutionFontsGlyphFetchingString length];
for (NSUInteger i=0; i < numSubstitutionChars; i++) {
CTFontRef substitutionFont = CTFontCreateForString((CTFontRef)baseFont, (CFStringRef)substitutionFontsGlyphFetchingString, CFRangeMake(i, 1));
if (substitutionFont) {
/* We have a font for this string */
CGGlyph glyph;
unichar c = [substitutionFontsGlyphFetchingString characterAtIndex:i];
NSString *substring = [[NSString alloc] initWithCharacters:&c length:1];
BOOL success = getGlyphs(&glyph, substring, (NSFont *)substitutionFont);
[substring release];
if (! success) {
/* Turns out there wasn't a glyph like we thought there would be, so set an invalid glyph marker */
outGlyphs[substitutionGlyphIndex] = invalidGlyph;
} else {
/* Find the index in fonts. If none, add to it. */
HFASSERT(fonts != nil);
NSUInteger fontIndex = [fonts indexOfObject:(id)substitutionFont];
if (fontIndex == NSNotFound) {
[fonts addObject:(id)substitutionFont];
fontIndex = [fonts count] - 1;
}
/* Now make the glyph */
HFASSERT(fontIndex < UINT16_MAX);
outGlyphs[substitutionGlyphIndex] = (struct HFGlyph_t){.fontIndex = (uint16_t)fontIndex, .glyph = glyph};
}
/* We're done with this */
CFRelease(substitutionFont);
}
substitutionGlyphIndex = [substitutionGlyphIndexes indexGreaterThanIndex:substitutionGlyphIndex];
}
}
[coveredGlyphFetchingString release];
[coveredGlyphIndexes release];
[substitutionFontsGlyphFetchingString release];
[substitutionGlyphIndexes release];
}
static int compareGlyphFontIndexes(const void *p1, const void *p2) {
const struct HFGlyph_t *g1 = p1, *g2 = p2;
if (g1->fontIndex != g2->fontIndex) {
/* Prefer to sort by font index */
return (g1->fontIndex > g2->fontIndex) - (g2->fontIndex > g1->fontIndex);
} else {
/* If they have equal font indexes, sort by glyph value */
return (g1->glyph > g2->glyph) - (g2->glyph > g1->glyph);
}
}
- (void)threadedPrecacheGlyphs:(const struct HFGlyph_t *)glyphs withFonts:(NSArray *)localFonts count:(NSUInteger)count {
/* This method draws glyphs anywhere, so that they get cached by CG and drawing them a second time can be fast. */
NSUInteger i, validGlyphCount;
/* We can use 0 advances */
NEW_ARRAY(CGSize, advances, count);
bzero(advances, count * sizeof *advances);
/* Make a local copy of the glyphs, and sort them according to their font index so that we can draw them with the fewest runs. */
NEW_ARRAY(struct HFGlyph_t, validGlyphs, count);
validGlyphCount = 0;
for (i=0; i < count; i++) {
if (glyphs[i].glyph <= kCGGlyphMax && glyphs[i].fontIndex != kHFGlyphFontIndexInvalid) {
validGlyphs[validGlyphCount++] = glyphs[i];
}
}
qsort(validGlyphs, validGlyphCount, sizeof *validGlyphs, compareGlyphFontIndexes);
/* Remove duplicate glyphs */
NSUInteger trailing = 0;
struct HFGlyph_t lastGlyph = {.glyph = kCGFontIndexInvalid, .fontIndex = kHFGlyphFontIndexInvalid};
for (i=0; i < validGlyphCount; i++) {
if (! HFGlyphEqualsGlyph(lastGlyph, validGlyphs[i])) {
lastGlyph = validGlyphs[i];
validGlyphs[trailing++] = lastGlyph;
}
}
validGlyphCount = trailing;
/* Draw the glyphs in runs */
NEW_ARRAY(CGGlyph, cgglyphs, count);
NSImage *glyphDrawingImage = [[NSImage alloc] initWithSize:NSMakeSize(100, 100)];
[glyphDrawingImage lockFocus];
CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
HFGlyphFontIndex runFontIndex = -1;
NSUInteger runLength = 0;
for (i=0; i <= validGlyphCount; i++) {
if (i == validGlyphCount || validGlyphs[i].fontIndex != runFontIndex) {
/* End the current run */
if (runLength > 0) {
NSLog(@"Drawing with %@", [localFonts[runFontIndex] screenFont]);
[[localFonts[runFontIndex] screenFont] set];
CGContextSetTextPosition(ctx, 0, 50);
CGContextShowGlyphsWithAdvances(ctx, cgglyphs, advances, runLength);
}
NSLog(@"Drew a run of length %lu", (unsigned long)runLength);
runLength = 0;
if (i < validGlyphCount) runFontIndex = validGlyphs[i].fontIndex;
}
if (i < validGlyphCount) {
/* Append to the current run */
cgglyphs[runLength++] = validGlyphs[i].glyph;
}
}
/* All done */
[glyphDrawingImage unlockFocus];
[glyphDrawingImage release];
FREE_ARRAY(advances);
FREE_ARRAY(validGlyphs);
FREE_ARRAY(cgglyphs);
}
- (void)threadedLoadGlyphs:(id)unused {
/* Note that this is running on a background thread */
USE(unused);
/* Do some things under the lock. Someone else may wish to read fonts, and we're going to write to it, so make a local copy. Also figure out what characters to load. */
NSMutableArray *localFonts;
NSIndexSet *charactersToLoad;
OSSpinLockLock(&glyphLoadLock);
localFonts = [fonts mutableCopy];
charactersToLoad = requestedCharacters;
/* Set requestedCharacters to nil so that the caller knows we aren't going to check again, and will have to re-invoke us. */
requestedCharacters = nil;
OSSpinLockUnlock(&glyphLoadLock);
/* The base font is the first font */
NSFont *font = localFonts[0];
NSUInteger charVal, glyphIdx, charCount = [charactersToLoad count];
NEW_ARRAY(struct HFGlyph_t, glyphs, charCount);
/* Now generate our glyphs */
NEW_ARRAY(NSUInteger, characters, charCount);
[charactersToLoad getIndexes:characters maxCount:charCount inIndexRange:NULL];
generateGlyphs(font, localFonts, glyphs, bytesPerChar, encoding, characters, charCount, NULL);
FREE_ARRAY(characters);
/* The first time we draw glyphs, it's slow, so pre-cache them by drawing them now. */
// This was disabled because it blows up the CG glyph cache
// [self threadedPrecacheGlyphs:glyphs withFonts:localFonts count:charCount];
/* Replace fonts. Do this before we insert into the glyph trie, because the glyph trie references fonts that we're just now putting in the fonts array. */
id oldFonts;
OSSpinLockLock(&glyphLoadLock);
oldFonts = fonts;
fonts = localFonts;
OSSpinLockUnlock(&glyphLoadLock);
[oldFonts release];
/* Now insert all of the glyphs into the glyph trie */
glyphIdx = 0;
for (charVal = [charactersToLoad firstIndex]; charVal != NSNotFound; charVal = [charactersToLoad indexGreaterThanIndex:charVal]) {
HFGlyphTrieInsert(&glyphTable, charVal, glyphs[glyphIdx++]);
}
FREE_ARRAY(glyphs);
/* Trigger a redisplay */
[self performSelectorOnMainThread:@selector(triggerRedisplay:) withObject:nil waitUntilDone:NO];
/* All done. We inherited the retain on requestedCharacters, so release it. */
[charactersToLoad release];
}
- (void)triggerRedisplay:unused {
USE(unused);
[self setNeedsDisplay:YES];
}
- (void)beginLoadGlyphsForCharacters:(NSIndexSet *)charactersToLoad {
/* Create the operation (and maybe the operation queue itself) */
if (! glyphLoader) {
glyphLoader = [[NSOperationQueue alloc] init];
[glyphLoader setMaxConcurrentOperationCount:1];
}
if (! fonts) {
NSFont *font = [self font];
fonts = [[NSMutableArray alloc] initWithObjects:&font count:1];
}
BOOL needToStartOperation;
OSSpinLockLock(&glyphLoadLock);
if (requestedCharacters) {
/* There's a pending request, so just add to it */
[requestedCharacters addIndexes:charactersToLoad];
needToStartOperation = NO;
} else {
/* There's no pending request, so we will create one */
requestedCharacters = [charactersToLoad mutableCopy];
needToStartOperation = YES;
}
OSSpinLockUnlock(&glyphLoadLock);
if (needToStartOperation) {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(threadedLoadGlyphs:) object:charactersToLoad];
[glyphLoader addOperation:op];
[op release];
}
}
- (void)dealloc {
HFGlyphTreeFree(&glyphTable);
[fonts release];
[super dealloc];
}
- (void)staleTieredProperties {
tier1DataIsStale = YES;
/* We have to free the glyph table */
requestedCancel = YES;
[glyphLoader waitUntilAllOperationsAreFinished];
requestedCancel = NO;
HFGlyphTreeFree(&glyphTable);
HFGlyphTrieInitialize(&glyphTable, bytesPerChar);
[fonts release];
fonts = nil;
[fontCache release];
fontCache = nil;
}
- (void)setFont:(NSFont *)font {
[self staleTieredProperties];
/* fonts is preloaded with our one font */
if (! fonts) fonts = [[NSMutableArray alloc] init];
[fonts addObject:font];
[super setFont:font];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
HFASSERT([coder allowsKeyedCoding]);
self = [super initWithCoder:coder];
encoding = (NSStringEncoding)[coder decodeInt64ForKey:@"HFStringEncoding"];
bytesPerChar = HFStringEncodingCharacterLength(encoding);
[self staleTieredProperties];
return self;
}
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
encoding = NSMacOSRomanStringEncoding;
bytesPerChar = HFStringEncodingCharacterLength(encoding);
[self staleTieredProperties];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
HFASSERT([coder allowsKeyedCoding]);
[super encodeWithCoder:coder];
[coder encodeInt64:encoding forKey:@"HFStringEncoding"];
}
- (NSStringEncoding)encoding {
return encoding;
}
- (void)setEncoding:(NSStringEncoding)val {
if (encoding != val) {
/* Our glyph table is now stale. Call this first to ensure our background operation is complete. */
[self staleTieredProperties];
/* Store the new encoding. */
encoding = val;
/* Compute bytes per character */
bytesPerChar = HFStringEncodingCharacterLength(encoding);
HFASSERT(bytesPerChar > 0);
/* Ensure the tree knows about the new bytes per character */
HFGlyphTrieInitialize(&glyphTable, bytesPerChar);
/* Redraw ourselves with our new glyphs */
[self setNeedsDisplay:YES];
}
}
- (void)loadTier1Data {
NSFont *font = [self font];
/* Use the max advance as the glyph advance */
glyphAdvancement = HFCeil([font maximumAdvancement].width);
/* Generate replacementGlyph */
CGGlyph glyph[1];
BOOL foundReplacement = NO;
if (! foundReplacement) foundReplacement = getGlyphs(glyph, @".", font);
if (! foundReplacement) foundReplacement = getGlyphs(glyph, @"*", font);
if (! foundReplacement) foundReplacement = getGlyphs(glyph, @"!", font);
if (! foundReplacement) {
/* Really we should just fall back to another font in this case */
[NSException raise:NSInternalInconsistencyException format:@"Unable to find replacement glyph for font %@", font];
}
replacementGlyph.fontIndex = 0;
replacementGlyph.glyph = glyph[0];
/* We're no longer stale */
tier1DataIsStale = NO;
}
/* Override of base class method for font substitution */
- (NSFont *)fontAtSubstitutionIndex:(uint16_t)idx {
HFASSERT(idx != kHFGlyphFontIndexInvalid);
if (idx >= [fontCache count]) {
/* Our font cache is out of date. Take the lock and update the cache. */
NSArray *newFonts = nil;
OSSpinLockLock(&glyphLoadLock);
HFASSERT(idx < [fonts count]);
newFonts = [fonts copy];
OSSpinLockUnlock(&glyphLoadLock);
/* Store the new cache */
[fontCache release];
fontCache = newFonts;
/* Now our cache should be up to date */
HFASSERT(idx < [fontCache count]);
}
return fontCache[idx];
}
/* Override of base class method in case we are 16 bit */
- (NSUInteger)bytesPerCharacter {
return bytesPerChar;
}
- (void)extractGlyphsForBytes:(const unsigned char *)bytes count:(NSUInteger)numBytes offsetIntoLine:(NSUInteger)offsetIntoLine intoArray:(struct HFGlyph_t *)glyphs advances:(CGSize *)advances resultingGlyphCount:(NSUInteger *)resultGlyphCount {
HFASSERT(bytes != NULL);
HFASSERT(glyphs != NULL);
HFASSERT(resultGlyphCount != NULL);
HFASSERT(advances != NULL);
USE(offsetIntoLine);
/* Ensure we have advance, etc. before trying to use it */
if (tier1DataIsStale) [self loadTier1Data];
CGSize advance = CGSizeMake(glyphAdvancement, 0);
NSMutableIndexSet *charactersToLoad = nil; //note: in UTF-32 this may have to move to an NSSet
const uint8_t localBytesPerChar = bytesPerChar;
NSUInteger charIndex, numChars = numBytes / localBytesPerChar, byteIndex = 0;
for (charIndex = 0; charIndex < numChars; charIndex++) {
NSUInteger character = -1;
if (localBytesPerChar == 1) {
character = *(const uint8_t *)(bytes + byteIndex);
} else if (localBytesPerChar == 2) {
character = *(const uint16_t *)(bytes + byteIndex);
} else if (localBytesPerChar == 4) {
character = *(const uint32_t *)(bytes + byteIndex);
}
struct HFGlyph_t glyph = HFGlyphTrieGet(&glyphTable, character);
if (glyph.glyph == 0 && glyph.fontIndex == 0) {
/* Unloaded glyph, so load it */
if (! charactersToLoad) charactersToLoad = [[NSMutableIndexSet alloc] init];
[charactersToLoad addIndex:character];
glyph = replacementGlyph;
} else if (glyph.glyph == (uint16_t)-1 && glyph.fontIndex == kHFGlyphFontIndexInvalid) {
/* Missing glyph, so ignore it */
glyph = replacementGlyph;
} else {
/* Valid glyph */
}
HFASSERT(glyph.fontIndex != kHFGlyphFontIndexInvalid);
advances[charIndex] = advance;
glyphs[charIndex] = glyph;
byteIndex += localBytesPerChar;
}
*resultGlyphCount = numChars;
if (charactersToLoad) {
[self beginLoadGlyphsForCharacters:charactersToLoad];
[charactersToLoad release];
}
}
- (CGFloat)advancePerCharacter {
/* The glyph advancement is determined by our glyph table */
if (tier1DataIsStale) [self loadTier1Data];
return glyphAdvancement;
}
- (CGFloat)advanceBetweenColumns {
return 0; //don't have any space between columns
}
- (NSUInteger)maximumGlyphCountForByteCount:(NSUInteger)byteCount {
return byteCount / [self bytesPerCharacter];
}
@end