ReadViewSDK/Doc/WXRead/decompiled/WRCoreTextLayouter.m
2026-05-21 19:40:51 +08:00

887 lines
27 KiB
Objective-C

//
// WRCoreTextLayouter.m
// WeRead
//
// Reverse-engineered from binary analysis
// CoreText typesetter wrapper implementation
//
// This file contains pseudo-code reconstruction of the WRCoreTextLayouter
// class based on ivar analysis, method signatures, and behavioral context.
//
#import "WRCoreTextLayouter.h"
#import "WRCoreTextLayoutFrame.h"
#import "WRMarkContentChapter.h"
#import <CoreText/CoreText.h>
// Forward declarations for internal C types (not in public headers)
typedef struct _CTTypesetter *CTTypesetterRef;
typedef struct _CTFramesetter *CTFramesetterRef;
#pragma mark - Internal Constants
// Size patterns for image resizing (from analysis of resizedImageForImagePath:)
static NSString * const kSizePatternFull = @"full";
static NSString * const kSizePatternHalf = @"half";
static NSString * const kSizePatternThird = @"third";
static NSString * const kSizePatternQuarter = @"quarter";
// Default layout configuration values
static const CGFloat kDefaultFrameWidth = 320.0;
static const CGFloat kDefaultFrameHeight = 480.0;
static const CGFloat kDefaultColumnGap = 20.0;
#pragma mark - WRCoreTextLayoutConfig Implementation
@implementation WRCoreTextLayoutConfig
- (instancetype)init {
self = [super init];
if (self) {
_frameWidth = kDefaultFrameWidth;
_frameHeight = kDefaultFrameHeight;
_edgeInsets = UIEdgeInsetsMake(10, 15, 10, 15);
_numberOfColumns = 1;
_columnGap = kDefaultColumnGap;
_avoidOrphans = YES;
_avoidWidows = YES;
_hyphenation = YES;
}
return self;
}
@end
#pragma mark - Private Interface
@interface WRCoreTextLayouter () {
// CoreText typesetter - the core text processing engine
// This is a C object that performs the actual glyph layout
CTTypesetterRef _typesetter;
// CoreText framesetter - creates frames from the typesetter
// Used when frame-based layout is needed (with paths)
CTFramesetterRef _framesetter;
// Track whether the typesetter needs recreation
BOOL _typesetterDirty;
// Track whether the framesetter needs recreation
BOOL _framesetterDirty;
// Internal lock for thread safety
NSLock *_layoutLock;
// Cache for image resizing operations
NSCache *_imageCache;
// Current layout configuration
WRCoreTextLayoutConfig *_config;
}
@end
#pragma mark - WRCoreTextLayouter Implementation
@implementation WRCoreTextLayouter
#pragma mark - Lifecycle
- (instancetype)init {
self = [super init];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString {
self = [super init];
if (self) {
[self commonInit];
_attributedString = [attributedString copy];
_internalAttributedString = [attributedString copy];
_typesetterDirty = YES;
}
return self;
}
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
config:(WRCoreTextLayoutConfig *)config {
self = [super init];
if (self) {
[self commonInit];
_attributedString = [attributedString copy];
_internalAttributedString = [attributedString copy];
_config = config;
_typesetterDirty = YES;
}
return self;
}
/**
* Common initialization - sets up internal state.
* Called by all init methods.
*/
- (void)commonInit {
_layoutLock = [[NSLock alloc] init];
_imageCache = [[NSCache alloc] init];
_imageCache.countLimit = 50; // Cache up to 50 resized images
_layoutFrames = [NSMutableArray array];
_typesetterDirty = YES;
_framesetterDirty = YES;
}
- (void)dealloc {
// Release CoreText objects - these are C objects, not ObjC
if (_typesetter) {
CFRelease(_typesetter);
_typesetter = NULL;
}
if (_framesetter) {
CFRelease(_framesetter);
_framesetter = NULL;
}
}
#pragma mark - Property Accessors
- (void)setAttributedString:(NSAttributedString *)attributedString {
// When the attributed string changes, mark typesetter as dirty
_attributedString = [attributedString copy];
_internalAttributedString = [attributedString copy];
_typesetterDirty = YES;
_framesetterDirty = YES;
// Clear existing layout frames as they're now invalid
[_layoutFrames removeAllObjects];
// Extract plain text for convenience
_plainText = [_attributedString string];
}
- (void)setPlainText:(NSString *)plainText {
_plainText = [plainText copy];
// Note: This doesn't update the attributed string - it's read-only metadata
}
#pragma mark - Typesetter Management
/**
* Creates the CTTypesetter from the current attributed string.
*
* CTTypesetter is the low-level CoreText object that performs glyph layout.
* It analyzes the attributed string and prepares it for line-by-line layout.
*
* Algorithm:
* 1. Validate we have an attributed string
* 2. Release existing typesetter if any
* 3. Create new CTTypesetter with the attributed string
* 4. Mark as clean
*/
- (void)createTypesetter {
[_layoutLock lock];
if (!_typesetterDirty && _typesetter) {
[_layoutLock unlock];
return; // Already up to date
}
// Release old typesetter
if (_typesetter) {
CFRelease(_typesetter);
_typesetter = NULL;
}
// Need attributed string to create typesetter
if (!_internalAttributedString) {
[_layoutLock unlock];
return;
}
// Create the CTTypesetter
// CTTypesetterCreateWithAttributedString analyzes the string and
// prepares internal data structures for efficient line breaking
_typesetter = CTTypesetterCreateWithAttributedString(
(__bridge CFAttributedStringRef)_internalAttributedString
);
_typesetterDirty = NO;
[_layoutLock unlock];
}
/**
* Creates the CTFramesetter from the current attributed string.
*
* CTFramesetter is a higher-level API that creates CTFrame objects.
* It internally manages its own CTTypesetter.
*/
- (void)createFramesetter {
[_layoutLock lock];
if (!_framesetterDirty && _framesetter) {
[_layoutLock unlock];
return;
}
if (_framesetter) {
CFRelease(_framesetter);
_framesetter = NULL;
}
if (!_internalAttributedString) {
[_layoutLock unlock];
return;
}
// CTFramesetterCreateWithAttributedString creates a framesetter
// that can produce CTFrame objects for arbitrary paths
_framesetter = CTFramesetterCreateWithAttributedString(
(__bridge CFAttributedStringRef)_internalAttributedString
);
_framesetterDirty = NO;
[_layoutLock unlock];
}
- (void)invalidateTypesetter {
[_layoutLock lock];
_typesetterDirty = YES;
_framesetterDirty = YES;
[_layoutLock unlock];
}
#pragma mark - Layout Frame Creation
/**
* Creates a WRCoreTextLayoutFrame for a given string range.
*
* This is the primary layout method. It:
* 1. Ensures the typesetter is ready
* 2. Creates a CTTypesetter layout for the range
* 3. Wraps it in a WRCoreTextLayoutFrame
*
* @param range Range of the attributed string to lay out
* @param frame Bounding rectangle for the layout
* @return A new WRCoreTextLayoutFrame
*/
- (WRCoreTextLayoutFrame *)layoutFrameWithRange:(NSRange)range
frame:(CGRect)frame {
[self createTypesetter];
if (!_typesetter) {
return nil;
}
[_layoutLock lock];
// Create a CGPath for the frame bounds
// The path defines the region where text will be laid out
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, frame);
// Create CTFrame from the typesetter
// CTTypesetterCreateFrame creates a frame for the given range within the path
CTFrameRef ctFrame = CTTypesetterCreateFrame(
_typesetter,
CFRangeMake(range.location, range.length),
path,
NULL // No frame attributes
);
CGPathRelease(path);
if (!ctFrame) {
[_layoutLock unlock];
return nil;
}
// Create our wrapper layout frame
WRCoreTextLayoutFrame *layoutFrame = [[WRCoreTextLayoutFrame alloc] init];
layoutFrame.attributedString = _internalAttributedString;
// The layout frame takes ownership of the CTFrame
[layoutFrame setCTFrame:ctFrame range:range];
CFRelease(ctFrame);
[_layoutFrames addObject:layoutFrame];
[_layoutLock unlock];
return layoutFrame;
}
/**
* Creates layout frames for the entire attributed string.
* This is the pagination method - it breaks content into pages.
*
* Algorithm:
* 1. Get the total string length
* 2. Start from index 0
* 3. For each page:
* a. Use CTTypesetterSuggestLineBreak to find how much fits
* b. Create a layout frame for that range
* c. Advance the cursor
* 4. Repeat until all text is laid out
*
* @param pageSize Size of each page
* @return Array of WRCoreTextLayoutFrame objects
*/
- (NSArray<WRCoreTextLayoutFrame *> *)layoutFramesForPageSize:(CGSize)pageSize {
[self createTypesetter];
if (!_typesetter || !_internalAttributedString) {
return @[];
}
[_layoutLock lock];
NSMutableArray<WRCoreTextLayoutFrame *> *frames = [NSMutableArray array];
NSUInteger totalLength = [_internalAttributedString length];
NSUInteger currentIndex = 0;
// Calculate the usable width (accounting for margins)
UIEdgeInsets insets = _config ? _config.edgeInsets : UIEdgeInsetsMake(10, 15, 10, 15);
CGFloat usableWidth = pageSize.width - insets.left - insets.right;
while (currentIndex < totalLength) {
// CTTypesetterSuggestLineBreak suggests how many characters fit in the width
// This is the core pagination algorithm
CFIndex lineBreakIndex = CTTypesetterSuggestLineBreak(
_typesetter,
currentIndex,
usableWidth
);
if (lineBreakIndex <= 0) {
// Safety: avoid infinite loop
break;
}
NSRange pageRange = NSMakeRange(currentIndex, lineBreakIndex);
CGRect pageRect = CGRectMake(0, 0, pageSize.width, pageSize.height);
// Create layout frame for this page
WRCoreTextLayoutFrame *frame = [self layoutFrameWithRange:pageRange
frame:pageRect];
if (frame) {
[frames addObject:frame];
}
currentIndex += lineBreakIndex;
}
[_layoutLock unlock];
return [frames copy];
}
- (WRCoreTextLayoutFrame *)layoutFrameForRange:(NSRange)range
rect:(CGRect)rect
columns:(NSUInteger)columns {
if (range.length == 0) {
return nil;
}
// For multi-column layout, we'd need to create a more complex path
if (columns > 1) {
// Create column-based path
CGMutablePathRef path = CGPathCreateMutable();
CGFloat columnWidth = (rect.size.width - (columns - 1) * _config.columnGap) / columns;
for (NSUInteger i = 0; i < columns; i++) {
CGFloat x = rect.origin.x + i * (columnWidth + _config.columnGap);
CGRect columnRect = CGRectMake(x, rect.origin.y, columnWidth, rect.size.height);
CGPathAddRect(path, NULL, columnRect);
}
[self createFramesetter];
if (!_framesetter) {
CGPathRelease(path);
return nil;
}
[_layoutLock lock];
CTFrameRef ctFrame = CTFramesetterCreateFrame(
_framesetter,
CFRangeMake(range.location, range.length),
path,
NULL
);
CGPathRelease(path);
if (!ctFrame) {
[_layoutLock unlock];
return nil;
}
WRCoreTextLayoutFrame *layoutFrame = [[WRCoreTextLayoutFrame alloc] init];
layoutFrame.attributedString = _internalAttributedString;
[layoutFrame setCTFrame:ctFrame range:range];
CFRelease(ctFrame);
[_layoutLock unlock];
return layoutFrame;
}
// Single column - use the simpler typesetter path
return [self layoutFrameWithRange:range frame:rect];
}
#pragma mark - Page Background Images
/**
* Generates a page background image for a given text range.
*
* This method creates a background image that can include:
* - Theme-specific background textures
* - Decorative elements (borders, patterns)
* - Chapter-specific backgrounds (e.g., for chapter openings)
*
* The themeBgColor is used to tint the background to match the current theme.
*
* @param range The text range on the page
* @param themeBgColor The theme's background color
* @return A UIImage for the page background
*/
- (UIImage *)pageBackgroundImageAtRange:(NSRange)range
themeBgColor:(UIColor *)themeBgColor {
// Check cache first
NSString *cacheKey = [NSString stringWithFormat:@"bg_%lu_%lu_%@",
(unsigned long)range.location,
(unsigned long)range.length,
themeBgColor];
UIImage *cachedImage = [_imageCache objectForKey:cacheKey];
if (cachedImage) {
return cachedImage;
}
// Get the text in this range to check for special content
NSString *pageText = [_internalAttributedString.string substringWithRange:range];
// Determine the background type based on content
// Chapter openings might get special treatment
BOOL isChapterStart = (range.location == 0);
BOOL hasChapterTitle = [pageText containsString:@""] ||
[pageText containsString:@"Chapter"];
CGSize imageSize = _config ? CGSizeMake(_config.frameWidth, _config.frameHeight)
: CGSizeMake(320, 480);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// Fill with theme background color
[themeBgColor setFill];
CGContextFillRect(context, CGRectMake(0, 0, imageSize.width, imageSize.height));
if (isChapterStart || hasChapterTitle) {
// Special background for chapter start pages
// Could include decorative borders, ornamental elements
[self drawChapterStartDecorationInContext:context size:imageSize color:themeBgColor];
} else {
// Regular page background
// Could include subtle patterns, margins, page numbers area
[self drawRegularPageDecorationInContext:context size:imageSize color:themeBgColor];
}
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Cache the result
if (result) {
[_imageCache setObject:result forKey:cacheKey];
}
return result;
}
/**
* Draws decorative elements for chapter start pages.
* This includes ornamental borders and chapter-specific decorations.
*/
- (void)drawChapterStartDecorationInContext:(CGContextRef)context
size:(CGSize)size
color:(UIColor *)color {
// Draw a decorative border
CGRect borderRect = CGRectInset(CGRectMake(0, 0, size.width, size.height), 15, 20);
CGContextSetStrokeColorWithColor(context, [color colorWithAlphaComponent:0.3].CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, borderRect);
// Could add more ornamental elements:
// - Corner decorations
// - Header/footer ornaments
// - Drop cap indicators
}
/**
* Draws standard page decorations (margins, guides).
*/
- (void)drawRegularPageDecorationInContext:(CGContextRef)context
size:(CGSize)size
color:(UIColor *)color {
// Draw subtle margin guides
CGFloat leftMargin = _config ? _config.edgeInsets.left : 15;
CGFloat rightMargin = _config ? _config.edgeInsets.right : 15;
CGContextSetStrokeColorWithColor(context, [color colorWithAlphaComponent:0.1].CGColor);
CGContextSetLineWidth(context, 0.5);
// Left margin line
CGContextMoveToPoint(context, leftMargin, 0);
CGContextAddLineToPoint(context, leftMargin, size.height);
CGContextStrokePath(context);
// Right margin line
CGContextMoveToPoint(context, size.width - rightMargin, 0);
CGContextAddLineToPoint(context, size.width - rightMargin, size.height);
CGContextStrokePath(context);
}
#pragma mark - Image Utilities
/**
* Resizes an image for display within the text layout.
*
* This method handles various image sizing scenarios:
* - Full-width images
* - Half-width images (side by side)
* - Thumbnail-sized images
* - Dark mode adaptations
*
* The sizePattern parameter controls how the image is scaled:
* - "full": Scale to fill the available width
* - "half": Scale to half width
* - "third": Scale to one-third width
* - "quarter": Scale to one-quarter width
*
* @param imagePath Path to the image file or URL string
* @param rect Target rectangle for positioning
* @param position Position index (for multi-image layouts)
* @param sizePattern Size pattern string
* @param darkMode Whether to apply dark mode adjustments
* @param themeBgColor Theme background color for blending
* @return Resized UIImage
*/
- (UIImage *)resizedImageForImagePath:(NSString *)imagePath
rect:(CGRect)rect
position:(NSUInteger)position
sizePattern:(NSString *)sizePattern
darkMode:(BOOL)darkMode
themeBgColor:(UIColor *)themeBgColor {
// Generate cache key
NSString *cacheKey = [NSString stringWithFormat:@"img_%@_%@_%lu_%d",
imagePath, sizePattern, (unsigned long)position, darkMode];
UIImage *cachedImage = [_imageCache objectForKey:cacheKey];
if (cachedImage) {
return cachedImage;
}
// Load the original image
UIImage *originalImage = nil;
// Handle different image path formats
if ([imagePath hasPrefix:@"http://"] || [imagePath hasPrefix:@"https://"]) {
// Remote image - would need async loading
// For now, return placeholder
originalImage = [UIImage imageNamed:@"placeholder_book_image"];
} else if ([imagePath hasPrefix:@"/"]) {
// Absolute file path
originalImage = [UIImage imageWithContentsOfFile:imagePath];
} else {
// Bundle resource
originalImage = [UIImage imageNamed:imagePath];
}
if (!originalImage) {
// Return a placeholder image
return [self placeholderImageForSize:rect.size];
}
// Calculate target size based on size pattern
CGSize targetSize = [self targetSizeForPattern:sizePattern
rect:rect
imageSize:originalImage.size];
// Handle dark mode
if (darkMode) {
originalImage = [self darkModeAdjustedImage:originalImage
withBgColor:themeBgColor];
}
// Resize the image
UIImage *resizedImage = [self resizeImage:originalImage toSize:targetSize];
// Cache the result
if (resizedImage) {
[_imageCache setObject:resizedImage forKey:cacheKey];
}
return resizedImage;
}
/**
* Calculates the target size based on a size pattern string.
*/
- (CGSize)targetSizeForPattern:(NSString *)pattern
rect:(CGRect)rect
imageSize:(CGSize)imageSize {
CGFloat availableWidth = rect.size.width;
if ([pattern isEqualToString:kSizePatternFull]) {
// Full width - maintain aspect ratio
CGFloat scale = availableWidth / imageSize.width;
return CGSizeMake(availableWidth, imageSize.height * scale);
} else if ([pattern isEqualToString:kSizePatternHalf]) {
// Half width
CGFloat halfWidth = availableWidth / 2.0;
CGFloat scale = halfWidth / imageSize.width;
return CGSizeMake(halfWidth, imageSize.height * scale);
} else if ([pattern isEqualToString:kSizePatternThird]) {
// One-third width
CGFloat thirdWidth = availableWidth / 3.0;
CGFloat scale = thirdWidth / imageSize.width;
return CGSizeMake(thirdWidth, imageSize.height * scale);
} else if ([pattern isEqualToString:kSizePatternQuarter]) {
// One-quarter width
CGFloat quarterWidth = availableWidth / 4.0;
CGFloat scale = quarterWidth / imageSize.width;
return CGSizeMake(quarterWidth, imageSize.height * scale);
}
// Default: fit within the rect while maintaining aspect ratio
return [self fitSize:imageSize inSize:rect.size];
}
/**
* Resizes an image to the target size using high-quality interpolation.
*/
- (UIImage *)resizeImage:(UIImage *)image toSize:(CGSize)size {
UIGraphicsBeginImageContextWithOptions(size, YES, [UIScreen mainScreen].scale);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
/**
* Adjusts an image for dark mode by blending with the background color.
*/
- (UIImage *)darkModeAdjustedImage:(UIImage *)image withBgColor:(UIColor *)bgColor {
CGSize size = image.size;
UIGraphicsBeginImageContextWithOptions(size, YES, [UIScreen mainScreen].scale);
// Draw background
[bgColor setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, size.width, size.height));
// Draw image with reduced opacity for dark mode blending
[image drawInRect:CGRectMake(0, 0, size.width, size.height)
blendMode:kCGBlendModeNormal
alpha:0.85];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
/**
* Calculates the size that fits within a container while maintaining aspect ratio.
*/
- (CGSize)fitSize:(CGSize)imageSize inSize:(CGSize)containerSize {
CGFloat widthRatio = containerSize.width / imageSize.width;
CGFloat heightRatio = containerSize.height / imageSize.height;
CGFloat scale = MIN(widthRatio, heightRatio);
return CGSizeMake(imageSize.width * scale, imageSize.height * scale);
}
/**
* Creates a placeholder image for missing images.
*/
- (UIImage *)placeholderImageForSize:(CGSize)size {
UIGraphicsBeginImageContextWithOptions(size, YES, [UIScreen mainScreen].scale);
// Light gray background
[[UIColor colorWithWhite:0.9 alpha:1.0] setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, size.width, size.height));
// Draw a simple icon indicator
[[UIColor colorWithWhite:0.7 alpha:1.0] setFill];
CGFloat iconSize = MIN(size.width, size.height) * 0.3;
CGFloat x = (size.width - iconSize) / 2;
CGFloat y = (size.height - iconSize) / 2;
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(x, y, iconSize, iconSize));
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
#pragma mark - Pagination Helpers
/**
* Calculates the total number of pages for a given page size.
*
* Uses CTTypesetterSuggestLineBreak to simulate pagination
* without actually creating layout frame objects.
*/
- (NSUInteger)numberOfPagesForPageSize:(CGSize)pageSize {
[self createTypesetter];
if (!_typesetter || !_internalAttributedString) {
return 0;
}
[_layoutLock lock];
NSUInteger pageCount = 0;
NSUInteger totalLength = [_internalAttributedString length];
NSUInteger currentIndex = 0;
UIEdgeInsets insets = _config ? _config.edgeInsets : UIEdgeInsetsMake(10, 15, 10, 15);
CGFloat usableWidth = pageSize.width - insets.left - insets.right;
while (currentIndex < totalLength) {
CFIndex lineBreakIndex = CTTypesetterSuggestLineBreak(
_typesetter,
currentIndex,
usableWidth
);
if (lineBreakIndex <= 0) {
break;
}
pageCount++;
currentIndex += lineBreakIndex;
}
[_layoutLock unlock];
return pageCount;
}
/**
* Returns the text range for a specific page index.
*/
- (NSRange)rangeForPageAtIndex:(NSUInteger)pageIndex
pageSize:(CGSize)pageSize {
[self createTypesetter];
if (!_typesetter || !_internalAttributedString) {
return NSMakeRange(0, 0);
}
[_layoutLock lock];
NSUInteger totalLength = [_internalAttributedString length];
NSUInteger currentIndex = 0;
NSUInteger currentPage = 0;
UIEdgeInsets insets = _config ? _config.edgeInsets : UIEdgeInsetsMake(10, 15, 10, 15);
CGFloat usableWidth = pageSize.width - insets.left - insets.right;
while (currentIndex < totalLength && currentPage <= pageIndex) {
CFIndex lineBreakIndex = CTTypesetterSuggestLineBreak(
_typesetter,
currentIndex,
usableWidth
);
if (lineBreakIndex <= 0) {
break;
}
if (currentPage == pageIndex) {
[_layoutLock unlock];
return NSMakeRange(currentIndex, lineBreakIndex);
}
currentIndex += lineBreakIndex;
currentPage++;
}
[_layoutLock unlock];
return NSMakeRange(0, 0);
}
/**
* Returns suggested line fragment heights for precise layout calculations.
*
* This method analyzes the attributed string and returns the heights
* of each line as CoreText would lay them out. This is used for:
* - Precise pagination calculations
* - Avoiding orphans and widows
* - Ensuring consistent line spacing
*/
- (NSArray<NSNumber *> *)suggestedLineFragHeights {
[self createTypesetter];
if (!_typesetter || !_internalAttributedString) {
return @[];
}
[_layoutLock lock];
NSMutableArray<NSNumber *> *heights = [NSMutableArray array];
NSUInteger totalLength = [_internalAttributedString length];
NSUInteger currentIndex = 0;
// Use a large width to get all lines in a single column
CGFloat width = _config ? _config.frameWidth : 320.0;
while (currentIndex < totalLength) {
CFIndex lineBreakIndex = CTTypesetterSuggestLineBreak(
_typesetter,
currentIndex,
width
);
if (lineBreakIndex <= 0) {
break;
}
// Create a temporary CTLine to measure its height
CTLineRef line = CTTypesetterCreateLine(
_typesetter,
CFRangeMake(currentIndex, lineBreakIndex)
);
if (line) {
CGFloat ascent, descent, leading;
CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat lineHeight = ascent + descent + leading;
[heights addObject:@(lineHeight)];
CFRelease(line);
}
currentIndex += lineBreakIndex;
}
[_layoutLock unlock];
return [heights copy];
}
@end