更新阅读器功能与示例
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import Foundation
|
||||
import RDSpeechReaderView
|
||||
|
||||
/// Adapts host-supplied PDF text runs to the generic speech reader. It reads
|
||||
/// native text when available; image-only PDF OCR remains owned by the PDF
|
||||
/// reader's OCR pipeline and can be added without changing the core API.
|
||||
@MainActor
|
||||
public final class RDPDFSpeechContentProvider: RDSpeechContentProvider {
|
||||
private let pageProvider: RDPDFReaderPageProvider
|
||||
private let book: RDPDFReaderBookDescriptor
|
||||
private weak var reader: RDPDFReaderViewController?
|
||||
private var textRunsByPage: [Int: [RDPDFReaderTextRun]] = [:]
|
||||
|
||||
public init(pageProvider: RDPDFReaderPageProvider) {
|
||||
self.pageProvider = pageProvider
|
||||
book = pageProvider.readerBookDescriptor()
|
||||
}
|
||||
|
||||
init(reader: RDPDFReaderViewController) {
|
||||
self.reader = reader
|
||||
pageProvider = reader.pageProvider
|
||||
book = reader.pageProvider.readerBookDescriptor()
|
||||
}
|
||||
|
||||
public func speechBookDescriptor() -> RDSpeechBookDescriptor {
|
||||
RDSpeechBookDescriptor(identifier: book.identifier, title: book.title)
|
||||
}
|
||||
|
||||
public func speechContentBatch(
|
||||
startingAt location: RDSpeechLocation?,
|
||||
limit: Int
|
||||
) async throws -> RDSpeechContentBatch {
|
||||
let startPage = max(0, Int(location?.resourceIdentifier ?? "") ?? 0)
|
||||
let startOffset = location?.textOffset ?? 0
|
||||
var units: [RDSpeechTextUnit] = []
|
||||
|
||||
for pageIndex in startPage..<book.totalPages {
|
||||
let sourceRuns: [RDPDFReaderTextRun]
|
||||
if let reader {
|
||||
sourceRuns = await reader.speechTextRuns(at: pageIndex)
|
||||
} else {
|
||||
sourceRuns = (await loadPage(at: pageIndex)).textRuns ?? []
|
||||
}
|
||||
let runs = sourceRuns.sorted { $0.readingOrder < $1.readingOrder }
|
||||
textRunsByPage[pageIndex] = runs
|
||||
let pageText = runs
|
||||
.map(\.text)
|
||||
.joined(separator: "\n")
|
||||
let pageLocation = RDSpeechLocation(
|
||||
bookIdentifier: book.identifier,
|
||||
resourceIdentifier: String(pageIndex)
|
||||
)
|
||||
let pageUnits = RDSpeechTextPreprocessor.makeUnits(text: pageText, location: pageLocation)
|
||||
.filter { pageIndex != startPage || NSMaxRange($0.textRange) > startOffset }
|
||||
|
||||
for unit in pageUnits {
|
||||
guard units.count < limit else {
|
||||
return RDSpeechContentBatch(units: units, nextLocation: unit.location)
|
||||
}
|
||||
units.append(unit)
|
||||
}
|
||||
}
|
||||
return RDSpeechContentBatch(units: units, nextLocation: nil)
|
||||
}
|
||||
|
||||
/// Converts the current utterance range into page rectangles. A text run
|
||||
/// may span several lines, so the first release highlights whole matching
|
||||
/// runs; character-level rectangles can refine this later without changing
|
||||
/// the speech controller API.
|
||||
public func normalizedRects(for spokenRange: RDSpeechSpokenRange) -> [CGRect] {
|
||||
guard let pageIndex = Int(spokenRange.unit.location.resourceIdentifier),
|
||||
let runs = textRunsByPage[pageIndex] else {
|
||||
return []
|
||||
}
|
||||
let target = NSRange(
|
||||
location: spokenRange.unit.textRange.location + spokenRange.range.location,
|
||||
length: spokenRange.range.length
|
||||
)
|
||||
var runStart = 0
|
||||
var rects: [CGRect] = []
|
||||
for run in runs {
|
||||
let runLength = run.text.utf16.count
|
||||
let runRange = NSRange(location: runStart, length: runLength)
|
||||
if NSIntersectionRange(runRange, target).length > 0 {
|
||||
rects.append(contentsOf: run.normalizedRects)
|
||||
}
|
||||
// The text provider joins runs with a newline before tokenizing.
|
||||
runStart += runLength + 1
|
||||
}
|
||||
return rects
|
||||
}
|
||||
|
||||
private func loadPage(at index: Int) async -> RDPDFReaderPageDescriptor {
|
||||
await withCheckedContinuation { continuation in
|
||||
pageProvider.readerPage(at: index) { page in
|
||||
continuation.resume(returning: page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension RDPDFReaderViewController {
|
||||
func makeSpeechContentProvider() -> RDPDFSpeechContentProvider {
|
||||
RDPDFSpeechContentProvider(reader: self)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user