import CoreText import ImageIO import SwiftUI import UIKit private struct ReaderPositionPreferenceKey: PreferenceKey { static let defaultValue: [Int: CGFloat] = [:] static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) { value.merge(nextValue(), uniquingKeysWith: { _, new in new }) } } struct ReaderStylePreset: Identifiable { let id: String let name: String let background: UIColor let foreground: UIColor static let legadoDefaults: [ReaderStylePreset] = [ .init(id: "wechat", name: "微信读书", background: UIColor(hex: 0xC0EDC6), foreground: UIColor(hex: 0x0B0B0B)), .init(id: "preset1", name: "预设1", background: UIColor(hex: 0xFFFFFF), foreground: UIColor(hex: 0x000000)), .init(id: "preset2", name: "预设2", background: UIColor(hex: 0xDDC090), foreground: UIColor(hex: 0x3E3422)), .init(id: "preset3", name: "预设3", background: UIColor(hex: 0xC2D8AA), foreground: UIColor(hex: 0x596C44)), .init(id: "preset4", name: "预设4", background: UIColor(hex: 0xDBB8E2), foreground: UIColor(hex: 0x68516C)), .init(id: "preset5", name: "预设5", background: UIColor(hex: 0xABCEE0), foreground: UIColor(hex: 0x3D4C54)), .init(id: "night", name: "夜间", background: UIColor(hex: 0x000000), foreground: UIColor(hex: 0xADADAD)) ] static let eInk = ReaderStylePreset( id: "eink", name: "E-Ink", background: UIColor(hex: 0xFFFFFF), foreground: UIColor(hex: 0x000000) ) static func resolve(_ id: String) -> ReaderStylePreset { let migratedID = switch id { case "dark": "night" case "light": "preset1" case "paper": "preset2" default: id } return legadoDefaults.first(where: { $0.id == migratedID }) ?? legadoDefaults[0] } } private extension UIColor { convenience init(hex: UInt32) { self.init( red: CGFloat((hex >> 16) & 0xff) / 255, green: CGFloat((hex >> 8) & 0xff) / 255, blue: CGFloat(hex & 0xff) / 255, alpha: 1 ) } } enum ReaderBackgroundStore { static let maximumPixelSize = 2560 static var url: URL { FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("Legado/reader-background.jpg") } static func load(from source: URL = url) -> UIImage? { guard let data = try? Data(contentsOf: source, options: .mappedIfSafe) else { return nil } return try? downsample(data) } static func save(_ data: Data, to target: URL = url) throws -> UIImage { let image = try downsample(data) guard let encoded = image.jpegData(compressionQuality: 0.9) else { throw CocoaError(.fileReadCorruptFile) } try FileManager.default.createDirectory(at: target.deletingLastPathComponent(), withIntermediateDirectories: true) try encoded.write(to: target, options: .atomic) return image } private static func downsample(_ data: Data) throws -> UIImage { // Decode only the size used by the reader, including backgrounds saved // by older versions. Full-resolution camera photos can exhaust memory. guard let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary), let image = CGImageSourceCreateThumbnailAtIndex(source, 0, [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceThumbnailMaxPixelSize: maximumPixelSize ] as CFDictionary) else { throw CocoaError(.fileReadCorruptFile) } return UIImage(cgImage: image) } static func remove() { try? FileManager.default.removeItem(at: url) } } enum ReaderFontStore { static var directory: URL { FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("Legado/fonts", isDirectory: true) } static func install(from source: URL) throws -> String { let accessing = source.startAccessingSecurityScopedResource() defer { if accessing { source.stopAccessingSecurityScopedResource() } } try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let target = directory.appendingPathComponent(source.lastPathComponent) if target != source { try? FileManager.default.removeItem(at: target) try FileManager.default.copyItem(at: source, to: target) } guard font(at: target.path, size: 20, weight: 0) != nil else { throw CocoaError(.fileReadCorruptFile) } return target.path } static func font(at path: String, size: CGFloat, weight: Int) -> UIFont? { guard !path.isEmpty, let provider = CGDataProvider(url: URL(fileURLWithPath: path) as CFURL), let graphicsFont = CGFont(provider) else { return nil } CTFontManagerRegisterGraphicsFont(graphicsFont, nil) guard let postScriptName = graphicsFont.postScriptName else { return nil } let name = postScriptName as String guard let base = UIFont(name: name, size: size) else { return nil } if weight == 1, let descriptor = base.fontDescriptor.withSymbolicTraits(.traitBold) { return UIFont(descriptor: descriptor, size: size) } if weight == 2 { let descriptor = base.fontDescriptor.addingAttributes([ .traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue] ]) return UIFont(descriptor: descriptor, size: size) } return base } }