v1.0.45-oss (#221)
* Implemented ML-based comic panel detection and a panel popup viewer. * Optimized `ComicPanelDetector` for performance and memory efficiency. * Optimized comic panel detection performance by introducing a dedicated single-thread dispatcher for ML tasks, replacing mutex-based synchronization. * Refactored system UI handling and layout padding in `PdfViewerScreen`. * Refactor `PdfViewerScreen.kt` by extracting components and logic into specialized files. * replace hardcoded padding with dynamic header height and adjust IME layout logic * Improve `PdfTextBox` interaction and visual consistency during zoom and pan. * improve PDF text box dragging and scaling behavior across zoom levels * optimize color scheme calculation using remember and expand text dimming coverage * Implement in-app language selection and per-app language preferences. * Improve PDF lock stability in `PdfVerticalReader` * Implement "Preserve Image Colors" option for PDF themes * Optimize TOC locate in ReaderDrawer * Update library and home screen UI components * smoother page turn animation in epub pagination mode * Implement automatic page skipping for TTS when no text is found in PDF viewer * Refine status bar handling and window insets across main screens * Optimize ViewModel initialization and integrate AndroidX SplashScreen * Move ComicPanelDetector to debug source set and introduce IPanelDetector interface * Optimize TOC scrolling in PdfNavigationDrawerContent * Bump version to 1.0.45(45)
This commit is contained in:
parent
71e3614ad6
commit
cb7de86224
33 changed files with 4795 additions and 3447 deletions
|
|
@ -347,7 +347,9 @@ data class PageStaticData(
|
|||
val targetWidth: Int,
|
||||
val targetHeight: Int,
|
||||
val colorFilter: StableHolder<ColorFilter?>,
|
||||
val isDarkMode: Boolean
|
||||
val isDarkMode: Boolean,
|
||||
val excludeImages: Boolean,
|
||||
val imageRects: StableHolder<List<android.graphics.Rect>>
|
||||
)
|
||||
|
||||
@Stable
|
||||
|
|
@ -416,6 +418,7 @@ internal fun PdfPageComposable(
|
|||
onTtsHighlightCenterCalculated: ((Float) -> Unit)? = null,
|
||||
onSearchHighlightCenterCalculated: ((Float) -> Unit)? = null,
|
||||
activeTheme: com.aryan.reader.ReaderTheme = com.aryan.reader.ReaderTheme("no_theme", "No Theme", Color.Unspecified, Color.Unspecified, false),
|
||||
excludeImages: Boolean = false,
|
||||
onDoubleTap: ((Offset) -> Unit)? = null,
|
||||
isEditMode: Boolean = false,
|
||||
drawingState: PdfDrawingState? = null,
|
||||
|
|
@ -451,7 +454,9 @@ internal fun PdfPageComposable(
|
|||
customHighlightColors: Map<PdfHighlightColor, Color> = emptyMap(),
|
||||
onPaletteClick: (() -> Unit)? = null,
|
||||
lockedState: Triple<Float, Float, Float>? = null,
|
||||
onZoomAndPanChanged: ((Float, Offset) -> Unit)? = null
|
||||
onZoomAndPanChanged: ((Float, Offset) -> Unit)? = null,
|
||||
onDetectPanels: suspend (Bitmap) -> List<android.graphics.RectF> = { emptyList() },
|
||||
onShowPanelPopup: (Bitmap) -> Unit = {}
|
||||
) {
|
||||
val pdfDocumentItem = pdfDocument.item
|
||||
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
||||
|
|
@ -852,11 +857,13 @@ internal fun PdfPageComposable(
|
|||
|
||||
@Suppress("VariableNeverRead") var embeddedAnnotations by remember { mutableStateOf<List<EmbeddedAnnotation>>(emptyList()) }
|
||||
var standardAnnotScreenRects by remember { mutableStateOf<List<Pair<EmbeddedAnnotation, Rect>>>(emptyList()) }
|
||||
var imageScreenRects by remember { mutableStateOf<List<android.graphics.Rect>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(pageIndex, pdfDocumentItem, actualBitmapWidthPx, actualBitmapHeightPx, virtualPage) {
|
||||
if (!isPdfPage || actualBitmapWidthPx == 0 || actualBitmapHeightPx == 0) {
|
||||
if (pageLinks.isNotEmpty()) pageLinks = emptyList()
|
||||
if (standardAnnotScreenRects.isNotEmpty()) standardAnnotScreenRects = emptyList()
|
||||
if (imageScreenRects.isNotEmpty()) imageScreenRects = emptyList()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
|
|
@ -864,6 +871,7 @@ internal fun PdfPageComposable(
|
|||
val allLinks = mutableListOf<PageLink>()
|
||||
var finalDisplayList = emptyList<EmbeddedAnnotation>()
|
||||
var mappedAnnots = emptyList<Pair<EmbeddedAnnotation, Rect>>()
|
||||
var mappedImageRects = emptyList<android.graphics.Rect>()
|
||||
val annotLink = 2
|
||||
|
||||
try {
|
||||
|
|
@ -931,6 +939,39 @@ internal fun PdfPageComposable(
|
|||
Timber.e(e, "Error fetching web links")
|
||||
}
|
||||
|
||||
// --- Extract Image Bounds ---
|
||||
try {
|
||||
val pagePtr = pageWrapper.getNativePointer()
|
||||
if (pagePtr != 0L) {
|
||||
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
|
||||
val imgRects = mutableListOf<android.graphics.Rect>()
|
||||
val outRect = FloatArray(4)
|
||||
|
||||
for (i in 0 until objCount) {
|
||||
if (NativePdfiumBridge.getPageObjectType(pagePtr, i) == 3) { // 3 = FPDF_PAGEOBJ_IMAGE
|
||||
if (NativePdfiumBridge.getPageObjectBoundingBox(pagePtr, i, outRect)) {
|
||||
val pdfRectF = android.graphics.RectF(
|
||||
min(outRect[0], outRect[2]),
|
||||
max(outRect[1], outRect[3]),
|
||||
max(outRect[0], outRect[2]),
|
||||
min(outRect[1], outRect[3])
|
||||
)
|
||||
val deviceRect = pageWrapper.mapRectToDevice(
|
||||
0, 0, actualBitmapWidthPx, actualBitmapHeightPx,
|
||||
currentPageRotation, pdfRectF
|
||||
)
|
||||
if (deviceRect.width() > 0 && deviceRect.height() > 0) {
|
||||
imgRects.add(android.graphics.Rect(deviceRect.left, deviceRect.top, deviceRect.right, deviceRect.bottom))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mappedImageRects = imgRects
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("PdfImageDebug").e(e, "Error extracting image rects")
|
||||
}
|
||||
|
||||
// 3. Extract Embedded Annotations
|
||||
try {
|
||||
val pagePtr = pageWrapper.getNativePointer()
|
||||
|
|
@ -1035,6 +1076,7 @@ internal fun PdfPageComposable(
|
|||
pageLinks = allLinks
|
||||
embeddedAnnotations = finalDisplayList
|
||||
standardAnnotScreenRects = mappedAnnots
|
||||
imageScreenRects = mappedImageRects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2627,7 +2669,38 @@ internal fun PdfPageComposable(
|
|||
coroutineScope.launch {
|
||||
val startScale = scale
|
||||
val targetScale = if (startScale > 1.1f) 1f else 2.5f
|
||||
Timber.tag("PdfZoomDebug").i("DoubleTap Triggered: CurrentScale=$startScale, Target=$targetScale")
|
||||
|
||||
if (com.aryan.reader.BuildConfig.DEBUG && startScale <= 1.1f && bitmapState != null) {
|
||||
val tapInContentCoords = screenToContentCoordinates(tapOffset)
|
||||
|
||||
val ratioX = bitmapState!!.width.toFloat() / actualBitmapWidthPx.toFloat()
|
||||
val ratioY = bitmapState!!.height.toFloat() / actualBitmapHeightPx.toFloat()
|
||||
val tapXInBitmap = tapInContentCoords.x * ratioX
|
||||
val tapYInBitmap = tapInContentCoords.y * ratioY
|
||||
|
||||
val panels = onDetectPanels(bitmapState!!)
|
||||
|
||||
val tappedPanel = panels.firstOrNull {
|
||||
it.contains(tapXInBitmap, tapYInBitmap)
|
||||
}
|
||||
|
||||
if (tappedPanel != null) {
|
||||
Timber.d("Popup: Cropping panel $tappedPanel")
|
||||
val left = tappedPanel.left.coerceAtLeast(0f).toInt()
|
||||
val top = tappedPanel.top.coerceAtLeast(0f).toInt()
|
||||
val right = tappedPanel.right.coerceAtMost(bitmapState!!.width.toFloat()).toInt()
|
||||
val bottom = tappedPanel.bottom.coerceAtMost(bitmapState!!.height.toFloat()).toInt()
|
||||
val width = right - left
|
||||
val height = bottom - top
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
val cropped = android.graphics.Bitmap.createBitmap(bitmapState!!, left, top, width, height)
|
||||
onShowPanelPopup(cropped)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val startOffset = offset
|
||||
val targetOffsetUnbounded = if (targetScale <= 1.1f) {
|
||||
Offset.Zero
|
||||
|
|
@ -2658,10 +2731,10 @@ internal fun PdfPageComposable(
|
|||
)
|
||||
) {
|
||||
val progress = value
|
||||
scale = lerp(
|
||||
scale = androidx.compose.ui.util.lerp(
|
||||
startScale, targetScale, progress
|
||||
)
|
||||
offset = lerp(
|
||||
offset = androidx.compose.ui.geometry.lerp(
|
||||
startOffset, targetOffset, progress
|
||||
)
|
||||
onScaleChanged(scale)
|
||||
|
|
@ -3555,6 +3628,7 @@ internal fun PdfPageComposable(
|
|||
val stableBitmapState = remember(bitmapState) { StableHolder(bitmapState) }
|
||||
val stableTiles = remember(tiles) { StableHolder(tiles) }
|
||||
val stableColorFilter = remember(colorFilter) { StableHolder(colorFilter) }
|
||||
val stableImageRects = remember(imageScreenRects) { StableHolder(imageScreenRects) }
|
||||
|
||||
val staticData = remember(
|
||||
stableBitmapState,
|
||||
|
|
@ -3567,7 +3641,9 @@ internal fun PdfPageComposable(
|
|||
actualBitmapWidthPx,
|
||||
actualBitmapHeightPx,
|
||||
stableColorFilter,
|
||||
isDarkMode
|
||||
isDarkMode,
|
||||
excludeImages,
|
||||
stableImageRects
|
||||
) {
|
||||
Timber.tag("PdfDrawPerf").v(
|
||||
"STATIC DATA GENERATED: Scale=$effectiveScale, Tiles=${stableTiles.item.size}"
|
||||
|
|
@ -3583,7 +3659,9 @@ internal fun PdfPageComposable(
|
|||
targetWidth = actualBitmapWidthPx,
|
||||
targetHeight = actualBitmapHeightPx,
|
||||
colorFilter = stableColorFilter,
|
||||
isDarkMode = isDarkMode
|
||||
isDarkMode = isDarkMode,
|
||||
excludeImages = excludeImages,
|
||||
imageRects = stableImageRects
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3659,6 +3737,7 @@ internal fun PdfPageComposable(
|
|||
}
|
||||
},
|
||||
scale = scale,
|
||||
uiScale = effectiveScale,
|
||||
offset = offset,
|
||||
startHandlePos = startHandleContentPosition.value,
|
||||
endHandlePos = endHandleContentPosition.value,
|
||||
|
|
@ -3936,11 +4015,11 @@ private fun PdfBitmapLayer(
|
|||
targetWidth: Int,
|
||||
targetHeight: Int,
|
||||
colorFilter: ColorFilter? = null,
|
||||
isDarkMode: Boolean = false
|
||||
isDarkMode: Boolean = false,
|
||||
excludeImages: Boolean = false,
|
||||
imageRects: List<android.graphics.Rect> = emptyList()
|
||||
) {
|
||||
Canvas(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer()) {
|
||||
Canvas(modifier = Modifier.fillMaxSize().graphicsLayer()) {
|
||||
translate(left = centeringOffsetX, top = centeringOffsetY) {
|
||||
clipRect(left = 0f, top = 0f, right = targetWidth.toFloat(), bottom = targetHeight.toFloat()) {
|
||||
if (bitmapState != null && !bitmapState.isRecycled) {
|
||||
|
|
@ -3959,6 +4038,32 @@ private fun PdfBitmapLayer(
|
|||
filterQuality = androidx.compose.ui.graphics.FilterQuality.High
|
||||
)
|
||||
|
||||
if (excludeImages && colorFilter != null && imageRects.isNotEmpty()) {
|
||||
imageRects.forEach { rect ->
|
||||
val scaleX = bitmapState.width.toFloat() / dstW.toFloat()
|
||||
val scaleY = bitmapState.height.toFloat() / dstH.toFloat()
|
||||
|
||||
val srcRectLeft = (rect.left * scaleX).roundToInt().coerceAtLeast(0)
|
||||
val srcRectTop = (rect.top * scaleY).roundToInt().coerceAtLeast(0)
|
||||
val srcRectRight = (rect.right * scaleX).roundToInt().coerceAtMost(bitmapState.width)
|
||||
val srcRectBottom = (rect.bottom * scaleY).roundToInt().coerceAtMost(bitmapState.height)
|
||||
|
||||
val w = srcRectRight - srcRectLeft
|
||||
val h = srcRectBottom - srcRectTop
|
||||
if (w > 0 && h > 0) {
|
||||
drawImage(
|
||||
image = bitmapState.asImageBitmap(),
|
||||
srcOffset = IntOffset(srcRectLeft, srcRectTop),
|
||||
srcSize = IntSize(w, h),
|
||||
dstOffset = IntOffset(rect.left, rect.top),
|
||||
dstSize = IntSize(rect.width(), rect.height()),
|
||||
colorFilter = null,
|
||||
filterQuality = androidx.compose.ui.graphics.FilterQuality.High
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val needsTiling = effectiveScale > 1f || targetWidth > 3000 || targetHeight > 3000
|
||||
if (needsTiling) {
|
||||
tiles.forEach { tile ->
|
||||
|
|
@ -3968,12 +4073,52 @@ private fun PdfBitmapLayer(
|
|||
srcOffset = IntOffset.Zero,
|
||||
srcSize = IntSize(tile.bitmap.width, tile.bitmap.height),
|
||||
dstOffset = IntOffset(tile.renderRect.left, tile.renderRect.top),
|
||||
dstSize = IntSize(
|
||||
tile.renderRect.width(), tile.renderRect.height()
|
||||
),
|
||||
dstSize = IntSize(tile.renderRect.width(), tile.renderRect.height()),
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = androidx.compose.ui.graphics.FilterQuality.High
|
||||
)
|
||||
|
||||
if (excludeImages && colorFilter != null && imageRects.isNotEmpty()) {
|
||||
imageRects.forEach { imgRect ->
|
||||
val scaledImgRectLeft = (imgRect.left * effectiveScale).roundToInt()
|
||||
val scaledImgRectTop = (imgRect.top * effectiveScale).roundToInt()
|
||||
val scaledImgRectRight = (imgRect.right * effectiveScale).roundToInt()
|
||||
val scaledImgRectBottom = (imgRect.bottom * effectiveScale).roundToInt()
|
||||
|
||||
val intersectLeft = max(scaledImgRectLeft, tile.renderRect.left)
|
||||
val intersectTop = max(scaledImgRectTop, tile.renderRect.top)
|
||||
val intersectRight = min(scaledImgRectRight, tile.renderRect.right)
|
||||
val intersectBottom = min(scaledImgRectBottom, tile.renderRect.bottom)
|
||||
|
||||
val iw = intersectRight - intersectLeft
|
||||
val ih = intersectBottom - intersectTop
|
||||
|
||||
if (iw > 0 && ih > 0) {
|
||||
val scaleXBmp = tile.bitmap.width.toFloat() / tile.renderRect.width()
|
||||
val scaleYBmp = tile.bitmap.height.toFloat() / tile.renderRect.height()
|
||||
|
||||
val srcLeft = ((intersectLeft - tile.renderRect.left) * scaleXBmp).roundToInt()
|
||||
val srcTop = ((intersectTop - tile.renderRect.top) * scaleYBmp).roundToInt()
|
||||
val srcRight = ((intersectRight - tile.renderRect.left) * scaleXBmp).roundToInt()
|
||||
val srcBottom = ((intersectBottom - tile.renderRect.top) * scaleYBmp).roundToInt()
|
||||
|
||||
val srcW = srcRight - srcLeft
|
||||
val srcH = srcBottom - srcTop
|
||||
|
||||
if (srcW > 0 && srcH > 0) {
|
||||
drawImage(
|
||||
image = tile.bitmap.asImageBitmap(),
|
||||
srcOffset = IntOffset(srcLeft, srcTop),
|
||||
srcSize = IntSize(srcW, srcH),
|
||||
dstOffset = IntOffset(intersectLeft, intersectTop),
|
||||
dstSize = IntSize(iw, ih),
|
||||
colorFilter = null,
|
||||
filterQuality = androidx.compose.ui.graphics.FilterQuality.High
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4497,7 +4642,9 @@ private fun PdfPageStaticLayer(data: PageStaticData) {
|
|||
targetWidth = data.targetWidth,
|
||||
targetHeight = data.targetHeight,
|
||||
colorFilter = data.colorFilter.item,
|
||||
isDarkMode = data.isDarkMode
|
||||
isDarkMode = data.isDarkMode,
|
||||
excludeImages = data.excludeImages,
|
||||
imageRects = data.imageRects.item
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -4571,6 +4718,7 @@ private fun PdfPageRenderer(
|
|||
drawingState: PdfDrawingState?,
|
||||
onCanvasSizeChanged: (Float, Float) -> Unit,
|
||||
scale: Float,
|
||||
uiScale: Float,
|
||||
offset: Offset,
|
||||
startHandlePos: Offset?,
|
||||
endHandlePos: Offset?,
|
||||
|
|
@ -4697,6 +4845,12 @@ private fun PdfPageRenderer(
|
|||
}
|
||||
}
|
||||
|
||||
if (textBoxes.isNotEmpty()) {
|
||||
androidx.compose.runtime.SideEffect {
|
||||
Timber.tag("PdfTextBoxDebug").d("PdfPageRenderer parent graphicsLayer applied | scale=$scale | offset=$offset | Centering: X=${staticData.centeringOffsetX}, Y=${staticData.centeringOffsetY}")
|
||||
}
|
||||
}
|
||||
|
||||
textBoxes.forEach { box ->
|
||||
val isDraggingThisBox = (box.id == draggingBoxId)
|
||||
val boxAlpha = if (isDraggingThisBox) 0f else 1f
|
||||
|
|
@ -4707,17 +4861,27 @@ private fun PdfPageRenderer(
|
|||
isSelected = (box.id == selectedTextBoxId),
|
||||
isEditMode = isEditMode,
|
||||
isDarkMode = staticData.isDarkMode,
|
||||
scale = uiScale,
|
||||
pageWidthPx = staticData.targetWidth.toFloat(),
|
||||
pageHeightPx = staticData.targetHeight.toFloat(),
|
||||
handlePosition = HandlePosition.AUTO,
|
||||
onBoundsChanged = { newBounds ->
|
||||
onTextBoxChange(box.copy(relativeBounds = newBounds))
|
||||
Timber.tag("PdfTextBoxDebug").v("PdfPageRenderer onBoundsChanged [ID: ${box.id}] bounds=$newBounds draggingBoxId=$draggingBoxId")
|
||||
if (draggingBoxId != box.id) {
|
||||
onTextBoxChange(box.copy(relativeBounds = newBounds))
|
||||
} else {
|
||||
Timber.tag("PdfTextBoxDebug").d("PdfPageRenderer onBoundsChanged IGNORED because box[ID: ${box.id}] is being dragged globally")
|
||||
}
|
||||
},
|
||||
onTextChanged = { newText ->
|
||||
onTextBoxChange(box.copy(text = newText))
|
||||
},
|
||||
onSelect = { onTextBoxSelect(box.id) },
|
||||
onSelect = {
|
||||
Timber.tag("PdfTextBoxDebug").d("PdfPageRenderer onSelect propagated[ID: ${box.id}]")
|
||||
onTextBoxSelect(box.id)
|
||||
},
|
||||
onDragStart = { touchOffset ->
|
||||
Timber.tag("PdfTextBoxDebug").d("PdfPageRenderer onDragStart[ID: ${box.id}] isVerticalScroll=$isVerticalScroll | offset=$touchOffset")
|
||||
if (isVerticalScroll) {
|
||||
val topLeft = Offset(
|
||||
box.relativeBounds.left * staticData.targetWidth,
|
||||
|
|
@ -4729,10 +4893,12 @@ private fun PdfPageRenderer(
|
|||
}
|
||||
},
|
||||
onDrag = { delta, currentBounds ->
|
||||
Timber.tag("PdfTextBoxDebug").v("PdfPageRenderer onDrag [ID: ${box.id}] delta=$delta currentBounds=$currentBounds scale=$scale")
|
||||
if (isVerticalScroll) {
|
||||
onTextBoxDrag(delta)
|
||||
} else {
|
||||
onTextBoxDrag(delta)
|
||||
val scaledDelta = delta * scale
|
||||
onTextBoxDrag(scaledDelta)
|
||||
|
||||
val width = staticData.targetWidth
|
||||
val edgeThreshold = 60f
|
||||
|
|
@ -4746,9 +4912,11 @@ private fun PdfPageRenderer(
|
|||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
Timber.tag("PdfTextBoxDebug").d("PdfPageRenderer onDragEnd[ID: ${box.id}]")
|
||||
onTextBoxDragEnd()
|
||||
},
|
||||
onDragCancel = {
|
||||
Timber.tag("PdfTextBoxDebug").d("PdfPageRenderer onDragCancel [ID: ${box.id}]")
|
||||
onTextBoxDragEnd()
|
||||
},
|
||||
modifier = Modifier
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue