ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift
2026-05-26 23:27:52 +08:00

230 lines
9.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import CryptoKit
// MARK: - WXRead
///
///
/// WXRead WRChapterPageCount NSRange +
/// NSAttributedString HTML CoreText
public struct RDEPUBTextChapterPaginationCache: Equatable {
///
public var href: String
///
public var pageRanges: [NSRange]
///
public var breakReasons: [RDEPUBTextPageBreakReason]
public var semanticHints: [RDEPUBTextSemanticHint]
public init(
href: String,
pageRanges: [NSRange],
breakReasons: [RDEPUBTextPageBreakReason],
semanticHints: [RDEPUBTextSemanticHint]
) {
self.href = href
self.pageRanges = pageRanges
self.breakReasons = breakReasons
self.semanticHints = semanticHints
}
}
// MARK: - NSCoding 使
/// NSKeyedArchiver
///
final class PaginationCacheArchive: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
let chapters: [ChapterPaginationArchive]
init(chapters: [ChapterPaginationArchive]) {
self.chapters = chapters
}
func encode(with coder: NSCoder) {
coder.encode(chapters, forKey: "chapters")
}
required init?(coder: NSCoder) {
guard let chapters = coder.decodeObject(of: [NSArray.self, ChapterPaginationArchive.self], forKey: "chapters") as? [ChapterPaginationArchive] else { return nil }
self.chapters = chapters
}
}
/// NSRange location/length 便 NSSecureCoding
final class ChapterPaginationArchive: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool { true }
let href: String
/// rangeLengths
let rangeLocations: [NSNumber]
///
let rangeLengths: [NSNumber]
///
let breakReasons: [String]
///
let semanticHints: [String]
///
init(from cache: RDEPUBTextChapterPaginationCache) {
self.href = cache.href
self.rangeLocations = cache.pageRanges.map { NSNumber(value: $0.location) }
self.rangeLengths = cache.pageRanges.map { NSNumber(value: $0.length) }
self.breakReasons = cache.breakReasons.map(\.rawValue)
self.semanticHints = cache.semanticHints.map(\.rawValue)
}
func encode(with coder: NSCoder) {
coder.encode(href, forKey: "href")
coder.encode(rangeLocations, forKey: "rangeLocations")
coder.encode(rangeLengths, forKey: "rangeLengths")
coder.encode(breakReasons, forKey: "breakReasons")
coder.encode(semanticHints, forKey: "semanticHints")
}
required init?(coder: NSCoder) {
guard let href = coder.decodeObject(of: NSString.self, forKey: "href") as String?,
let rangeLocations = coder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "rangeLocations") as? [NSNumber],
let rangeLengths = coder.decodeObject(of: [NSArray.self, NSNumber.self], forKey: "rangeLengths") as? [NSNumber],
let breakReasons = coder.decodeObject(of: [NSArray.self, NSString.self], forKey: "breakReasons") as? [String],
let semanticHints = coder.decodeObject(of: [NSArray.self, NSString.self], forKey: "semanticHints") as? [String] else {
return nil
}
self.href = href
self.rangeLocations = rangeLocations
self.rangeLengths = rangeLengths
self.breakReasons = breakReasons
self.semanticHints = semanticHints
}
///
func toCache() -> RDEPUBTextChapterPaginationCache {
let pageRanges = zip(rangeLocations, rangeLengths).map { loc, len in
NSRange(location: loc.intValue, length: len.intValue)
}
return RDEPUBTextChapterPaginationCache(
href: href,
pageRanges: pageRanges,
breakReasons: breakReasons.compactMap(RDEPUBTextPageBreakReason.init(rawValue:)),
semanticHints: semanticHints.compactMap(RDEPUBTextSemanticHint.init(rawValue:))
)
}
}
// MARK: -
/// WXRead WRChapterPageCount
///
///
/// - = SHA256(ID + + + + + schema)
/// - = NSRange + NSKeyedArchiver
/// - HTML CoreText
/// - 线 serial DispatchQueue
public final class RDEPUBTextBookCache {
///
//
public var schemaVersion: Int = 6
/// 线
private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)
///
private let cacheDirectory: URL
///
/// - Parameter subdirectory: Caches
public init(subdirectory: String = "RDEPUBTextBookCache") {
let baseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
.appendingPathComponent(subdirectory, isDirectory: true)
?? FileManager.default.temporaryDirectory.appendingPathComponent(subdirectory, isDirectory: true)
self.cacheDirectory = baseURL
try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
}
// MARK: -
// WRChapterPageCount.currentCacheKeyWithBookId
// bookID + fontSize + lineHeightMultiple + contentInsets + pageSize
/// SHA256 + ".cache"
///
///
public func cacheKey(
bookID: String,
fontSize: CGFloat,
lineHeightMultiple: CGFloat,
contentInsets: UIEdgeInsets,
pageSize: CGSize,
layoutConfigSignature: String = RDEPUBTextLayoutConfig.default.cacheSignature
) -> String {
let raw = "\(bookID)_\(fontSize)_\(lineHeightMultiple)_\(contentInsets.top)_\(contentInsets.left)_\(contentInsets.bottom)_\(contentInsets.right)_\(pageSize.width)_\(pageSize.height)_\(layoutConfigSignature)_v\(schemaVersion)"
let digest = SHA256.hash(data: Data(raw.utf8))
let hex = digest.map { String(format: "%02x", $0) }.joined()
return hex + ".cache"
}
// MARK: - /
/// href
/// nil
public func load(key: String) -> [String: RDEPUBTextChapterPaginationCache]? {
queue.sync {
let fileURL = cacheDirectory.appendingPathComponent(key)
guard FileManager.default.fileExists(atPath: fileURL.path) else {
print("[Cache] load MISS key=\(key)")
return nil
}
do {
let data = try Data(contentsOf: fileURL)
guard let archive = try NSKeyedUnarchiver.unarchivedObject(
ofClass: PaginationCacheArchive.self,
from: data
) else {
print("[Cache] load MISS key=\(key) (unarchive returned nil)")
return nil
}
var result: [String: RDEPUBTextChapterPaginationCache] = [:]
for chapter in archive.chapters {
result[chapter.href] = chapter.toCache()
}
print("[Cache] load HIT key=\(key) chapters=\(result.count)")
return result
} catch {
print("[Cache] load MISS key=\(key) error=\(error)")
return nil
}
}
}
///
public func save(_ chapters: [RDEPUBTextChapterPaginationCache], key: String) {
queue.sync {
let fileURL = cacheDirectory.appendingPathComponent(key)
do {
let archives = chapters.map { ChapterPaginationArchive(from: $0) }
let bookArchive = PaginationCacheArchive(chapters: archives)
let data = try NSKeyedArchiver.archivedData(withRootObject: bookArchive, requiringSecureCoding: true)
try data.write(to: fileURL, options: .atomic)
print("[Cache] save key=\(key) chapters=\(chapters.count)")
} catch {
print("[Cache] save FAILED key=\(key) error=\(error)")
}
}
}
// MARK: -
///
public func invalidateAll() {
queue.sync {
let fileManager = FileManager.default
guard let contents = try? fileManager.contentsOfDirectory(at: cacheDirectory, includingPropertiesForKeys: nil) else {
return
}
for file in contents {
try? fileManager.removeItem(at: file)
}
print("[Cache] invalidateAll")
}
}
}