// // 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 #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 *)glyphs { [self extractGlyphs]; NSMutableArray *glyphArray = [NSMutableArray arrayWithCapacity:_glyphCount]; for (CFIndex i = 0; i < _glyphCount; i++) { [glyphArray addObject:@(_glyphs[i])]; } return [glyphArray copy]; } - (NSArray *)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 *)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