319 lines
12 KiB
Swift
319 lines
12 KiB
Swift
import UIKit
|
|
|
|
/// SDK 与主程序之间的阅读书籍摘要。下载与解析仍由主程序负责。
|
|
public struct RDPDFReaderBookDescriptor: Equatable {
|
|
public let identifier: String
|
|
public let title: String
|
|
public let totalPages: Int
|
|
|
|
public init(identifier: String, title: String, totalPages: Int) {
|
|
self.identifier = identifier
|
|
self.title = title
|
|
self.totalPages = totalPages
|
|
}
|
|
}
|
|
|
|
/// 宿主解析 PDF 后交给 SDK 的单页数据。后续链接、标注、文本区域均从这里扩展,
|
|
/// SDK 不直接触碰 PDF 文件、数据库模型或网络层。
|
|
public struct RDPDFReaderPageDescriptor {
|
|
public let index: Int
|
|
public let image: UIImage?
|
|
/// 宿主已解析的文本区域。坐标使用页面图片左上角为原点的 0...1 比例;
|
|
/// `nil` 表示宿主没有提供文本数据,SDK 可按需使用 OCR 作为兜底。
|
|
public let textRuns: [RDPDFReaderTextRun]?
|
|
|
|
public init(index: Int, image: UIImage?) {
|
|
self.index = index
|
|
self.image = image
|
|
textRuns = nil
|
|
}
|
|
|
|
public init(index: Int, image: UIImage?, textRuns: [RDPDFReaderTextRun]?) {
|
|
self.index = index
|
|
self.image = image
|
|
self.textRuns = textRuns
|
|
}
|
|
}
|
|
|
|
/// 页面中的一段可选择文本。一个 run 可以由多个不连续的矩形组成,方便宿主表达
|
|
/// 断行、双栏或 OCR 合并后的文字块。
|
|
///
|
|
/// 所有矩形均以页面图片左上角为原点,x/y/width/height 都是 0...1 的比例值。
|
|
public struct RDPDFReaderTextRun: Equatable, Codable, Sendable {
|
|
public let text: String
|
|
public let normalizedRects: [CGRect]
|
|
/// 页内阅读顺序。SDK 在相同页的文本选择中依此排序。
|
|
public let readingOrder: Int
|
|
/// 逐字符外接矩形,与 `text` 的组合字符(`Character`)序列一一对应。
|
|
/// 为 `nil` 或数量不匹配时,SDK 会按行矩形宽度均分估算每个字符的位置,
|
|
/// 仍可进行字符级选择,只是边界略有偏差。
|
|
public let characterRects: [CGRect]?
|
|
|
|
public init(text: String, normalizedRects: [CGRect], readingOrder: Int, characterRects: [CGRect]? = nil) {
|
|
self.text = text
|
|
self.normalizedRects = normalizedRects
|
|
self.readingOrder = readingOrder
|
|
self.characterRects = characterRects
|
|
}
|
|
|
|
/// 单行/单区域文本的便捷初始化方式。
|
|
public init(text: String, normalizedRect: CGRect, readingOrder: Int, characterRects: [CGRect]? = nil) {
|
|
self.init(text: text, normalizedRects: [normalizedRect], readingOrder: readingOrder, characterRects: characterRects)
|
|
}
|
|
|
|
/// 覆盖全部文本区域的最小外接矩形,便于命中测试和滚动定位。
|
|
public var normalizedRect: CGRect {
|
|
normalizedRects.reduce(into: CGRect.null) { result, rect in
|
|
result = result.union(rect)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 标注的文字来源,用于让 UI 清楚地展示“宿主文本”“OCR”或“区域标注”的能力差异。
|
|
public enum RDPDFReaderAnnotationSource: String, Codable, Equatable, Sendable {
|
|
/// 宿主直接提供的 PDF 解析文字坐标。
|
|
case text
|
|
/// SDK 从页面图片识别出的 OCR 文字坐标。
|
|
case ocr
|
|
/// 没有文字坐标时由用户框选出的图片区域。
|
|
case region
|
|
|
|
/// 标注的来源仅影响交互与展示;遇到未来版本新增的来源时,按区域标注安全降级,
|
|
/// 避免一条新记录导致整本书的历史标注都无法读取。
|
|
public init(from decoder: Decoder) throws {
|
|
let rawValue = try decoder.singleValueContainer().decode(String.self)
|
|
self = Self(rawValue: rawValue) ?? .region
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(rawValue)
|
|
}
|
|
}
|
|
|
|
/// 统一表示 PDF 的高亮和笔记。`note == nil` 时是纯高亮;存在 `note` 时是带注释的标注。
|
|
/// 坐标与 `RDPDFReaderTextRun` 相同,使用页面图片左上角为原点的 0...1 比例。
|
|
public struct RDPDFReaderAnnotation: Equatable, Codable {
|
|
public let id: String
|
|
public var pageIndex: Int
|
|
public var selectedText: String?
|
|
public var normalizedRects: [CGRect]
|
|
public var color: String
|
|
public var note: String?
|
|
public var source: RDPDFReaderAnnotationSource
|
|
public var createdAt: Date
|
|
public var updatedAt: Date
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case pageIndex
|
|
case selectedText
|
|
case normalizedRects
|
|
case color
|
|
case note
|
|
case source
|
|
case createdAt
|
|
case updatedAt
|
|
}
|
|
|
|
public init(
|
|
id: String = UUID().uuidString,
|
|
pageIndex: Int,
|
|
selectedText: String? = nil,
|
|
normalizedRects: [CGRect],
|
|
color: String = "#F8E16C",
|
|
note: String? = nil,
|
|
source: RDPDFReaderAnnotationSource = .region,
|
|
createdAt: Date = Date(),
|
|
updatedAt: Date = Date()
|
|
) {
|
|
self.id = id
|
|
self.pageIndex = pageIndex
|
|
self.selectedText = selectedText
|
|
self.normalizedRects = normalizedRects
|
|
self.color = color
|
|
self.note = note
|
|
self.source = source
|
|
self.createdAt = createdAt
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
/// 为演进中的标注文件提供向后兼容的默认值。字段缺失时保留可恢复的内容,
|
|
/// 而不会令整组历史标注都解码失败。
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
|
pageIndex = try container.decodeIfPresent(Int.self, forKey: .pageIndex) ?? 0
|
|
selectedText = try container.decodeIfPresent(String.self, forKey: .selectedText)
|
|
normalizedRects = try container.decodeIfPresent([CGRect].self, forKey: .normalizedRects) ?? []
|
|
color = try container.decodeIfPresent(String.self, forKey: .color) ?? "#F8E16C"
|
|
note = try container.decodeIfPresent(String.self, forKey: .note)
|
|
source = try container.decodeIfPresent(RDPDFReaderAnnotationSource.self, forKey: .source) ?? .region
|
|
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date()
|
|
updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) ?? createdAt
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(id, forKey: .id)
|
|
try container.encode(pageIndex, forKey: .pageIndex)
|
|
try container.encodeIfPresent(selectedText, forKey: .selectedText)
|
|
try container.encode(normalizedRects, forKey: .normalizedRects)
|
|
try container.encode(color, forKey: .color)
|
|
try container.encodeIfPresent(note, forKey: .note)
|
|
try container.encode(source, forKey: .source)
|
|
try container.encode(createdAt, forKey: .createdAt)
|
|
try container.encode(updatedAt, forKey: .updatedAt)
|
|
}
|
|
|
|
/// 单区域标注的便捷初始化方式。
|
|
public init(
|
|
id: String = UUID().uuidString,
|
|
pageIndex: Int,
|
|
selectedText: String? = nil,
|
|
normalizedRect: CGRect,
|
|
color: String = "#F8E16C",
|
|
note: String? = nil,
|
|
source: RDPDFReaderAnnotationSource = .region,
|
|
createdAt: Date = Date(),
|
|
updatedAt: Date = Date()
|
|
) {
|
|
self.init(
|
|
id: id,
|
|
pageIndex: pageIndex,
|
|
selectedText: selectedText,
|
|
normalizedRects: [normalizedRect],
|
|
color: color,
|
|
note: note,
|
|
source: source,
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt
|
|
)
|
|
}
|
|
|
|
/// 覆盖全部标注区域的最小外接矩形,适合用于页内定位。
|
|
public var normalizedRect: CGRect {
|
|
normalizedRects.reduce(into: CGRect.null) { result, rect in
|
|
result = result.union(rect)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct RDPDFReaderOutlineItem: Equatable {
|
|
public let title: String
|
|
public let pageIndex: Int
|
|
public let level: Int
|
|
|
|
public init(title: String, pageIndex: Int, level: Int = 0) {
|
|
self.title = title
|
|
self.pageIndex = pageIndex
|
|
self.level = level
|
|
}
|
|
}
|
|
|
|
public struct RDPDFReaderBookmark: Equatable {
|
|
public let pageIndex: Int
|
|
public let title: String?
|
|
|
|
public init(pageIndex: Int, title: String? = nil) {
|
|
self.pageIndex = pageIndex
|
|
self.title = title
|
|
}
|
|
}
|
|
|
|
/// 与存储实现无关的 PDF 文字标注。坐标均为相对于页面宽高的 0...1 比例。
|
|
public struct RDPDFReaderHighlight: Equatable, Codable {
|
|
public let id: Int
|
|
public let pageIndex: Int
|
|
public let selectedText: String
|
|
public let color: String
|
|
public let normalizedRect: CGRect
|
|
|
|
public init(id: Int, pageIndex: Int, selectedText: String, color: String, normalizedRect: CGRect) {
|
|
self.id = id
|
|
self.pageIndex = pageIndex
|
|
self.selectedText = selectedText
|
|
self.color = color
|
|
self.normalizedRect = normalizedRect
|
|
}
|
|
}
|
|
|
|
/// 主程序实现此协议,把“已经解析好的页面”提供给 SDK。页面和缩略图回调可在任意
|
|
/// 队列触发;SDK 会在接收结果后自行切回主线程更新界面。
|
|
public protocol RDPDFReaderPageProvider: AnyObject {
|
|
func readerBookDescriptor() -> RDPDFReaderBookDescriptor
|
|
func readerPage(at index: Int, completion: @escaping (RDPDFReaderPageDescriptor) -> Void)
|
|
func readerThumbnail(at index: Int, targetSize: CGSize, completion: @escaping (UIImage?) -> Void)
|
|
}
|
|
|
|
/// 进度、书签、笔记和画笔数据由宿主保存,SDK 只通过此协议读写。
|
|
public protocol RDPDFReaderPersistence: AnyObject {
|
|
func restoreReadingPage(for bookIdentifier: String) -> Int?
|
|
func saveReadingPage(_ pageIndex: Int, for bookIdentifier: String)
|
|
func loadBookmarks(for bookIdentifier: String) -> [RDPDFReaderBookmark]
|
|
func setBookmark(_ isBookmarked: Bool, pageIndex: Int, for bookIdentifier: String)
|
|
}
|
|
|
|
/// 可选的标注存储能力。成品阅读器仅在提供此协议时启用标注列表、创建与编辑功能;
|
|
/// 宿主可使用 SDK 的文件库,也可以接入自己的数据库实现。
|
|
public protocol RDPDFReaderAnnotationPersisting: AnyObject {
|
|
func loadAnnotations() throws -> [RDPDFReaderAnnotation]
|
|
@discardableResult func addAnnotation(_ annotation: RDPDFReaderAnnotation) throws -> RDPDFReaderAnnotation
|
|
@discardableResult func updateAnnotation(_ annotation: RDPDFReaderAnnotation) throws -> RDPDFReaderAnnotation?
|
|
@discardableResult func deleteAnnotation(id: String) throws -> Bool
|
|
}
|
|
|
|
/// 目录是可选的页面提供扩展;未实现时成品阅读器自动按页生成目录。
|
|
public protocol RDPDFReaderOutlineProviding: AnyObject {
|
|
func readerOutlineItems() -> [RDPDFReaderOutlineItem]
|
|
}
|
|
|
|
/// 可选的异步目录提供协议。适用于目录解析可能耗时的文件格式;SDK 会在加载完成后
|
|
/// 再展示导航面板,避免阻塞主线程。未实现时继续兼容 `RDPDFReaderOutlineProviding`。
|
|
public protocol RDPDFReaderAsyncOutlineProviding: AnyObject {
|
|
func readerOutlineItems(completion: @escaping ([RDPDFReaderOutlineItem]) -> Void)
|
|
}
|
|
|
|
/// 媒体、购买、外链等业务行为由宿主处理,SDK 不引用路由或播放器实现。
|
|
public enum RDPDFReaderHostAction: Equatable {
|
|
case back
|
|
case openLink(identifier: String)
|
|
case playMedia(identifier: String)
|
|
case requestPurchase
|
|
}
|
|
|
|
/// SDK 内部使用的翻页方式,宿主负责映射到具体阅读容器的实现。
|
|
public enum RDPDFReaderDisplayMode: Int {
|
|
case pageCurl
|
|
case horizontalScroll
|
|
case verticalScroll
|
|
}
|
|
|
|
/// 画笔工具的 UI 语义;实际笔迹模型和存储仍由宿主决定。
|
|
public enum RDPDFReaderDrawingTool: Int, Codable {
|
|
// 保持和既有 PDF 笔迹 JSON 的取值一致,升级后可无损读取旧数据。
|
|
case pen = 1
|
|
case highlighter = 2
|
|
case eraser = 3
|
|
}
|
|
|
|
/// 选区菜单的动作(在文本选择时显示)。
|
|
public enum RDPDFReaderAnnotationMenuAction {
|
|
case copy
|
|
case highlight
|
|
case annotate
|
|
}
|
|
|
|
/// 已有划线的菜单动作(点击已有划线时显示)。
|
|
public enum RDPDFReaderExistingHighlightMenuAction {
|
|
case copy
|
|
case createUnderline
|
|
case deleteUnderline
|
|
case annotate
|
|
case deleteAnnotation
|
|
}
|
|
|
|
public protocol RDPDFReaderHostActionHandling: AnyObject {
|
|
func readerHandle(action: RDPDFReaderHostAction)
|
|
}
|