General fixes (#102)
* Improved PDF embedded annotation hit detection logic * Updated EPUB and PDF reader logic to improve position tracking and TTS stability. Key changes: - Refined CFI and locator handling in `EpubReaderScreen` with improved initialization. - Updated `BookPaginator` to more accurately calculate text offsets in chunks and improve CFI matching logic. - Modified `TtsPlaybackManager` to insert media items at the correct index, ensuring proper playback order. - Simplified `EpubReaderTts` page scrolling by removing conditional checks for backward jumps. - Disabled OCR fallback in `PdfViewerScreen` during text extraction for TTS. * Updated extractTextWithCfiFromTop to use getBoundingClientRect for identifying the starting block
This commit is contained in:
parent
ddcd253c7b
commit
fbe8e7aa56
9 changed files with 95 additions and 184 deletions
|
|
@ -1097,62 +1097,44 @@
|
||||||
|
|
||||||
window.extractTextWithCfiFromTop = function () {
|
window.extractTextWithCfiFromTop = function () {
|
||||||
try {
|
try {
|
||||||
// 1. Find the element at the top of the viewport.
|
|
||||||
const viewportX = window.innerWidth / 2;
|
|
||||||
const viewportY = window.VIEWPORT_PADDING_TOP + 20; // A bit down from the very top edge
|
|
||||||
let topElement = document.elementFromPoint(viewportX, viewportY);
|
|
||||||
|
|
||||||
if (!topElement) {
|
|
||||||
// Fallback if nothing is found (e.g., blank space between elements)
|
|
||||||
topElement = document.body.querySelector("p, h1, h2, h3, h4, img, svg, table, li");
|
|
||||||
if (!topElement) return "[]"; // Chapter seems empty
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Find its containing block-level element that we use for TTS.
|
|
||||||
const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote";
|
const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote";
|
||||||
let startBlock = topElement.closest(ttsNodeSelector);
|
|
||||||
|
|
||||||
if (!startBlock) {
|
|
||||||
// If the element itself isn't in a TTS block, fall back to the start of the chapter.
|
|
||||||
console.log("Could not find a starting TTS block. Falling back to full chapter.");
|
|
||||||
return window.extractTextWithCfi();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Get all potential TTS nodes.
|
|
||||||
const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector));
|
const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector));
|
||||||
|
|
||||||
// 4. Find the index of our starting block.
|
let startBlock = null;
|
||||||
const startIndex = allContentNodes.findIndex((node) => node === startBlock);
|
let startIndex = -1;
|
||||||
|
|
||||||
if (startIndex === -1) {
|
for (let i = 0; i < allContentNodes.size || i < allContentNodes.length; i++) {
|
||||||
// Should be rare if startBlock was found, but as a safeguard:
|
const node = allContentNodes[i];
|
||||||
console.log("Could not find the start block in the node list. Falling back to full chapter.");
|
const rect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.bottom > (window.VIEWPORT_PADDING_TOP + 10)) {
|
||||||
|
startBlock = node;
|
||||||
|
startIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startBlock) {
|
||||||
return window.extractTextWithCfi();
|
return window.extractTextWithCfi();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Slice the array and process it.
|
|
||||||
const nodesToProcess = allContentNodes.slice(startIndex);
|
const nodesToProcess = allContentNodes.slice(startIndex);
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
nodesToProcess.forEach((node) => {
|
nodesToProcess.forEach((node) => {
|
||||||
const text = node.innerText ? node.innerText.trim() : "";
|
const text = node.innerText ? node.innerText.trim() : "";
|
||||||
|
|
||||||
if (text.length > 0 && node.offsetParent !== null) {
|
if (text.length > 0 && node.offsetParent !== null) {
|
||||||
try {
|
try {
|
||||||
const cfi = getCfiPathForElement(node, 0);
|
const cfiObj = getCfiPathForElement(node, 0);
|
||||||
|
if (cfiObj && cfiObj.cfi) {
|
||||||
if (cfi) {
|
results.push({ cfi: cfiObj, text: text });
|
||||||
results.push({ cfi: cfi, text: text });
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// ignore CFI generation errors for a single node
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return JSON.stringify(results);
|
return JSON.stringify(results);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// On any error, fall back to extracting everything to not break TTS completely.
|
|
||||||
return window.extractTextWithCfi();
|
return window.extractTextWithCfi();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -720,9 +720,7 @@ fun ChapterWebView(
|
||||||
|
|
||||||
if (!initialCfi.isNullOrBlank()) {
|
if (!initialCfi.isNullOrBlank()) {
|
||||||
val cfiJsCommand = "javascript:window.scrollToCfi('$initialCfi');"
|
val cfiJsCommand = "javascript:window.scrollToCfi('$initialCfi');"
|
||||||
Timber.d(
|
Timber.tag("POS_DIAG").d("WebView onPageFinished: Triggering scroll to initialCfi: $initialCfi")
|
||||||
"WebView onPageFinished: Executing initial scroll to CFI: $initialCfi"
|
|
||||||
)
|
|
||||||
view?.evaluateJavascript(cfiJsCommand) {
|
view?.evaluateJavascript(cfiJsCommand) {
|
||||||
onChapterInitiallyScrolled()
|
onChapterInitiallyScrolled()
|
||||||
scrollActionTaken = true
|
scrollActionTaken = true
|
||||||
|
|
|
||||||
|
|
@ -884,11 +884,15 @@ fun EpubReaderHost(
|
||||||
var chapterHead by remember(currentChapterIndex) { mutableStateOf("") }
|
var chapterHead by remember(currentChapterIndex) { mutableStateOf("") }
|
||||||
var isChapterParsing by remember(currentChapterIndex) { mutableStateOf(true) }
|
var isChapterParsing by remember(currentChapterIndex) { mutableStateOf(true) }
|
||||||
|
|
||||||
var cfiToLoad by remember { mutableStateOf<String?>(null) }
|
var cfiToLoad by remember { mutableStateOf(initialCfi) }
|
||||||
var fragmentToLoad by remember { mutableStateOf<String?>(null) }
|
var fragmentToLoad by remember { mutableStateOf<String?>(null) }
|
||||||
var isInitialCfiLoad by remember(initialLocator) { mutableStateOf(initialLocator != null) }
|
var isInitialCfiLoad by remember(initialLocator) { mutableStateOf(initialLocator != null) }
|
||||||
var bookmarkPageMap by remember { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
var bookmarkPageMap by remember { mutableStateOf<Map<String, Int>>(emptyMap()) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
Timber.tag("POS_DIAG").d("Reader Opening: initialLocator=$initialLocator, initialCfi=$initialCfi")
|
||||||
|
}
|
||||||
|
|
||||||
var initialScrollTargetForChapter by rememberSaveable(epubBook.title) {
|
var initialScrollTargetForChapter by rememberSaveable(epubBook.title) {
|
||||||
mutableStateOf(if (initialLocator != null) null else ChapterScrollPosition.START)
|
mutableStateOf(if (initialLocator != null) null else ChapterScrollPosition.START)
|
||||||
}
|
}
|
||||||
|
|
@ -1175,8 +1179,8 @@ fun EpubReaderHost(
|
||||||
var foundIdx = -1
|
var foundIdx = -1
|
||||||
for (i in chunks.indices) {
|
for (i in chunks.indices) {
|
||||||
val c = chunks[i]
|
val c = chunks[i]
|
||||||
val cPath = c.sourceCfi.substringBefore(":")
|
val cPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(c.sourceCfi)
|
||||||
val bPath = baseCfi.substringBefore(":")
|
val bPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(baseCfi)
|
||||||
if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) {
|
if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) {
|
||||||
foundIdx = i
|
foundIdx = i
|
||||||
break
|
break
|
||||||
|
|
@ -2550,10 +2554,9 @@ fun EpubReaderHost(
|
||||||
showDictionaryUpsellDialog = true
|
showDictionaryUpsellDialog = true
|
||||||
},
|
},
|
||||||
onCfiGenerated = { cfi ->
|
onCfiGenerated = { cfi ->
|
||||||
Timber.tag("PosSaveDiag").d("EpubReaderScreen: onCfiGenerated callback triggered with CFI: '$cfi'")
|
Timber.tag("POS_DIAG").d("JS generated CFI: '$cfi'")
|
||||||
|
|
||||||
if (cfi.isBlank() || !cfi.startsWith('/')) {
|
if (cfi.isBlank() || !cfi.startsWith('/')) {
|
||||||
Timber.tag("PosSaveDiag").w("EpubReaderScreen: onCfiGenerated received an invalid CFI, aborting save: '$cfi'")
|
|
||||||
if (isSavingAndExiting) {
|
if (isSavingAndExiting) {
|
||||||
isSavingAndExiting = false
|
isSavingAndExiting = false
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
|
|
@ -2562,7 +2565,6 @@ fun EpubReaderHost(
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Requesting locator conversion for chapter $latestChapterIndex")
|
|
||||||
val locator =
|
val locator =
|
||||||
locatorConverter.getLocatorFromCfi(
|
locatorConverter.getLocatorFromCfi(
|
||||||
epubBook,
|
epubBook,
|
||||||
|
|
@ -2570,8 +2572,6 @@ fun EpubReaderHost(
|
||||||
cfi
|
cfi
|
||||||
)
|
)
|
||||||
|
|
||||||
Timber.tag("PosSaveDiag").d("EpubReaderScreen: Locator conversion returned: $locator")
|
|
||||||
|
|
||||||
if (locator != null) {
|
if (locator != null) {
|
||||||
lastKnownLocator = locator
|
lastKnownLocator = locator
|
||||||
|
|
||||||
|
|
@ -2639,8 +2639,7 @@ fun EpubReaderHost(
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
Timber.d("CFI received for saving: $cfi. Progress: $progress%"
|
Timber.tag("POS_DIAG").i("Saving Position: Chapter=$latestChapterIndex, CFI=$cfi, Progress=$progress%")
|
||||||
)
|
|
||||||
onSavePosition(locator, cfi, progress)
|
onSavePosition(locator, cfi, progress)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Failed to convert CFI to Locator: $cfi."
|
Timber.w("Failed to convert CFI to Locator: $cfi."
|
||||||
|
|
|
||||||
|
|
@ -215,11 +215,8 @@ fun TtsHighlightHandler(
|
||||||
val targetPage = pag.findPageForCfiAndOffset(chapterIdx, cfi, offset)
|
val targetPage = pag.findPageForCfiAndOffset(chapterIdx, cfi, offset)
|
||||||
|
|
||||||
if (targetPage != null && targetPage != pagerState.currentPage) {
|
if (targetPage != null && targetPage != pagerState.currentPage) {
|
||||||
// Prevent backward jumps during reading (unless significant) to avoid jitter
|
scope.launch {
|
||||||
if (targetPage >= pagerState.currentPage) {
|
pagerState.animateScrollToPage(targetPage)
|
||||||
scope.launch {
|
|
||||||
pagerState.animateScrollToPage(targetPage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -352,15 +352,23 @@ class BookPaginator(
|
||||||
if (blockText.isNotBlank()) {
|
if (blockText.isNotBlank()) {
|
||||||
val textChunksInBlock = splitTextIntoChunks(blockText)
|
val textChunksInBlock = splitTextIntoChunks(blockText)
|
||||||
|
|
||||||
var currentOffsetInBlock = 0
|
var currentSearchIndex = 0
|
||||||
textChunksInBlock.forEach { chunkText ->
|
textChunksInBlock.forEach { chunkText ->
|
||||||
|
val firstWord = chunkText.trim().substringBefore(' ')
|
||||||
|
val relativeOffset = if (firstWord.isNotEmpty()) {
|
||||||
|
val idx = blockText.indexOf(firstWord, currentSearchIndex)
|
||||||
|
if (idx != -1) idx else currentSearchIndex
|
||||||
|
} else {
|
||||||
|
currentSearchIndex
|
||||||
|
}
|
||||||
|
|
||||||
val chunk = TtsChunk(
|
val chunk = TtsChunk(
|
||||||
text = chunkText,
|
text = chunkText,
|
||||||
sourceCfi = block.cfi!!,
|
sourceCfi = block.cfi!!,
|
||||||
startOffsetInSource = block.startCharOffsetInSource + currentOffsetInBlock
|
startOffsetInSource = block.startCharOffsetInSource + relativeOffset
|
||||||
)
|
)
|
||||||
allTtsChunks.add(chunk)
|
allTtsChunks.add(chunk)
|
||||||
currentOffsetInBlock += chunkText.length
|
currentSearchIndex = relativeOffset + chunkText.length
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.d("PAGINATOR: Skipping blank text block. CFI: ${block.cfi}, startOffset: ${block.startCharOffsetInSource}")
|
Timber.d("PAGINATOR: Skipping blank text block. CFI: ${block.cfi}, startOffset: ${block.startCharOffsetInSource}")
|
||||||
|
|
@ -1036,8 +1044,10 @@ class BookPaginator(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val targetPath = CfiUtils.getPath(cfi)
|
||||||
val foundRange = index.find { range ->
|
val foundRange = index.find { range ->
|
||||||
val cfiMatches = cfi.startsWith(range.cfi)
|
val rangePath = CfiUtils.getPath(range.cfi)
|
||||||
|
val cfiMatches = targetPath == rangePath || targetPath.startsWith(rangePath) || rangePath.startsWith(targetPath)
|
||||||
val offsetMatches = charOffset >= range.startOffset && charOffset < range.endOffset
|
val offsetMatches = charOffset >= range.startOffset && charOffset < range.endOffset
|
||||||
cfiMatches && offsetMatches
|
cfiMatches && offsetMatches
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,39 +51,25 @@ class LocatorConverter(
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) {
|
) {
|
||||||
private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List<SemanticBlock>? = withContext(Dispatchers.IO) {
|
private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List<SemanticBlock>? = withContext(Dispatchers.IO) {
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: STARTED for book='${book.title}', chapterIndex=$chapterIndex")
|
Timber.tag("POS_DIAG").d("processAndCacheChapter: Processing for bookId='${book.title}' index=$chapterIndex")
|
||||||
try {
|
try {
|
||||||
val chapter = book.chapters.getOrNull(chapterIndex)
|
val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null
|
||||||
if (chapter == null) {
|
|
||||||
Timber.tag("PosSaveDiag").e("processAndCacheChapter: FAILED. Chapter is null for index $chapterIndex")
|
|
||||||
return@withContext null
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Checking HTML content. RAM content length: ${chapter.htmlContent.length}")
|
val htmlToParse = chapter.htmlContent.ifBlank {
|
||||||
|
|
||||||
val htmlToParse = if (chapter.htmlContent.isNotBlank()) {
|
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Using HTML from RAM.")
|
|
||||||
chapter.htmlContent
|
|
||||||
} else {
|
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: RAM HTML is blank. Falling back to disk. Path: ${book.extractionBasePath} / ${chapter.htmlFilePath}")
|
|
||||||
try {
|
try {
|
||||||
val file = File(book.extractionBasePath, chapter.htmlFilePath)
|
val file = File(book.extractionBasePath, chapter.htmlFilePath)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
val content = file.readText()
|
val content = file.readText()
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Read file from disk SUCCESS. Content length: ${content.length}")
|
|
||||||
content
|
content
|
||||||
} else {
|
} else {
|
||||||
Timber.tag("PosSaveDiag").e("processAndCacheChapter: File DOES NOT EXIST at ${file.absolutePath}")
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: Exception reading chapter file from disk")
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (htmlToParse.isBlank()) {
|
if (htmlToParse.isBlank()) {
|
||||||
Timber.tag("PosSaveDiag").w("processAndCacheChapter: Final HTML to parse is blank. Aborting semantic block generation.")
|
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +115,6 @@ class LocatorConverter(
|
||||||
otherComplex = mergedOtherComplex
|
otherComplex = mergedOtherComplex
|
||||||
)
|
)
|
||||||
|
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Calling htmlToSemanticBlocks...")
|
|
||||||
val semanticBlocks = htmlToSemanticBlocks(
|
val semanticBlocks = htmlToSemanticBlocks(
|
||||||
html = htmlToParse,
|
html = htmlToParse,
|
||||||
cssRules = parsingCssRules,
|
cssRules = parsingCssRules,
|
||||||
|
|
@ -140,10 +125,8 @@ class LocatorConverter(
|
||||||
fontFamilyMap = emptyMap(),
|
fontFamilyMap = emptyMap(),
|
||||||
constraints = constraints
|
constraints = constraints
|
||||||
)
|
)
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: htmlToSemanticBlocks returned ${semanticBlocks.size} blocks.")
|
|
||||||
|
|
||||||
val protoBytes = proto.encodeToByteArray(semanticBlocks)
|
val protoBytes = proto.encodeToByteArray(semanticBlocks)
|
||||||
Timber.tag("PosSaveDiag").d("processAndCacheChapter: Encoded blocks to protoBytes (size: ${protoBytes.size} bytes).")
|
|
||||||
|
|
||||||
val newCacheEntry = ProcessedChapter(
|
val newCacheEntry = ProcessedChapter(
|
||||||
bookId = book.title,
|
bookId = book.title,
|
||||||
|
|
@ -152,10 +135,8 @@ class LocatorConverter(
|
||||||
estimatedPageCount = 0
|
estimatedPageCount = 0
|
||||||
)
|
)
|
||||||
bookCacheDao.insertProcessedChapters(listOf(newCacheEntry))
|
bookCacheDao.insertProcessedChapters(listOf(newCacheEntry))
|
||||||
Timber.tag("PosSaveDiag").i("processAndCacheChapter: On-demand processing and DB caching SUCCESS for chapter $chapterIndex.")
|
|
||||||
semanticBlocks
|
semanticBlocks
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: FAILED for chapter $chapterIndex")
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +153,10 @@ class LocatorConverter(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allBlocks.isNullOrEmpty()) {
|
if (allBlocks.isNullOrEmpty()) {
|
||||||
Timber.tag("PosSaveDiag").w("getLocatorFromCfi: Cache missing or empty for chapter $chapterIndex. Triggering on-demand processing.")
|
|
||||||
allBlocks = processAndCacheChapter(book, chapterIndex)
|
allBlocks = processAndCacheChapter(book, chapterIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allBlocks.isNullOrEmpty()) {
|
if (allBlocks.isNullOrEmpty()) {
|
||||||
Timber.tag("PosSaveDiag").e("getLocatorFromCfi: FAILED. Could not get or process semantic blocks.")
|
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ import kotlin.math.PI
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
@ -919,20 +920,27 @@ internal fun PdfPageComposable(
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
val allAnnots = (0 until count).mapNotNull { i ->
|
val allAnnots = (0 until count).mapNotNull { i ->
|
||||||
val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i)
|
val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i)
|
||||||
if (subtype == annotLink) return@mapNotNull null // skip links here
|
if (subtype == annotLink) return@mapNotNull null
|
||||||
|
|
||||||
|
var contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents")
|
||||||
|
if (contents.isNullOrBlank()) {
|
||||||
|
contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "RC")
|
||||||
|
}
|
||||||
|
|
||||||
val contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents")
|
|
||||||
val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM")
|
val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM")
|
||||||
val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT")
|
val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT")
|
||||||
val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T")
|
val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T")
|
||||||
|
|
||||||
val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i)
|
val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i)
|
||||||
val pdfRectF = if (pdfRectArray != null) {
|
val pdfRectF = if (pdfRectArray != null) {
|
||||||
android.graphics.RectF(pdfRectArray[0], pdfRectArray[3], pdfRectArray[2], pdfRectArray[1])
|
android.graphics.RectF(
|
||||||
|
min(pdfRectArray[0], pdfRectArray[2]),
|
||||||
|
max(pdfRectArray[1], pdfRectArray[3]),
|
||||||
|
max(pdfRectArray[0], pdfRectArray[2]),
|
||||||
|
min(pdfRectArray[1], pdfRectArray[3])
|
||||||
|
)
|
||||||
} else android.graphics.RectF()
|
} else android.graphics.RectF()
|
||||||
|
|
||||||
Timber.tag("PdfCommentDebug").v("Extracted Annot[$i]: Name=$name, IRT=$irt, Subtype=$subtype, Text=${contents?.take(10)}...")
|
|
||||||
|
|
||||||
EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt)
|
EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2405,6 +2413,8 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
detectTapGestures(onTap = { tapOffset ->
|
detectTapGestures(onTap = { tapOffset ->
|
||||||
val tapInContentCoords = screenToContentCoordinates(tapOffset)
|
val tapInContentCoords = screenToContentCoordinates(tapOffset)
|
||||||
|
val tapXInBitmap = tapInContentCoords.x
|
||||||
|
val tapYInBitmap = tapInContentCoords.y
|
||||||
|
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val wasHandled = withContext(Dispatchers.IO) {
|
val wasHandled = withContext(Dispatchers.IO) {
|
||||||
|
|
@ -2439,9 +2449,6 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val tapXInBitmap = tapInContentCoords.x
|
|
||||||
val tapYInBitmap = tapInContentCoords.y
|
|
||||||
|
|
||||||
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
|
val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale
|
||||||
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
|
val hitTolerance = with(density) { 16.dp.toPx() } / inputScale
|
||||||
|
|
||||||
|
|
@ -2466,14 +2473,24 @@ internal fun PdfPageComposable(
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
val standardHit = standardAnnotScreenRects.findLast { (_, screenRect) ->
|
val standardHit = standardAnnotScreenRects.findLast { (annot, screenRect) ->
|
||||||
|
if (annot.subtype == 2) return@findLast false
|
||||||
|
|
||||||
|
val left = min(screenRect.left, screenRect.right)
|
||||||
|
val right = max(screenRect.left, screenRect.right)
|
||||||
|
val top = min(screenRect.top, screenRect.bottom)
|
||||||
|
val bottom = max(screenRect.top, screenRect.bottom)
|
||||||
|
|
||||||
val inflatedHitBox = Rect(
|
val inflatedHitBox = Rect(
|
||||||
(screenRect.left - annotHitTolerance).toInt(),
|
(left - annotHitTolerance).toInt(),
|
||||||
(screenRect.top - annotHitTolerance).toInt(),
|
(top - annotHitTolerance).toInt(),
|
||||||
(screenRect.right + annotHitTolerance).toInt(),
|
(right + annotHitTolerance).toInt(),
|
||||||
(screenRect.bottom + annotHitTolerance).toInt()
|
(bottom + annotHitTolerance).toInt()
|
||||||
)
|
)
|
||||||
inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
|
|
||||||
|
val isHit = inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt())
|
||||||
|
|
||||||
|
isHit
|
||||||
}
|
}
|
||||||
|
|
||||||
if (standardHit != null) {
|
if (standardHit != null) {
|
||||||
|
|
|
||||||
|
|
@ -2792,93 +2792,14 @@ fun PdfViewerScreen(
|
||||||
withContext(Dispatchers.IO) { tempTextPage?.close() }
|
withContext(Dispatchers.IO) { tempTextPage?.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ocrUsedForCurrentPageTts = false
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
tempPage?.close()
|
||||||
|
}
|
||||||
if (rawPageText.isNullOrBlank()) {
|
if (rawPageText.isNullOrBlank()) {
|
||||||
Timber.i(
|
Timber.i("TTS: Pdfium text is blank or extraction failed. OCR fallback is temporarily disabled.")
|
||||||
"TTS: Pdfium text is blank or extraction failed. Attempting OCR for page $pageToRead."
|
|
||||||
)
|
|
||||||
ocrAttempted = true
|
|
||||||
ocrUsedForCurrentPageTts = true
|
|
||||||
var ocrBitmap: Bitmap? = null
|
|
||||||
try {
|
|
||||||
if (tempPage == null) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tempPage?.close()
|
|
||||||
Timber.d("TTS/OCR: Re-opening page $pageToRead for bitmap rendering.")
|
|
||||||
tempPage = pdfDocument!!.openPage(pageToRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val pageForOcr = tempPage ?: throw IllegalStateException(
|
|
||||||
"TTS/OCR: PDF page couldn't be opened for OCR."
|
|
||||||
)
|
|
||||||
|
|
||||||
val ocrBitmapWidth = 1080
|
|
||||||
val ocrBitmapHeight: Int
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val originalWidthPoints = pageForOcr.getPageWidthPoint()
|
|
||||||
val originalHeightPoints = pageForOcr.getPageHeightPoint()
|
|
||||||
|
|
||||||
if (originalWidthPoints <= 0 || originalHeightPoints <= 0) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"TTS/OCR: Invalid page dimensions (points) from Pdfium for page $pageToRead."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val aspectRatio =
|
|
||||||
originalWidthPoints.toFloat() / originalHeightPoints.toFloat()
|
|
||||||
ocrBitmapHeight = (ocrBitmapWidth / aspectRatio).toInt()
|
|
||||||
|
|
||||||
if (ocrBitmapHeight <= 0) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"TTS/OCR: Calculated invalid bitmap dimensions for OCR ($ocrBitmapWidth x $ocrBitmapHeight) for page $pageToRead."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d(
|
|
||||||
"TTS/OCR: Rendering page $pageToRead to bitmap of size ${ocrBitmapWidth}x$ocrBitmapHeight."
|
|
||||||
)
|
|
||||||
ocrBitmap = createBitmap(ocrBitmapWidth, ocrBitmapHeight)
|
|
||||||
pageForOcr.renderPageBitmap(
|
|
||||||
bitmap = ocrBitmap,
|
|
||||||
startX = 0,
|
|
||||||
startY = 0,
|
|
||||||
drawSizeX = ocrBitmapWidth,
|
|
||||||
drawSizeY = ocrBitmapHeight,
|
|
||||||
renderAnnot = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Timber.d("TTS/OCR: Bitmap rendered for page $pageToRead. Attempting OCR.")
|
|
||||||
|
|
||||||
rawPageText = OcrHelper.extractTextFromBitmap(ocrBitmap!!) {
|
|
||||||
isOcrModelDownloading = true
|
|
||||||
}?.text
|
|
||||||
|
|
||||||
if (!rawPageText.isNullOrBlank()) {
|
|
||||||
Timber.i(
|
|
||||||
"TTS: Text extracted via OCR for page $pageToRead (length: ${rawPageText?.length})."
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Timber.w(
|
|
||||||
"TTS: OCR process completed for page $pageToRead but returned no text or blank text."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "TTS: Error during OCR process for page $pageToRead")
|
|
||||||
} finally {
|
|
||||||
ocrBitmap?.recycle()
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tempPage?.close()
|
|
||||||
Timber.d("TTS/OCR: Closed page $pageToRead after OCR attempt.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ocrUsedForCurrentPageTts = false
|
Timber.d("TTS: Closed page $pageToRead after successful Pdfium text extraction.")
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tempPage?.close()
|
|
||||||
Timber.d(
|
|
||||||
"TTS: Closed page $pageToRead after successful Pdfium text extraction."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawPageText != null && rawPageText!!.isNotBlank()) {
|
if (rawPageText != null && rawPageText!!.isNotBlank()) {
|
||||||
|
|
|
||||||
|
|
@ -473,7 +473,15 @@ class TtsPlaybackManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
player.addMediaItem(nextMediaItem)
|
var insertPosition = player.mediaItemCount
|
||||||
|
for (k in 0 until player.mediaItemCount) {
|
||||||
|
val id = player.getMediaItemAt(k).mediaId.toIntOrNull() ?: -1
|
||||||
|
if (id > targetIndex) {
|
||||||
|
insertPosition = k
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.addMediaItem(insertPosition, nextMediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.playbackState == Player.STATE_ENDED && player.playWhenReady && targetIndex == player.currentMediaItemIndex + 1) {
|
if (player.playbackState == Player.STATE_ENDED && player.playWhenReady && targetIndex == player.currentMediaItemIndex + 1) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue