// // DTCoreTextLayouter.m // WeRead // // Reverse-engineered from binary analysis // Based on DTCoreText open-source library, customized by WeRead // // This file contains pseudo-code reconstruction of the DTCoreTextLayouter // class based on ivar analysis, method signatures, and the open-source DTCoreText library. // #import "DTCoreTextLayouter.h" #import "DTCoreTextLayoutFrame.h" #import #pragma mark - Private Interface @interface DTCoreTextLayouter () { // CoreText typesetter - the core text processing engine CTTypesetterRef _typesetter; // Track whether the typesetter is valid BOOL _typesetterValid; // Internal lock for thread safety NSLock *_lock; } @end #pragma mark - DTCoreTextLayouter Implementation @implementation DTCoreTextLayouter #pragma mark - Lifecycle - (instancetype)init { self = [super init]; if (self) { _layoutFrameCache = [[NSCache alloc] init]; _layoutFrameCache.countLimit = 20; // Cache up to 20 layout frames _layoutFrames = [NSMutableArray array]; _typesetterValid = NO; _lock = [[NSLock alloc] init]; } return self; } - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString { self = [self init]; if (self) { _attributedString = attributedString; } return self; } - (void)dealloc { if (_typesetter) { CFRelease(_typesetter); _typesetter = NULL; } } #pragma mark - Property Accessors - (void)setAttributedString:(NSAttributedString *)attributedString { [_lock lock]; _attributedString = attributedString; // Invalidate the typesetter when the string changes _typesetterValid = NO; // Clear the layout frames array [_layoutFrames removeAllObjects]; [_lock unlock]; } #pragma mark - Typesetter Management /** * Returns the internal CTTypesetter, creating it if necessary. * * CTTypesetter is the low-level CoreText object that performs glyph layout. * It analyzes the attributed string and prepares it for line-by-line layout. * * @return The CTTypesetter reference */ - (CTTypesetterRef)typesetter { [_lock lock]; if (!_typesetterValid || !_typesetter) { // Release old typesetter if any if (_typesetter) { CFRelease(_typesetter); _typesetter = NULL; } if (_attributedString) { // Create new typesetter from the attributed string _typesetter = CTTypesetterCreateWithAttributedString( (__bridge CFAttributedStringRef)_attributedString ); _typesetterValid = YES; } } CTTypesetterRef result = _typesetter; [_lock unlock]; return result; } /** * Invalidates the current typesetter. * Forces recreation on next access. */ - (void)invalidateTypesetter { [_lock lock]; _typesetterValid = NO; [_lock unlock]; } #pragma mark - Layout Frame Creation /** * Creates a DTCoreTextLayoutFrame for a given string range. * * This method creates a layout frame by: * 1. Getting the typesetter * 2. Creating a CTFrame for the range within the given rect * 3. Wrapping it in a DTCoreTextLayoutFrame * * @param range Range of the attributed string to lay out * @param frame Bounding rectangle for the layout * @return A new DTCoreTextLayoutFrame */ - (DTCoreTextLayoutFrame *)layoutFrameWithRange:(NSRange)range frame:(CGRect)frame { CTTypesetterRef typesetter = [self typesetter]; if (!typesetter || range.length == 0) { return nil; } [_lock lock]; // Create a CGPath for the frame bounds CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, frame); // Create CTFrame from the typesetter CTFrameRef ctFrame = CTTypesetterCreateFrame( typesetter, CFRangeMake(range.location, range.length), path, NULL ); CGPathRelease(path); if (!ctFrame) { [_lock unlock]; return nil; } // Create layout frame wrapper DTCoreTextLayoutFrame *layoutFrame = [[DTCoreTextLayoutFrame alloc] initWithAttributedString:_attributedString range:range ctFrame:ctFrame]; CFRelease(ctFrame); [_layoutFrames addObject:layoutFrame]; [_lock unlock]; return layoutFrame; } /** * Suggests a line break for the given start index and width. * * Uses CTTypesetterSuggestLineBreak to determine how many characters * fit within the given width. * * @param startIndex The starting index in the string * @param width The available width * @return The suggested line break index */ - (NSUInteger)suggestLineBreakStartIndex:(NSUInteger)startIndex width:(CGFloat)width { CTTypesetterRef typesetter = [self typesetter]; if (!typesetter) { return 0; } // CTTypesetterSuggestLineBreak returns the number of characters // that fit in the given width starting from startIndex CFIndex breakIndex = CTTypesetterSuggestLineBreak( typesetter, startIndex, width ); return (NSUInteger)breakIndex; } /** * Suggests a fitting string length for pagination. * * This method simulates pagination to determine how much text * fits within the given constraints. * * @param width The available width * @param startIndex The starting index * @return The suggested fitting length */ - (NSUInteger)stringIndexFittingLengthForWidth:(CGFloat)width startIndex:(NSUInteger)startIndex { CTTypesetterRef typesetter = [self typesetter]; if (!_attributedString || !typesetter) { return 0; } NSUInteger totalLength = [_attributedString length]; NSUInteger currentIndex = startIndex; NSUInteger totalFitted = 0; // Simulate line-by-line layout to find total fitting length while (currentIndex < totalLength) { CFIndex lineBreak = CTTypesetterSuggestLineBreak( typesetter, currentIndex, width ); if (lineBreak <= 0) { break; } totalFitted += lineBreak; currentIndex += lineBreak; } return totalFitted; } #pragma mark - Frame Caching /** * Returns a cached layout frame for the given key. */ - (DTCoreTextLayoutFrame *)cachedLayoutFrameForKey:(NSString *)key { return [_layoutFrameCache objectForKey:key]; } /** * Caches a layout frame with the given key. */ - (void)cacheLayoutFrame:(DTCoreTextLayoutFrame *)frame forKey:(NSString *)key { if (frame && key) { [_layoutFrameCache setObject:frame forKey:key]; } } /** * Clears the layout frame cache. */ - (void)clearLayoutFrameCache { [_layoutFrameCache removeAllObjects]; } @end