Epub阅读器0.0.1
This commit is contained in:
@@ -0,0 +1,886 @@
|
||||
//
|
||||
// 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
|
||||
Reference in New Issue
Block a user