Epub阅读器0.0.1
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
//
|
||||
// WREpubPositionConverter.m
|
||||
// WeRead (微信读书)
|
||||
// Reverse-engineered implementation reconstruction
|
||||
//
|
||||
// Detailed pseudo-code based on binary analysis, ivar types, known methods,
|
||||
// and contextual knowledge of EPUB position/bookmark handling.
|
||||
//
|
||||
|
||||
#import "WREpubPositionConverter.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WRPositionPair
|
||||
// ---------------------------------------------------------------------------
|
||||
@implementation WRPositionPair
|
||||
|
||||
- (instancetype)initWithRow:(NSInteger)row column:(NSInteger)column
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_row = row;
|
||||
_column = column;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<WRPositionPair row=%ld col=%ld>",
|
||||
(long)_row, (long)_column];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WREpubPositionConverter
|
||||
// ---------------------------------------------------------------------------
|
||||
@implementation WREpubPositionConverter
|
||||
{
|
||||
// Core data from binary analysis
|
||||
NSArray *_filePaths;
|
||||
NSArray *_attributedStrings;
|
||||
NSMutableArray *_fileLengths; // character count per file
|
||||
NSMutableArray *_cumulativeOffsets; // prefix sum of character counts
|
||||
NSMutableDictionary *_indexCache; // cache: key -> index
|
||||
NSMutableDictionary *_stringRangeCache; // cache: key -> NSRange
|
||||
|
||||
// Additional state inferred from context
|
||||
NSInteger _baseOffset; // offset for non-chapter content
|
||||
BOOL _containsIntroFlyleaf; // includes cover/flyleaf page
|
||||
BOOL _indicesBuilt; // whether initIndices has been called
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
- (instancetype)initWithFilePaths:(NSArray<NSString *> *)filePaths
|
||||
attributedStrings:(NSArray<NSAttributedString *> *)attributedStrings
|
||||
offset:(NSInteger)offset
|
||||
isContainIntroFlyleaf:(BOOL)isContainIntroFlyleaf
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_filePaths = [filePaths copy];
|
||||
_attributedStrings = [attributedStrings copy];
|
||||
_baseOffset = offset;
|
||||
_containsIntroFlyleaf = isContainIntroFlyleaf;
|
||||
|
||||
_fileLengths = [NSMutableArray arrayWithCapacity:filePaths.count];
|
||||
_cumulativeOffsets = [NSMutableArray arrayWithCapacity:filePaths.count];
|
||||
_indexCache = [NSMutableDictionary dictionary];
|
||||
_stringRangeCache = [NSMutableDictionary dictionary];
|
||||
_indicesBuilt = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Index Building
|
||||
|
||||
- (void)initIndices
|
||||
{
|
||||
if (_indicesBuilt) return;
|
||||
|
||||
[_fileLengths removeAllObjects];
|
||||
[_cumulativeOffsets removeAllObjects];
|
||||
|
||||
NSInteger cumulative = _baseOffset;
|
||||
|
||||
for (NSUInteger i = 0; i < _attributedStrings.count; i++) {
|
||||
NSAttributedString *attrStr = _attributedStrings[i];
|
||||
NSInteger length = (NSInteger)attrStr.string.length;
|
||||
|
||||
[_fileLengths addObject:@(length)];
|
||||
|
||||
// Cumulative offset = sum of lengths of all previous files + baseOffset
|
||||
[_cumulativeOffsets addObject:@(cumulative)];
|
||||
cumulative += length;
|
||||
}
|
||||
|
||||
_indicesBuilt = YES;
|
||||
}
|
||||
|
||||
#pragma mark - Position Conversion: Row/Column -> String Index
|
||||
|
||||
- (void)indicesInFile:(NSString *)filePath
|
||||
forRowColumnPairs:(NSArray<WRPositionPair *> *)rowColumnPairs
|
||||
stringIndices:(NSArray *_Nullable *_Nullable)stringIndices
|
||||
string:(NSString *)string
|
||||
fileIndexOffset:(NSInteger)fileIndexOffset
|
||||
stringIndexOffset:(NSInteger)stringIndexOffset
|
||||
{
|
||||
// 1. Find the file index for the given path
|
||||
NSInteger fileIndex = [_filePaths indexOfObject:filePath];
|
||||
if (fileIndex == NSNotFound) {
|
||||
// Try matching by last path component (filename only)
|
||||
NSString *filename = [filePath lastPathComponent];
|
||||
for (NSUInteger i = 0; i < _filePaths.count; i++) {
|
||||
if ([[_filePaths[i] lastPathComponent] isEqualToString:filename]) {
|
||||
fileIndex = (NSInteger)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileIndex == NSNotFound) {
|
||||
if (stringIndices) *stringIndices = @[];
|
||||
return;
|
||||
}
|
||||
|
||||
fileIndex += fileIndexOffset;
|
||||
|
||||
// 2. Build a line-offset table from the string
|
||||
//
|
||||
// We need to map (row, column) -> character offset within the string.
|
||||
// A "row" is a line number; "column" is the character position in that line.
|
||||
//
|
||||
// Strategy: scan the string and record the starting offset of each line.
|
||||
|
||||
NSMutableArray<NSNumber *> *lineStarts = [NSMutableArray array];
|
||||
[lineStarts addObject:@(0)]; // line 0 starts at offset 0
|
||||
|
||||
NSUInteger len = string.length;
|
||||
for (NSUInteger i = 0; i < len; i++) {
|
||||
unichar c = [string characterAtIndex:i];
|
||||
if (c == '\n' || c == '\r') {
|
||||
// Handle \r\n as a single line ending
|
||||
if (c == '\r' && i + 1 < len && [string characterAtIndex:i + 1] == '\n') {
|
||||
i++; // skip the \n
|
||||
}
|
||||
if (i + 1 < len) {
|
||||
[lineStarts addObject:@(i + 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Convert each (row, column) pair to a string index
|
||||
NSMutableArray<NSNumber *> *results =
|
||||
[NSMutableArray arrayWithCapacity:rowColumnPairs.count];
|
||||
|
||||
for (WRPositionPair *pair in rowColumnPairs) {
|
||||
NSInteger row = pair.row;
|
||||
NSInteger column = pair.column;
|
||||
|
||||
if (row < 0 || row >= (NSInteger)lineStarts.count) {
|
||||
[results addObject:@(NSNotFound)];
|
||||
continue;
|
||||
}
|
||||
|
||||
NSInteger lineStart = [lineStarts[row] integerValue];
|
||||
|
||||
// Find the end of this line
|
||||
NSInteger lineEnd;
|
||||
if (row + 1 < (NSInteger)lineStarts.count) {
|
||||
lineEnd = [lineStarts[row + 1] integerValue] - 1;
|
||||
} else {
|
||||
lineEnd = (NSInteger)len;
|
||||
}
|
||||
|
||||
NSInteger lineLength = lineEnd - lineStart;
|
||||
if (column > lineLength) {
|
||||
column = lineLength; // clamp to end of line
|
||||
}
|
||||
|
||||
NSInteger stringIndex = lineStart + column + stringIndexOffset;
|
||||
|
||||
// Also add the cumulative offset for this file to get the global position
|
||||
if (fileIndex >= 0 && fileIndex < (NSInteger)_cumulativeOffsets.count) {
|
||||
stringIndex += [_cumulativeOffsets[fileIndex] integerValue];
|
||||
}
|
||||
|
||||
[results addObject:@(stringIndex)];
|
||||
}
|
||||
|
||||
if (stringIndices) {
|
||||
*stringIndices = [results copy];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Position Conversion: File Range -> String Range
|
||||
|
||||
- (NSRange)stringRangeFromFileRange:(NSDictionary *)fileRange
|
||||
{
|
||||
// Expected keys in fileRange:
|
||||
// @"fileIndex" -> NSNumber (NSInteger)
|
||||
// @"start" -> NSNumber (NSInteger) local offset within the file
|
||||
// @"end" -> NSNumber (NSInteger) local offset within the file
|
||||
|
||||
NSNumber *fileIndexNum = fileRange[@"fileIndex"];
|
||||
NSNumber *startNum = fileRange[@"start"];
|
||||
NSNumber *endNum = fileRange[@"end"];
|
||||
|
||||
if (!fileIndexNum || !startNum || !endNum) {
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
NSInteger fileIndex = [fileIndexNum integerValue];
|
||||
NSInteger localStart = [startNum integerValue];
|
||||
NSInteger localEnd = [endNum integerValue];
|
||||
|
||||
// Check cache
|
||||
NSString *cacheKey =
|
||||
[NSString stringWithFormat:@"%ld:%ld:%ld", (long)fileIndex,
|
||||
(long)localStart, (long)localEnd];
|
||||
NSNumber *cached = _stringRangeCache[cacheKey];
|
||||
if (cached) {
|
||||
return NSRangeFromString(cached.stringValue);
|
||||
}
|
||||
|
||||
// Validate file index
|
||||
if (!_indicesBuilt) [self initIndices];
|
||||
if (fileIndex < 0 || fileIndex >= (NSInteger)_cumulativeOffsets.count) {
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
// Get the cumulative offset for this file
|
||||
NSInteger cumulativeOffset = [_cumulativeOffsets[fileIndex] integerValue];
|
||||
|
||||
// File-local length
|
||||
NSInteger fileLength = [_fileLengths[fileIndex] integerValue];
|
||||
|
||||
// Clamp to file bounds
|
||||
if (localStart < 0) localStart = 0;
|
||||
if (localEnd > fileLength) localEnd = fileLength;
|
||||
if (localStart >= localEnd) {
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
NSInteger globalStart = cumulativeOffset + localStart;
|
||||
NSInteger length = localEnd - localStart;
|
||||
|
||||
NSRange result = NSMakeRange(globalStart, length);
|
||||
|
||||
// Cache the result
|
||||
_stringRangeCache[cacheKey] = NSStringFromRange(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark - Utility
|
||||
|
||||
- (NSInteger)totalCharacterCount
|
||||
{
|
||||
if (!_indicesBuilt) [self initIndices];
|
||||
|
||||
if (_cumulativeOffsets.count == 0) return _baseOffset;
|
||||
|
||||
NSInteger lastCumulative =
|
||||
[_cumulativeOffsets.lastObject integerValue];
|
||||
NSInteger lastLength = [_fileLengths.lastObject integerValue];
|
||||
|
||||
return lastCumulative + lastLength;
|
||||
}
|
||||
|
||||
- (NSInteger)fileIndexForCharacterPosition:(NSInteger)position
|
||||
{
|
||||
if (!_indicesBuilt) [self initIndices];
|
||||
|
||||
// Binary search through cumulative offsets
|
||||
NSInteger lo = 0;
|
||||
NSInteger hi = (NSInteger)_cumulativeOffsets.count - 1;
|
||||
NSInteger result = -1;
|
||||
|
||||
while (lo <= hi) {
|
||||
NSInteger mid = lo + (hi - lo) / 2;
|
||||
NSInteger offset = [_cumulativeOffsets[mid] integerValue];
|
||||
|
||||
if (offset <= position) {
|
||||
result = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the position is within this file's range
|
||||
if (result >= 0) {
|
||||
NSInteger fileLen = [_fileLengths[result] integerValue];
|
||||
NSInteger cumOff = [_cumulativeOffsets[result] integerValue];
|
||||
if (position >= cumOff + fileLen) {
|
||||
result = -1; // beyond end of last file
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSInteger)localOffsetInFileAtIndex:(NSInteger)fileIndex
|
||||
forGlobalPosition:(NSInteger)position
|
||||
{
|
||||
if (!_indicesBuilt) [self initIndices];
|
||||
|
||||
if (fileIndex < 0 || fileIndex >= (NSInteger)_cumulativeOffsets.count) {
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
NSInteger cumulativeOffset = [_cumulativeOffsets[fileIndex] integerValue];
|
||||
NSInteger localOffset = position - cumulativeOffset;
|
||||
|
||||
NSInteger fileLen = [_fileLengths[fileIndex] integerValue];
|
||||
if (localOffset < 0 || localOffset >= fileLen) {
|
||||
return NSNotFound;
|
||||
}
|
||||
|
||||
return localOffset;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user