// // DTCoreTextLayoutLine.m // WeRead // // Reverse-engineered from binary analysis // Based on DTCoreText open-source library, customized by WeRead // // This file contains pseudo-code reconstruction of the DTCoreTextLayoutLine // class based on ivar analysis, method signatures, and the open-source DTCoreText library. // #import "DTCoreTextLayoutLine.h" #import "DTCoreTextGlyphRun.h" #import #pragma mark - Private Interface @interface DTCoreTextLayoutLine () { // Cached glyph runs NSArray *_cachedGlyphRuns; // Whether glyph runs have been extracted BOOL _glyphRunsExtracted; // Cached typographic metrics CGFloat _cachedAscent; CGFloat _cachedDescent; CGFloat _cachedLeading; CGFloat _cachedWidth; BOOL _metricsCached; // Internal lock NSLock *_lock; } @end #pragma mark - DTCoreTextLayoutLine Implementation @implementation DTCoreTextLayoutLine #pragma mark - Lifecycle - (instancetype)initWithCTLine:(CTLineRef)ctLine origin:(CGPoint)origin range:(NSRange)range index:(NSUInteger)index { self = [super init]; if (self) { _lock = [[NSLock alloc] init]; // Store the CTLine with a retain if (ctLine) { _ctLine = (CTLineRef)CFRetain(ctLine); } _origin = origin; _stringRange = range; _lineIndex = index; // Initialize state _glyphRunsExtracted = NO; _metricsCached = NO; _baselineOffset = 0; _isLastLineInParagraph = NO; _isLineBreak = NO; } return self; } - (void)dealloc { if (_ctLine) { CFRelease(_ctLine); _ctLine = NULL; } } #pragma mark - Glyph Run Extraction /** * Extracts glyph runs from the CTLine. * * This method iterates through the CTLine's runs and creates * DTCoreTextGlyphRun wrapper objects. * * Algorithm: * 1. Get array of CTRuns from CTLine * 2. For each CTRun: * a. Get its string range * b. Get glyph positions and advances * c. Create DTCoreTextGlyphRun wrapper * 3. Cache the results */ - (void)extractGlyphRuns { if (_glyphRunsExtracted || !_ctLine) { return; } [_lock lock]; // Get the runs from the CTLine CFArrayRef ctRuns = CTLineGetGlyphRuns(_ctLine); if (!ctRuns) { _cachedGlyphRuns = @[]; _glyphRunsExtracted = YES; [_lock unlock]; return; } CFIndex runCount = CFArrayGetCount(ctRuns); if (runCount == 0) { _cachedGlyphRuns = @[]; _glyphRunsExtracted = YES; [_lock unlock]; return; } NSMutableArray *runs = [NSMutableArray arrayWithCapacity:runCount]; // Calculate the baseline x offset for this line CGFloat lineXOffset = _origin.x; for (CFIndex i = 0; i < runCount; i++) { CTRunRef ctRun = CFArrayGetValueAtIndex(ctRuns, i); // Get the string range for this run CFRange cfRange = CTRunGetStringRange(ctRun); NSRange range = NSMakeRange(cfRange.location, cfRange.length); // Get the run's typographic bounds CGFloat ascent, descent, leading; double width = CTRunGetTypographicBounds(ctRun, CFRangeMake(0, 0), &ascent, &descent, &leading); // Get the positions of the glyphs CFIndex glyphCount = CTRunGetGlyphCount(ctRun); CGPoint *positions = (CGPoint *)malloc(sizeof(CGPoint) * glyphCount); CTRunGetPositions(ctRun, CFRangeMake(0, 0), positions); // Get the attributes NSDictionary *attributes = (__bridge NSDictionary *)CTRunGetAttributes(ctRun); // Calculate the run's frame CGRect runFrame; if (glyphCount > 0) { CGFloat minX = positions[0].x; CGFloat maxX = positions[glyphCount - 1].x + width / glyphCount; // Approximate // More accurate: use the actual glyph advances CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * glyphCount); CTRunGetGlyphs(ctRun, CFRangeMake(0, 0), glyphs); // Calculate actual bounds using the glyph advances CGFloat *advances = (CGFloat *)malloc(sizeof(CGFloat) * glyphCount); CTFontRef font = (__bridge CTFontRef)attributes[(__bridge NSString *)kCTFontAttributeName]; if (font) { CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, glyphs, advances, glyphCount); } // Recalculate max X using actual advances maxX = minX; for (CFIndex j = 0; j < glyphCount; j++) { maxX += advances[j]; } free(glyphs); free(advances); runFrame = CGRectMake( lineXOffset + minX, _origin.y - descent, maxX - minX, ascent + descent ); } else { runFrame = CGRectZero; } free(positions); // Create glyph run wrapper DTCoreTextGlyphRun *glyphRun = [[DTCoreTextGlyphRun alloc] initWithCTRun:ctRun frame:runFrame range:range attributes:attributes index:i]; [runs addObject:glyphRun]; } _cachedGlyphRuns = [runs copy]; _glyphRunsExtracted = YES; [_lock unlock]; } /** * Returns the array of glyph runs. */ - (NSArray *)glyphRuns { [self extractGlyphRuns]; return _cachedGlyphRuns; } - (DTCoreTextGlyphRun *)glyphRunAtIndex:(NSUInteger)index { NSArray *runs = [self glyphRuns]; if (index < [runs count]) { return runs[index]; } return nil; } /** * Returns the glyph run containing the given string index. */ - (DTCoreTextGlyphRun *)glyphRunContainingStringIndex:(NSUInteger)index { NSArray *allRuns = [self glyphRuns]; for (DTCoreTextGlyphRun *run in allRuns) { NSRange range = run.stringRange; if (index >= range.location && index < range.location + range.length) { return run; } } return nil; } #pragma mark - Typographic Metrics /** * Calculates and caches typographic metrics. */ - (void)calculateMetrics { if (_metricsCached || !_ctLine) { return; } [_lock lock]; // Get typographic bounds from CTLine _cachedWidth = CTLineGetTypographicBounds(_ctLine, &_cachedAscent, &_cachedDescent, &_cachedLeading); // Get trailing whitespace width _trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(_ctLine); _metricsCached = YES; [_lock unlock]; } - (CGFloat)ascent { [self calculateMetrics]; return _cachedAscent; } - (CGFloat)descent { [self calculateMetrics]; return _cachedDescent; } - (CGFloat)leading { [self calculateMetrics]; return _cachedLeading; } - (CGFloat)height { return [self ascent] + [self descent] + [self leading]; } - (CGFloat)width { [self calculateMetrics]; return _cachedWidth; } /** * Returns the typographic bounds of the line. */ - (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; } /** * Returns the bounds of the line (bounding box). */ - (CGRect)bounds { [self calculateMetrics]; return CGRectMake(0, -_cachedDescent, _cachedWidth, [self height]); } /** * Returns the frame of the line (position + bounds). */ - (CGRect)frame { return CGRectMake(_origin.x, _origin.y - _cachedDescent, _cachedWidth, [self height]); } #pragma mark - Hit Testing /** * Returns the string index at the given point within this line. * * Uses CTLineGetStringIndexForPosition to find which character * is at the given point. * * @param point The point to test (in the line's coordinate system) * @return The string index, or NSNotFound */ - (NSUInteger)stringIndexAtPoint:(CGPoint)point { if (!_ctLine) { return NSNotFound; } // Convert point to line-relative coordinates CGPoint linePoint = CGPointMake(point.x - _origin.x, point.y - _origin.y); // Use CTLine to find the character index CFIndex index = CTLineGetStringIndexForPosition(_ctLine, linePoint); if (index == kCFNotFound) { return NSNotFound; } return (NSUInteger)index; } /** * Returns the offset for a given string index. * * @param index The string index * @return The horizontal offset from the line's origin */ - (CGFloat)offsetForStringIndex:(NSUInteger)index { if (!_ctLine) { return 0; } // CTLineGetOffsetForStringIndex returns the offset from the line's origin CGFloat offset = CTLineGetOffsetForStringIndex(_ctLine, index, NULL); return offset; } /** * Returns the rect for a given string index. */ - (CGRect)rectForStringIndex:(NSUInteger)index { CGFloat xOffset = [self offsetForStringIndex:index]; return CGRectMake( _origin.x + xOffset, _origin.y - _cachedDescent, 1, // Width of 1 character [self height] ); } #pragma mark - String Operations /** * Returns the substring represented by this line. */ - (NSString *)substringFromAttributedString:(NSAttributedString *)attributedString { if (!attributedString || _stringRange.location >= [attributedString length]) { return nil; } // Clamp the range to the string length NSUInteger maxLength = [attributedString length] - _stringRange.location; NSUInteger length = MIN(_stringRange.length, maxLength); return [[attributedString string] substringWithRange:NSMakeRange(_stringRange.location, length)]; } /** * Returns the attributes at a given string index. */ - (NSDictionary *)attributesAtIndex:(NSUInteger)index fromAttributedString:(NSAttributedString *)attributedString { if (!attributedString || index >= [attributedString length]) { return nil; } return [attributedString attributesAtIndex:index effectiveRange:NULL]; } #pragma mark - Line Comparison /** * Compares this line to another line for ordering. */ - (NSComparisonResult)compareToLine:(DTCoreTextLayoutLine *)otherLine { // Compare by line index first if (_lineIndex < otherLine.lineIndex) { return NSOrderedAscending; } else if (_lineIndex > otherLine.lineIndex) { return NSOrderedDescending; } // If same index, compare by string range location if (_stringRange.location < otherLine.stringRange.location) { return NSOrderedAscending; } else if (_stringRange.location > otherLine.stringRange.location) { return NSOrderedDescending; } return NSOrderedSame; } /** * Returns whether this line contains the given string index. */ - (BOOL)containsStringIndex:(NSUInteger)index { return index >= _stringRange.location && index < _stringRange.location + _stringRange.length; } /** * Returns whether this line intersects with the given range. */ - (BOOL)intersectsRange:(NSRange)range { NSRange intersection = NSIntersectionRange(_stringRange, range); return intersection.length > 0; } #pragma mark - Drawing /** * Draws this line into a CGContext. * * @param context The CGContext to draw into */ - (void)drawInContext:(CGContextRef)context { if (!context || !_ctLine) { return; } // Save graphics state CGContextSaveGState(context); // Move to the line's origin CGContextSetTextPosition(context, _origin.x, _origin.y); // Draw the line CTLineDraw(_ctLine, context); // Restore graphics state CGContextRestoreGState(context); } /** * Draws this line with a specific color. * * @param context The CGContext to draw into * @param color The text color */ - (void)drawInContext:(CGContextRef)context withColor:(UIColor *)color { if (!context || !_ctLine || !color) { return; } // Save graphics state CGContextSaveGState(context); // Set the text color CGContextSetFillColorWithColor(context, color.CGColor); // Move to the line's origin CGContextSetTextPosition(context, _origin.x, _origin.y); // Draw the line CTLineDraw(_ctLine, context); // Restore graphics state CGContextRestoreGState(context); } @end