281 lines
6.8 KiB
Objective-C
281 lines
6.8 KiB
Objective-C
//
|
|
// 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 <CoreText/CoreText.h>
|
|
|
|
#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
|