699 lines
17 KiB
Objective-C
699 lines
17 KiB
Objective-C
//
|
|
// DTCoreTextGlyphRun.m
|
|
// WeRead
|
|
//
|
|
// Reverse-engineered from binary analysis
|
|
// Based on DTCoreText open-source library, customized by WeRead
|
|
//
|
|
// This file contains pseudo-code reconstruction of the DTCoreTextGlyphRun
|
|
// class based on ivar analysis, method signatures, and the open-source DTCoreText library.
|
|
//
|
|
|
|
#import "DTCoreTextGlyphRun.h"
|
|
#import "DTCoreTextLayoutLine.h"
|
|
#import <CoreText/CoreText.h>
|
|
|
|
#pragma mark - DTTextAttachment Stub
|
|
|
|
/**
|
|
* Stub for DTTextAttachment class.
|
|
* Represents an embedded object (image, view, etc.) in the text.
|
|
*/
|
|
@interface DTTextAttachment : NSObject
|
|
|
|
@property (nonatomic, strong) UIImage *image;
|
|
@property (nonatomic, assign) CGSize displaySize;
|
|
@property (nonatomic, assign) CGRect frame;
|
|
@property (nonatomic, copy) NSString *contentType;
|
|
|
|
@end
|
|
|
|
@implementation DTTextAttachment
|
|
@end
|
|
|
|
#pragma mark - Private Interface
|
|
|
|
@interface DTCoreTextGlyphRun () {
|
|
// Cached glyph data
|
|
CGGlyph *_glyphs;
|
|
CGPoint *_positions;
|
|
CGFloat *_advances;
|
|
CFIndex _glyphCount;
|
|
|
|
// Whether glyph data has been extracted
|
|
BOOL _glyphsExtracted;
|
|
|
|
// Cached metrics
|
|
CGFloat _cachedAscent;
|
|
CGFloat _cachedDescent;
|
|
CGFloat _cachedLeading;
|
|
CGFloat _cachedWidth;
|
|
BOOL _metricsCached;
|
|
|
|
// Internal lock
|
|
NSLock *_lock;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - DTCoreTextGlyphRun Implementation
|
|
|
|
@implementation DTCoreTextGlyphRun
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (instancetype)initWithCTRun:(CTRunRef)ctRun
|
|
frame:(CGRect)frame
|
|
range:(NSRange)range
|
|
attributes:(NSDictionary *)attributes
|
|
index:(NSUInteger)index {
|
|
self = [super init];
|
|
if (self) {
|
|
_lock = [[NSLock alloc] init];
|
|
|
|
// Store the CTRun with a retain
|
|
if (ctRun) {
|
|
_ctRun = (CTRunRef)CFRetain(ctRun);
|
|
}
|
|
|
|
_frame = frame;
|
|
_stringRange = range;
|
|
_attributes = [attributes copy];
|
|
_runIndex = index;
|
|
|
|
// Initialize state
|
|
_glyphsExtracted = NO;
|
|
_metricsCached = NO;
|
|
_glyphs = NULL;
|
|
_positions = NULL;
|
|
_advances = NULL;
|
|
_glyphCount = 0;
|
|
|
|
// Extract common attributes
|
|
[self extractAttributes];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
if (_ctRun) {
|
|
CFRelease(_ctRun);
|
|
_ctRun = NULL;
|
|
}
|
|
|
|
// Free allocated glyph data
|
|
if (_glyphs) {
|
|
free(_glyphs);
|
|
_glyphs = NULL;
|
|
}
|
|
if (_positions) {
|
|
free(_positions);
|
|
_positions = NULL;
|
|
}
|
|
if (_advances) {
|
|
free(_advances);
|
|
_advances = NULL;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Attribute Extraction
|
|
|
|
/**
|
|
* Extracts common attributes from the attributes dictionary.
|
|
*/
|
|
- (void)extractAttributes {
|
|
if (!_attributes) {
|
|
return;
|
|
}
|
|
|
|
// Extract font
|
|
CTFontRef ctFont = (__bridge CTFontRef)_attributes[(__bridge NSString *)kCTFontAttributeName];
|
|
if (ctFont) {
|
|
_font = [UIFont fontWithDescriptor:[UIFontDescriptor fontDescriptorWithCTFont:ctFont]
|
|
size:CTFontGetSize(ctFont)];
|
|
}
|
|
|
|
// Extract text color
|
|
CGColorRef textColor = (__bridge CGColorRef)_attributes[(__bridge NSString *)kCTForegroundColorAttributeName];
|
|
if (textColor) {
|
|
_textColor = [UIColor colorWithCGColor:textColor];
|
|
}
|
|
|
|
// Extract background color
|
|
CGColorRef bgColor = (__bridge CGColorRef)_attributes[@"DTBackgroundColor"];
|
|
if (bgColor) {
|
|
_backgroundColor = [UIColor colorWithCGColor:bgColor];
|
|
}
|
|
|
|
// Extract strikethrough
|
|
NSNumber *strikethrough = _attributes[(__bridge NSString *)kCTSuperscriptAttributeName];
|
|
if (strikethrough) {
|
|
_hasStrikethrough = [strikethrough boolValue];
|
|
}
|
|
|
|
// Extract underline
|
|
NSNumber *underlineStyle = _attributes[(__bridge NSString *)kCTUnderlineColorAttributeName];
|
|
if (underlineStyle) {
|
|
_hasUnderline = YES;
|
|
_underlineStyle = [underlineStyle integerValue];
|
|
}
|
|
|
|
// Check for attachment
|
|
_attachment = _attributes[@"DTTextAttachment"];
|
|
if (_attachment) {
|
|
_isAttachment = YES;
|
|
}
|
|
|
|
// Extract writing direction
|
|
NSArray *writingDirection = _attributes[(__bridge NSString *)kCTWritingDirectionAttributeName];
|
|
if ([writingDirection count] > 0) {
|
|
_writingDirection = [writingDirection[0] integerValue];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Glyph Extraction
|
|
|
|
/**
|
|
* Extracts glyph data from the CTRun.
|
|
*
|
|
* This method extracts:
|
|
* - Glyph values (CGGlyph)
|
|
* - Glyph positions (CGPoint)
|
|
* - Glyph advances (CGFloat)
|
|
*/
|
|
- (void)extractGlyphs {
|
|
if (_glyphsExtracted || !_ctRun) {
|
|
return;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
// Get the number of glyphs
|
|
_glyphCount = CTRunGetGlyphCount(_ctRun);
|
|
|
|
if (_glyphCount == 0) {
|
|
_glyphsExtracted = YES;
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
|
|
// Allocate memory for glyph data
|
|
_glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * _glyphCount);
|
|
_positions = (CGPoint *)malloc(sizeof(CGPoint) * _glyphCount);
|
|
_advances = (CGFloat *)malloc(sizeof(CGFloat) * _glyphCount);
|
|
|
|
// Extract glyph values
|
|
CTRunGetGlyphs(_ctRun, CFRangeMake(0, 0), _glyphs);
|
|
|
|
// Extract glyph positions
|
|
CTRunGetPositions(_ctRun, CFRangeMake(0, 0), _positions);
|
|
|
|
// Extract glyph advances
|
|
CTRunGetAdvances(_ctRun, CFRangeMake(0, 0), (CGSize *)_advances);
|
|
|
|
_glyphsExtracted = YES;
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
#pragma mark - Glyph Access
|
|
|
|
- (CGGlyph)glyphAtIndex:(NSUInteger)index {
|
|
[self extractGlyphs];
|
|
|
|
if (index >= _glyphCount || !_glyphs) {
|
|
return 0;
|
|
}
|
|
|
|
return _glyphs[index];
|
|
}
|
|
|
|
- (CGPoint)positionForGlyphAtIndex:(NSUInteger)index {
|
|
[self extractGlyphs];
|
|
|
|
if (index >= _glyphCount || !_positions) {
|
|
return CGPointZero;
|
|
}
|
|
|
|
return _positions[index];
|
|
}
|
|
|
|
- (CGFloat)advanceForGlyphAtIndex:(NSUInteger)index {
|
|
[self extractGlyphs];
|
|
|
|
if (index >= _glyphCount || !_advances) {
|
|
return 0;
|
|
}
|
|
|
|
return _advances[index];
|
|
}
|
|
|
|
- (CGRect)rectForGlyphAtIndex:(NSUInteger)index {
|
|
[self extractGlyphs];
|
|
|
|
if (index >= _glyphCount) {
|
|
return CGRectNull;
|
|
}
|
|
|
|
CGPoint position = _positions[index];
|
|
CGFloat advance = _advances[index];
|
|
|
|
// Calculate the rect for this glyph
|
|
// The position is relative to the run's origin
|
|
return CGRectMake(
|
|
_frame.origin.x + position.x,
|
|
_frame.origin.y,
|
|
advance,
|
|
_frame.size.height
|
|
);
|
|
}
|
|
|
|
#pragma mark - Path Operations
|
|
|
|
/**
|
|
* Creates a CGPath containing all glyphs in this run.
|
|
*
|
|
* This method creates a path by transforming each glyph's outline
|
|
* to its position within the run. This is used for:
|
|
* - Custom drawing with effects
|
|
* - Hit testing with complex shapes
|
|
* - Creating outlines for selection
|
|
*
|
|
* Algorithm:
|
|
* 1. Extract all glyph data
|
|
* 2. For each glyph:
|
|
* a. Get the glyph's path from the font
|
|
* b. Transform to the glyph's position
|
|
* c. Add to the combined path
|
|
* 3. Return the combined path
|
|
*
|
|
* @return A CGPath containing all glyph outlines
|
|
*/
|
|
- (CGPathRef)newPathWithGlyphs {
|
|
[self extractGlyphs];
|
|
|
|
if (!_ctRun || _glyphCount == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create a mutable path for the combined glyphs
|
|
CGMutablePathRef combinedPath = CGPathCreateMutable();
|
|
|
|
// Get the font from attributes
|
|
CTFontRef font = (__bridge CTFontRef)_attributes[(__bridge NSString *)kCTFontAttributeName];
|
|
if (!font) {
|
|
return combinedPath;
|
|
}
|
|
|
|
// Process each glyph
|
|
for (CFIndex i = 0; i < _glyphCount; i++) {
|
|
CGGlyph glyph = _glyphs[i];
|
|
CGPoint position = _positions[i];
|
|
|
|
// Get the path for this glyph from the font
|
|
CGPathRef glyphPath = CTFontCreatePathForGlyph(font, glyph, NULL);
|
|
|
|
if (glyphPath) {
|
|
// Create a transform to position the glyph
|
|
// The position is relative to the run's origin
|
|
CGAffineTransform transform = CGAffineTransformMakeTranslation(
|
|
_frame.origin.x + position.x,
|
|
_frame.origin.y + position.y
|
|
);
|
|
|
|
// Add the transformed glyph path to the combined path
|
|
CGPathAddPath(combinedPath, &transform, glyphPath);
|
|
|
|
// Release the individual glyph path
|
|
CGPathRelease(glyphPath);
|
|
}
|
|
}
|
|
|
|
return combinedPath;
|
|
}
|
|
|
|
/**
|
|
* Creates a CGPath for a specific glyph.
|
|
*/
|
|
- (CGPathRef)newPathForGlyphAtIndex:(NSUInteger)index {
|
|
[self extractGlyphs];
|
|
|
|
if (!_ctRun || index >= _glyphCount) {
|
|
return NULL;
|
|
}
|
|
|
|
// Get the font from attributes
|
|
CTFontRef font = (__bridge CTFontRef)_attributes[(__bridge NSString *)kCTFontAttributeName];
|
|
if (!font) {
|
|
return NULL;
|
|
}
|
|
|
|
CGGlyph glyph = _glyphs[index];
|
|
CGPoint position = _positions[index];
|
|
|
|
// Get the path for this glyph
|
|
CGPathRef glyphPath = CTFontCreatePathForGlyph(font, glyph, NULL);
|
|
|
|
if (!glyphPath) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create a transform to position the glyph
|
|
CGAffineTransform transform = CGAffineTransformMakeTranslation(
|
|
_frame.origin.x + position.x,
|
|
_frame.origin.y + position.y
|
|
);
|
|
|
|
// Create a new path with the transform applied
|
|
CGMutablePathRef transformedPath = CGPathCreateMutable();
|
|
CGPathAddPath(transformedPath, &transform, glyphPath);
|
|
|
|
// Release the original glyph path
|
|
CGPathRelease(glyphPath);
|
|
|
|
return transformedPath;
|
|
}
|
|
|
|
/**
|
|
* Creates a bounding path for all glyphs.
|
|
*/
|
|
- (CGPathRef)newBoundingPath {
|
|
[self extractGlyphs];
|
|
|
|
if (_glyphCount == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create a simple rectangular path that bounds all glyphs
|
|
CGMutablePathRef path = CGPathCreateMutable();
|
|
CGPathAddRect(path, NULL, _frame);
|
|
|
|
return path;
|
|
}
|
|
|
|
#pragma mark - Image Operations
|
|
|
|
/**
|
|
* Returns the image for the glyph at the specified index.
|
|
*
|
|
* This is used for custom rendering where glyphs are replaced with images.
|
|
*/
|
|
- (UIImage *)imageForGlyphAtIndex:(NSUInteger)index {
|
|
if (!_glyphImages || index >= [_glyphImages count]) {
|
|
return nil;
|
|
}
|
|
|
|
return _glyphImages[index];
|
|
}
|
|
|
|
/**
|
|
* Returns the bounding rect for the glyph image.
|
|
*/
|
|
- (CGRect)imageRectForGlyphAtIndex:(NSUInteger)index {
|
|
[self extractGlyphs];
|
|
|
|
if (index >= _glyphCount) {
|
|
return CGRectNull;
|
|
}
|
|
|
|
CGPoint position = _positions[index];
|
|
CGFloat advance = _advances[index];
|
|
|
|
// The image rect is centered on the glyph position
|
|
return CGRectMake(
|
|
_frame.origin.x + position.x,
|
|
_frame.origin.y,
|
|
advance,
|
|
_frame.size.height
|
|
);
|
|
}
|
|
|
|
#pragma mark - Hit Testing
|
|
|
|
/**
|
|
* Returns the glyph index at the given point.
|
|
*
|
|
* This method checks if the point falls within any glyph's bounding rect.
|
|
*
|
|
* @param point The point to test
|
|
* @return The glyph index, or NSNotFound
|
|
*/
|
|
- (NSUInteger)glyphIndexAtPoint:(CGPoint)point {
|
|
[self extractGlyphs];
|
|
|
|
for (NSUInteger i = 0; i < _glyphCount; i++) {
|
|
CGRect glyphRect = [self rectForGlyphAtIndex:i];
|
|
|
|
if (CGRectContainsPoint(glyphRect, point)) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return NSNotFound;
|
|
}
|
|
|
|
/**
|
|
* Returns the string index at the given point.
|
|
*/
|
|
- (NSUInteger)stringIndexAtPoint:(CGPoint)point {
|
|
NSUInteger glyphIndex = [self glyphIndexAtPoint:point];
|
|
|
|
if (glyphIndex == NSNotFound) {
|
|
return NSNotFound;
|
|
}
|
|
|
|
// Convert glyph index to string index
|
|
// This assumes a 1:1 mapping between glyphs and characters
|
|
// For complex scripts, this may need more sophisticated mapping
|
|
return _stringRange.location + glyphIndex;
|
|
}
|
|
|
|
/**
|
|
* Returns the rect for a given string index.
|
|
*/
|
|
- (CGRect)rectForStringIndex:(NSUInteger)index {
|
|
if (index < _stringRange.location ||
|
|
index >= _stringRange.location + _stringRange.length) {
|
|
return CGRectNull;
|
|
}
|
|
|
|
// Convert string index to glyph index
|
|
NSUInteger glyphIndex = index - _stringRange.location;
|
|
|
|
return [self rectForGlyphAtIndex:glyphIndex];
|
|
}
|
|
|
|
#pragma mark - Drawing
|
|
|
|
/**
|
|
* Draws this run into a CGContext.
|
|
*
|
|
* This method draws the glyphs using CTLineDraw or by manually
|
|
* drawing each glyph at its position.
|
|
*
|
|
* @param context The CGContext to draw into
|
|
*/
|
|
- (void)drawInContext:(CGContextRef)context {
|
|
if (!context || !_ctRun) {
|
|
return;
|
|
}
|
|
|
|
// Save graphics state
|
|
CGContextSaveGState(context);
|
|
|
|
// Draw the run using CoreText
|
|
CTRunDraw(_ctRun, context, CFRangeMake(0, 0));
|
|
|
|
// Restore graphics state
|
|
CGContextRestoreGState(context);
|
|
}
|
|
|
|
/**
|
|
* Draws this run with a specific color.
|
|
*/
|
|
- (void)drawInContext:(CGContextRef)context withColor:(UIColor *)color {
|
|
if (!context || !_ctRun || !color) {
|
|
return;
|
|
}
|
|
|
|
// Save graphics state
|
|
CGContextSaveGState(context);
|
|
|
|
// Set the text color
|
|
CGContextSetFillColorWithColor(context, color.CGColor);
|
|
|
|
// Draw the run
|
|
CTRunDraw(_ctRun, context, CFRangeMake(0, 0));
|
|
|
|
// Restore graphics state
|
|
CGContextRestoreGState(context);
|
|
}
|
|
|
|
/**
|
|
* Draws the attachment (if any) into the context.
|
|
*/
|
|
- (void)drawAttachmentInContext:(CGContextRef)context {
|
|
if (!context || !_attachment || !_attachment.image) {
|
|
return;
|
|
}
|
|
|
|
// Save graphics state
|
|
CGContextSaveGState(context);
|
|
|
|
// Draw the attachment image at the run's frame
|
|
CGContextDrawImage(context, _frame, _attachment.image.CGImage);
|
|
|
|
// Restore graphics state
|
|
CGContextRestoreGState(context);
|
|
}
|
|
|
|
#pragma mark - Run Comparison
|
|
|
|
/**
|
|
* Compares this run to another run for ordering.
|
|
*/
|
|
- (NSComparisonResult)compareToRun:(DTCoreTextGlyphRun *)otherRun {
|
|
// Compare by string range location
|
|
if (_stringRange.location < otherRun.stringRange.location) {
|
|
return NSOrderedAscending;
|
|
} else if (_stringRange.location > otherRun.stringRange.location) {
|
|
return NSOrderedDescending;
|
|
}
|
|
|
|
// If same location, compare by run index
|
|
if (_runIndex < otherRun.runIndex) {
|
|
return NSOrderedAscending;
|
|
} else if (_runIndex > otherRun.runIndex) {
|
|
return NSOrderedDescending;
|
|
}
|
|
|
|
return NSOrderedSame;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this run contains the given string index.
|
|
*/
|
|
- (BOOL)containsStringIndex:(NSUInteger)index {
|
|
return index >= _stringRange.location &&
|
|
index < _stringRange.location + _stringRange.length;
|
|
}
|
|
|
|
/**
|
|
* Returns whether this run intersects with the given range.
|
|
*/
|
|
- (BOOL)intersectsRange:(NSRange)range {
|
|
NSRange intersection = NSIntersectionRange(_stringRange, range);
|
|
return intersection.length > 0;
|
|
}
|
|
|
|
#pragma mark - Metrics
|
|
|
|
- (void)calculateMetrics {
|
|
if (_metricsCached || !_ctRun) {
|
|
return;
|
|
}
|
|
|
|
[_lock lock];
|
|
|
|
// Get typographic bounds from CTRun
|
|
_cachedWidth = CTRunGetTypographicBounds(_ctRun, CFRangeMake(0, 0),
|
|
&_cachedAscent, &_cachedDescent, &_cachedLeading);
|
|
|
|
_metricsCached = YES;
|
|
|
|
[_lock unlock];
|
|
}
|
|
|
|
- (CGFloat)getTypographicBoundsAscent:(CGFloat *)ascent
|
|
descent:(CGFloat *)descent
|
|
leading:(CGFloat *)leading {
|
|
[self calculateMetrics];
|
|
|
|
if (ascent) *ascent = _cachedAscent;
|
|
if (descent) *descent = _cachedDescent;
|
|
if (leading) *leading = _cachedLeading;
|
|
|
|
return _cachedWidth;
|
|
}
|
|
|
|
- (CGRect)bounds {
|
|
[self calculateMetrics];
|
|
|
|
return CGRectMake(0, -_cachedDescent, _cachedWidth, _cachedAscent + _cachedDescent);
|
|
}
|
|
|
|
- (CGFloat)width {
|
|
[self calculateMetrics];
|
|
return _cachedWidth;
|
|
}
|
|
|
|
- (CGFloat)height {
|
|
[self calculateMetrics];
|
|
return _cachedAscent + _cachedDescent;
|
|
}
|
|
|
|
- (NSUInteger)numberOfGlyphs {
|
|
[self extractGlyphs];
|
|
return _glyphCount;
|
|
}
|
|
|
|
- (NSArray<NSNumber *> *)glyphs {
|
|
[self extractGlyphs];
|
|
|
|
NSMutableArray *glyphArray = [NSMutableArray arrayWithCapacity:_glyphCount];
|
|
for (CFIndex i = 0; i < _glyphCount; i++) {
|
|
[glyphArray addObject:@(_glyphs[i])];
|
|
}
|
|
|
|
return [glyphArray copy];
|
|
}
|
|
|
|
- (NSArray<NSValue *> *)glyphPositions {
|
|
[self extractGlyphs];
|
|
|
|
NSMutableArray *positionArray = [NSMutableArray arrayWithCapacity:_glyphCount];
|
|
for (CFIndex i = 0; i < _glyphCount; i++) {
|
|
[positionArray addObject:[NSValue valueWithCGPoint:_positions[i]]];
|
|
}
|
|
|
|
return [positionArray copy];
|
|
}
|
|
|
|
- (NSArray<NSNumber *> *)glyphAdvances {
|
|
[self extractGlyphs];
|
|
|
|
NSMutableArray *advanceArray = [NSMutableArray arrayWithCapacity:_glyphCount];
|
|
for (CFIndex i = 0; i < _glyphCount; i++) {
|
|
[advanceArray addObject:@(_advances[i])];
|
|
}
|
|
|
|
return [advanceArray copy];
|
|
}
|
|
|
|
- (BOOL)isRTL {
|
|
return _writingDirection == kCTWritingDirectionRightToLeft;
|
|
}
|
|
|
|
- (BOOL)isWhitespace {
|
|
if (!_ctRun || _glyphCount == 0) {
|
|
return NO;
|
|
}
|
|
|
|
// Check if all characters in the range are whitespace
|
|
// This would need access to the full attributed string
|
|
// For now, return NO as a conservative default
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)isNewline {
|
|
if (!_ctRun || _glyphCount == 0) {
|
|
return NO;
|
|
}
|
|
|
|
// Check if the run contains only newline characters
|
|
// This would need access to the full attributed string
|
|
return NO;
|
|
}
|
|
|
|
@end
|