229 lines
No EOL
11 KiB
Kotlin
229 lines
No EOL
11 KiB
Kotlin
/*
|
|
* Episteme Reader - A native Android document reader.
|
|
* Copyright (C) 2026 Episteme Authors
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* Electronic mail: epistemereader@gmail.com
|
|
*/
|
|
package com.aryan.reader.pdf
|
|
|
|
import android.graphics.Rect
|
|
import timber.log.Timber
|
|
import androidx.compose.foundation.Canvas
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.draw.shadow
|
|
import androidx.compose.ui.geometry.Offset
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.ColorFilter
|
|
import androidx.compose.ui.graphics.ImageBitmap
|
|
import androidx.compose.ui.graphics.asImageBitmap
|
|
import androidx.compose.ui.unit.Dp
|
|
import androidx.compose.ui.unit.IntOffset
|
|
import androidx.compose.ui.unit.IntSize
|
|
import androidx.compose.ui.unit.dp
|
|
import kotlin.math.roundToInt
|
|
|
|
@Composable
|
|
fun MagnifierComposable(
|
|
sourceBitmap: ImageBitmap,
|
|
tiles: List<PdfTile>,
|
|
currentScale: Float,
|
|
magnifierCenterOnBitmap: Offset,
|
|
modifier: Modifier = Modifier,
|
|
magnifierWidth: Dp = 120.dp,
|
|
magnifierHeight: Dp = 60.dp,
|
|
zoomFactor: Float = 1.5f,
|
|
selectionRectsInBitmapCoords: List<Rect>,
|
|
highlightColor: Color,
|
|
colorFilter: ColorFilter? = null
|
|
) {
|
|
val stadiumShape = RoundedCornerShape(magnifierHeight / 2)
|
|
|
|
Box(
|
|
modifier = modifier
|
|
.width(magnifierWidth)
|
|
.height(magnifierHeight)
|
|
.shadow(4.dp, stadiumShape)
|
|
.clip(stadiumShape)
|
|
) {
|
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
|
val magnifierWidthPx = size.width
|
|
val magnifierHeightPx = size.height
|
|
|
|
Timber.d("Magnifier: START. scale=$currentScale, centerOnBitmap=$magnifierCenterOnBitmap")
|
|
|
|
val relevantTile = if (currentScale > 1f) {
|
|
tiles.find {
|
|
it.renderRect.contains(magnifierCenterOnBitmap.x.toInt(), magnifierCenterOnBitmap.y.toInt())
|
|
}
|
|
} else null
|
|
|
|
if (relevantTile != null) {
|
|
// --- HIGH-RES TILE PATH ---
|
|
Timber.d("Magnifier: Using HIGH-RES TILE path.")
|
|
Timber.d("Magnifier: Tile.renderRect=${relevantTile.renderRect}, Tile.bitmap.size=${relevantTile.bitmap.width}x${relevantTile.bitmap.height}")
|
|
val bitmapToUse = relevantTile.bitmap.asImageBitmap()
|
|
|
|
val tileBitmapWidth = relevantTile.bitmap.width.toFloat()
|
|
val tileRenderRectWidth = relevantTile.renderRect.width().toFloat()
|
|
|
|
val tileScale = if (tileRenderRectWidth > 0) {
|
|
tileBitmapWidth / tileRenderRectWidth
|
|
} else {
|
|
1f
|
|
}
|
|
Timber.d("Magnifier: Using derived tileScale=$tileScale instead of parent's currentScale=$currentScale")
|
|
|
|
|
|
val centerInTileBitmap = Offset(
|
|
x = (magnifierCenterOnBitmap.x - relevantTile.renderRect.left) * tileScale,
|
|
y = (magnifierCenterOnBitmap.y - relevantTile.renderRect.top) * tileScale
|
|
)
|
|
|
|
Timber.d("Magnifier: Calculated centerInTileBitmap=$centerInTileBitmap")
|
|
|
|
val sourceRectWidth = magnifierWidthPx / zoomFactor
|
|
val sourceRectHeight = magnifierHeightPx / zoomFactor
|
|
Timber.d("Magnifier: Desired sourceRect size=${sourceRectWidth}x$sourceRectHeight")
|
|
|
|
val srcLeft = (centerInTileBitmap.x - sourceRectWidth / 2f)
|
|
val srcTop = (centerInTileBitmap.y - sourceRectHeight / 2f)
|
|
Timber.d("Magnifier: Calculated source top-left=($srcLeft, $srcTop)")
|
|
|
|
val clampedSrcLeft = srcLeft.coerceIn(0f, bitmapToUse.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
|
|
val clampedSrcTop = srcTop.coerceIn(0f, bitmapToUse.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
|
|
Timber.d("Magnifier: Clamped source top-left=($clampedSrcLeft, $clampedSrcTop)")
|
|
|
|
val finalSrcLeftInt = clampedSrcLeft.roundToInt()
|
|
val finalSrcTopInt = clampedSrcTop.roundToInt()
|
|
|
|
val finalSrcWidthInt = (bitmapToUse.width - finalSrcLeftInt)
|
|
.coerceAtMost(sourceRectWidth.roundToInt()).coerceAtLeast(1)
|
|
val finalSrcHeightInt = (bitmapToUse.height - finalSrcTopInt)
|
|
.coerceAtMost(sourceRectHeight.roundToInt()).coerceAtLeast(1)
|
|
Timber.d("Magnifier: Final source rect to draw from tile: offset=($finalSrcLeftInt, $finalSrcTopInt), size=${finalSrcWidthInt}x$finalSrcHeightInt")
|
|
|
|
if (finalSrcWidthInt <= 0 || finalSrcHeightInt <= 0 || finalSrcLeftInt >= bitmapToUse.width || finalSrcTopInt >= bitmapToUse.height) {
|
|
Timber.w("Magnifier: Final source rect is invalid, returning.")
|
|
return@Canvas
|
|
}
|
|
|
|
drawImage(
|
|
image = bitmapToUse,
|
|
srcOffset = IntOffset(finalSrcLeftInt, finalSrcTopInt),
|
|
srcSize = IntSize(finalSrcWidthInt, finalSrcHeightInt),
|
|
dstSize = IntSize(magnifierWidthPx.roundToInt(), magnifierHeightPx.roundToInt()),
|
|
colorFilter = colorFilter
|
|
)
|
|
|
|
selectionRectsInBitmapCoords.forEach { rectInBitmap ->
|
|
val translatedLeft = (rectInBitmap.left - relevantTile.renderRect.left) * tileScale
|
|
val translatedTop = (rectInBitmap.top - relevantTile.renderRect.top) * tileScale
|
|
val translatedRight = (rectInBitmap.right - relevantTile.renderRect.left) * tileScale
|
|
val translatedBottom = (rectInBitmap.bottom - relevantTile.renderRect.top) * tileScale
|
|
|
|
val finalLeft = translatedLeft - clampedSrcLeft
|
|
val finalTop = translatedTop - clampedSrcTop
|
|
val finalRight = translatedRight - clampedSrcLeft
|
|
val finalBottom = translatedBottom - clampedSrcTop
|
|
|
|
val magnifiedLeft = finalLeft * zoomFactor
|
|
val magnifiedTop = finalTop * zoomFactor
|
|
val magnifiedRight = finalRight * zoomFactor
|
|
val magnifiedBottom = finalBottom * zoomFactor
|
|
|
|
if (magnifiedRight > 0 && magnifiedLeft < magnifierWidthPx && magnifiedBottom > 0 && magnifiedTop < magnifierHeightPx) {
|
|
drawRect(
|
|
color = highlightColor,
|
|
topLeft = Offset(magnifiedLeft, magnifiedTop),
|
|
size = androidx.compose.ui.geometry.Size(
|
|
width = magnifiedRight - magnifiedLeft,
|
|
height = magnifiedBottom - magnifiedTop
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
} else {
|
|
// --- LOW-RES / NO-ZOOM PATH ---
|
|
Timber.d("Magnifier: Using LOW-RES (base bitmap) path.")
|
|
val sourceRectWidth = magnifierWidthPx / zoomFactor
|
|
val sourceRectHeight = magnifierHeightPx / zoomFactor
|
|
Timber.d("Magnifier: Desired sourceRect size=${sourceRectWidth}x$sourceRectHeight")
|
|
|
|
val srcLeft = (magnifierCenterOnBitmap.x - sourceRectWidth / 2f)
|
|
val srcTop = (magnifierCenterOnBitmap.y - sourceRectHeight / 2f)
|
|
Timber.d("Magnifier: Calculated source top-left=($srcLeft, $srcTop)")
|
|
|
|
val clampedSrcLeft = srcLeft.coerceIn(0f, sourceBitmap.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
|
|
val clampedSrcTop = srcTop.coerceIn(0f, sourceBitmap.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
|
|
Timber.d("Magnifier: Clamped source top-left=($clampedSrcLeft, $clampedSrcTop)")
|
|
|
|
val finalSrcLeftInt = clampedSrcLeft.roundToInt()
|
|
val finalSrcTopInt = clampedSrcTop.roundToInt()
|
|
|
|
val finalSrcWidthInt = (sourceBitmap.width - finalSrcLeftInt)
|
|
.coerceAtMost(sourceRectWidth.roundToInt()).coerceAtLeast(1)
|
|
val finalSrcHeightInt = (sourceBitmap.height - finalSrcTopInt)
|
|
.coerceAtMost(sourceRectHeight.roundToInt()).coerceAtLeast(1)
|
|
Timber.d("Magnifier: Final source rect to draw from base: offset=($finalSrcLeftInt, $finalSrcTopInt), size=${finalSrcWidthInt}x$finalSrcHeightInt")
|
|
|
|
if (finalSrcWidthInt <= 0 || finalSrcHeightInt <= 0 || finalSrcLeftInt >= sourceBitmap.width || finalSrcTopInt >= sourceBitmap.height) {
|
|
Timber.w("Magnifier: Final source rect is invalid, returning.")
|
|
return@Canvas
|
|
}
|
|
|
|
drawImage(
|
|
image = sourceBitmap,
|
|
srcOffset = IntOffset(finalSrcLeftInt, finalSrcTopInt),
|
|
srcSize = IntSize(finalSrcWidthInt, finalSrcHeightInt),
|
|
dstSize = IntSize(magnifierWidthPx.roundToInt(), magnifierHeightPx.roundToInt()),
|
|
colorFilter = colorFilter
|
|
)
|
|
|
|
selectionRectsInBitmapCoords.forEach { rectInBitmap ->
|
|
val translatedLeft = rectInBitmap.left - clampedSrcLeft
|
|
val translatedTop = rectInBitmap.top - clampedSrcTop
|
|
val rectWidthInBitmap = rectInBitmap.width().toFloat()
|
|
val rectHeightInBitmap = rectInBitmap.height().toFloat()
|
|
|
|
val magnifiedLeft = translatedLeft * zoomFactor
|
|
val magnifiedTop = translatedTop * zoomFactor
|
|
val magnifiedWidth = rectWidthInBitmap * zoomFactor
|
|
val magnifiedHeight = rectHeightInBitmap * zoomFactor
|
|
|
|
if (magnifiedLeft + magnifiedWidth > 0 && magnifiedLeft < magnifierWidthPx &&
|
|
magnifiedTop + magnifiedHeight > 0 && magnifiedTop < magnifierHeightPx) {
|
|
drawRect(
|
|
color = highlightColor,
|
|
topLeft = Offset(magnifiedLeft, magnifiedTop),
|
|
size = androidx.compose.ui.geometry.Size(
|
|
width = magnifiedWidth,
|
|
height = magnifiedHeight
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |