490 lines
12 KiB
Objective-C
490 lines
12 KiB
Objective-C
//
|
|
// 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 <CoreText/CoreText.h>
|
|
|
|
#pragma mark - Private Interface
|
|
|
|
@interface DTCoreTextLayoutLine () {
|
|
// Cached glyph runs
|
|
NSArray<DTCoreTextGlyphRun *> *_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<DTCoreTextGlyphRun *> *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<DTCoreTextGlyphRun *> *)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<DTCoreTextGlyphRun *> *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
|