* Add performance and stylus debugging logs

* Refactor and decouple UI models from `MainViewModel`

* Refactor library state management and projection logic

* Implement desktop shell using Compose Multiplatform

* Implement desktop shell using Compose Multiplatform

* Implement desktop shell using Compose Multiplatform

* Introduce ReaderEngine and enhance EPUB reader features in windows app

* Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support.

* Implement PDF rendering and text extraction for desktop using Pdfium

* Add `NonReaderScreens.kt` and UI dependencies

* Refactor and centralize library state management and models to improve cross-platform consistency

* Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing

* Implement PDF annotation system and enhanced zoom controls for the desktop viewer

* Implement WebView-based EPUB rendering for desktop using CEF and embedded resources

* Optimize UI state projection, navigation state handling, and main screen pager performance

* Implement Bring Your Own Key (BYOK) support for AI features in OSS version

* Support Gemini-based Cloud TTS with BYOK support for OSS builds

* Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling.

* crash fixes

* Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6

* Implement dynamic bottom padding for the page info bar to account for device rounded corners

* Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar`

* Optimize PDF tiling performance and refine pan-and-fling gesture handling

* Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers

* Updated UI for customize toolbar

* Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls

* restructure PDF viewer UI component hierarchy to fix verifier crash

* Implement separate text dimming factors for light and dark themes

* Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers

* Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning

* Centralize file type resolution logic and implement HTML sanitization during import

* Introduce vertical margin customization and configurable progress bar positioning

* texture support in epub reader

* Enhance TTS session management, progress tracking, and diagnostic logging

* Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic.

* Refine TTS page mapping for PDF and overhaul TTS control UI

* Implement natural session completion logic in `TtsPlaybackManager` for cloud tts

* Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen`

* Refine TTS playback continuity across PDF pages and improve state management for session transitions

* Implement global texture transparency and enhance textured theme support across PDF and EPUB readers.

* Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode

* Add Support Project screen

* Optimize library performance via projection caching, batch database updates, and scoped folder synchronization.

* Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic

* Bump version to 1.0.47 (51)
This commit is contained in:
Aryan 2026-05-04 21:55:38 +05:30 committed by GitHub
parent f42de6b462
commit d7a9cae9e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
126 changed files with 15287 additions and 3154 deletions

View file

@ -40,6 +40,7 @@ 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.max
import kotlin.math.roundToInt
@Composable
@ -68,6 +69,9 @@ fun MagnifierComposable(
Canvas(modifier = Modifier.fillMaxSize()) {
val magnifierWidthPx = size.width
val magnifierHeightPx = size.height
if (magnifierWidthPx <= 0f || magnifierHeightPx <= 0f || zoomFactor <= 0f) {
return@Canvas
}
Timber.d("Magnifier: START. scale=$currentScale, centerOnBitmap=$magnifierCenterOnBitmap")
@ -109,8 +113,10 @@ fun MagnifierComposable(
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))
val maxSrcLeft = max(0f, bitmapToUse.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
val maxSrcTop = max(0f, bitmapToUse.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
val clampedSrcLeft = srcLeft.coerceIn(0f, maxSrcLeft)
val clampedSrcTop = srcTop.coerceIn(0f, maxSrcTop)
Timber.d("Magnifier: Clamped source top-left=($clampedSrcLeft, $clampedSrcTop)")
val finalSrcLeftInt = clampedSrcLeft.roundToInt()
@ -174,8 +180,10 @@ fun MagnifierComposable(
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))
val maxSrcLeft = max(0f, sourceBitmap.width.toFloat() - sourceRectWidth.coerceAtLeast(1f))
val maxSrcTop = max(0f, sourceBitmap.height.toFloat() - sourceRectHeight.coerceAtLeast(1f))
val clampedSrcLeft = srcLeft.coerceIn(0f, maxSrcLeft)
val clampedSrcTop = srcTop.coerceIn(0f, maxSrcTop)
Timber.d("Magnifier: Clamped source top-left=($clampedSrcLeft, $clampedSrcTop)")
val finalSrcLeftInt = clampedSrcLeft.roundToInt()
@ -226,4 +234,4 @@ fun MagnifierComposable(
}
}
}
}
}

View file

@ -1,5 +1,7 @@
package com.aryan.reader.pdf
import com.aryan.reader.shared.pdf.PdfiumAnnotationSubtype
object NativePdfiumBridge {
init {
System.loadLibrary("native-lib")
@ -31,9 +33,9 @@ object NativePdfiumBridge {
@JvmStatic external fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray?
@JvmStatic external fun checkActionSupport(): Boolean
const val ANNOT_TEXT = 1 // Sticky Note
const val ANNOT_LINK = 2 // Link
const val ANNOT_HIGHLIGHT = 8 // Highlight
const val ANNOT_INK = 12 // Freehand drawing
const val ANNOT_WIDGET = 19
}
const val ANNOT_TEXT = PdfiumAnnotationSubtype.TEXT
const val ANNOT_LINK = PdfiumAnnotationSubtype.LINK
const val ANNOT_HIGHLIGHT = PdfiumAnnotationSubtype.HIGHLIGHT
const val ANNOT_INK = PdfiumAnnotationSubtype.INK
const val ANNOT_WIDGET = PdfiumAnnotationSubtype.WIDGET
}

View file

@ -84,7 +84,9 @@ fun VerticalScrollbar(
if (viewportRatio >= 1f) return@derivedStateOf null
val thumbHeight = (viewportHeight * viewportRatio).coerceIn(80f, viewportHeight / 2)
val maxThumbHeight = viewportHeight / 2f
val minThumbHeight = minOf(80f, maxThumbHeight)
val thumbHeight = (viewportHeight * viewportRatio).coerceIn(minThumbHeight, maxThumbHeight)
val firstItemIndex = listState.firstVisibleItemIndex
val firstItemOffset = listState.firstVisibleItemScrollOffset

View file

@ -69,9 +69,13 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageShader
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
@ -119,6 +123,7 @@ import androidx.core.graphics.scale
import androidx.core.graphics.set
import com.aryan.reader.R
import com.aryan.reader.SearchResult
import com.aryan.reader.loadReaderTextureBitmap
import com.aryan.reader.ml.SpeechBubble
import com.aryan.reader.pdf.data.PdfAnnotation
import com.aryan.reader.pdf.data.PdfTextBox
@ -127,6 +132,7 @@ import com.aryan.reader.pdf.ocr.OcrElement
import com.aryan.reader.pdf.ocr.OcrResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
@ -180,6 +186,13 @@ data class PdfPoint(val x: Float, val y: Float, val timestamp: Long = 0L)
data class PdfTile(val bitmap: Bitmap, val renderRect: Rect, val tileId: Int, val renderScale: Float = 1f)
private const val PDF_TILE_SIZE_DP = 256
private const val PDF_MAX_TILE_BITMAP_SIZE_PX = 3072
private const val PDF_TILE_SCALE_TOLERANCE = 0.06f
private const val PDF_TILE_IDLE_RENDER_DELAY_MS = 90L
private const val PDF_PAGINATION_PAN_FLING_MIN_VELOCITY = 600f
private const val PDF_PAGINATION_PAN_FLING_MULTIPLIER = 0.72f
enum class LinkSource {
ANNOTATION, TEXT_CONTENT
}
@ -426,7 +439,10 @@ data class PageStaticData(
val colorFilter: StableHolder<ColorFilter?>,
val isDarkMode: Boolean,
val excludeImages: Boolean,
val imageRects: StableHolder<List<android.graphics.Rect>>
val imageRects: StableHolder<List<android.graphics.Rect>>,
val textureBitmap: StableHolder<ImageBitmap?>,
val textureAlpha: Float,
val textureBlendMode: BlendMode
)
@Stable
@ -497,6 +513,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),
activeTextureAlpha: Float = 0.55f,
excludeImages: Boolean = false,
onDoubleTap: ((Offset) -> Unit)? = null,
isEditMode: Boolean = false,
@ -552,7 +569,7 @@ internal fun PdfPageComposable(
var isLoadingPage by remember { mutableStateOf(true) }
var pageErrorMessage by remember { mutableStateOf<String?>(null) }
val density = LocalDensity.current
LocalContext.current
val context = LocalContext.current
val viewConfiguration = LocalViewConfiguration.current
val coroutineScope = rememberCoroutineScope()
var isStylusEraserOverride by remember { mutableStateOf(false) }
@ -564,6 +581,7 @@ internal fun PdfPageComposable(
var isTransforming by remember { mutableStateOf(false) }
var scale by remember { mutableFloatStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
var paginationPanFlingJob by remember { mutableStateOf<Job?>(null) }
LaunchedEffect(scale, offset) {
onZoomAndPanChanged?.invoke(scale, offset)
@ -590,8 +608,10 @@ internal fun PdfPageComposable(
val pdfPageIndex = (virtualPage as? VirtualPage.PdfPage)?.pdfIndex ?: pageIndex
var tiles by remember { mutableStateOf<List<PdfTile>>(emptyList()) }
val tileSizeDp = 256.dp
val tileSizeDp = PDF_TILE_SIZE_DP.dp
val tileSizePx = with(LocalDensity.current) { tileSizeDp.toPx().toInt() }
val latestEffectiveScale by rememberUpdatedState(effectiveScale)
val latestEffectiveOffset by rememberUpdatedState(effectiveOffset)
SideEffect {
Timber.tag("PdfDrawPerf")
@ -633,6 +653,8 @@ internal fun PdfPageComposable(
var actualBitmapHeightPx by remember { mutableIntStateOf(0) }
var currentPageRotation by remember { mutableIntStateOf(0) }
val needsTilingNow = (effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000) && (isVerticalScroll || isActivePage)
val canvasWidthPx = remember { mutableFloatStateOf(0f) }
val canvasHeightPx = remember { mutableFloatStateOf(0f) }
@ -687,6 +709,15 @@ internal fun PdfPageComposable(
activeTheme.backgroundColor
}
}
val textureBitmap = remember(activeTheme.textureId) {
loadReaderTextureBitmap(context, activeTheme.textureId)
}
val effectiveTextureAlpha = remember(activeTheme.textureId, activeTextureAlpha) {
if (activeTheme.textureId == null) 0f else activeTextureAlpha.coerceIn(0f, 1f)
}
val textureBlendMode = remember(activeTheme.textureId, activeTheme.isDark, activeTheme.id) {
if (activeTheme.isDark || activeTheme.id == "reverse") BlendMode.Screen else BlendMode.Multiply
}
val centeringOffsetX by remember(canvasWidthPx.floatValue, actualBitmapWidthPx) {
derivedStateOf { (canvasWidthPx.floatValue - actualBitmapWidthPx) / 2f }
@ -1146,13 +1177,13 @@ internal fun PdfPageComposable(
try {
val pagePtr = pageWrapper.getNativePointer()
if (pagePtr != 0L) {
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
val objCount = PdfiumEngineProvider.bridge.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)) {
if (PdfiumEngineProvider.bridge.getPageObjectType(pagePtr, i) == 3) { // 3 = FPDF_PAGEOBJ_IMAGE
if (PdfiumEngineProvider.bridge.getPageObjectBoundingBox(pagePtr, i, outRect)) {
val pdfRectF = android.graphics.RectF(
min(outRect[0], outRect[2]),
max(outRect[1], outRect[3]),
@ -1180,26 +1211,26 @@ internal fun PdfPageComposable(
val pagePtr = pageWrapper.getNativePointer()
if (pagePtr != 0L) {
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
val count = PdfiumEngineProvider.bridge.getAnnotCount(pagePtr)
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
if (count > 0) {
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
val count = PdfiumEngineProvider.bridge.getAnnotCount(pagePtr)
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
if (count > 0) {
val allAnnots = (0 until count).mapNotNull { i ->
val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i)
val subtype = PdfiumEngineProvider.bridge.getAnnotSubtype(pagePtr, i)
if (subtype == annotLink) return@mapNotNull null
var contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents")
var contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "Contents")
if (contents.isNullOrBlank()) {
contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "RC")
contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "RC")
}
val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM")
val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT")
val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T")
val name = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "NM")
val irt = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "IRT")
val author = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "T")
val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i)
val pdfRectArray = PdfiumEngineProvider.bridge.getAnnotRect(pagePtr, i)
val pdfRectF = if (pdfRectArray != null) {
android.graphics.RectF(
min(pdfRectArray[0], pdfRectArray[2]),
@ -1311,8 +1342,7 @@ internal fun PdfPageComposable(
}
LaunchedEffect(
effectiveScale,
effectiveOffset,
needsTilingNow,
actualBitmapWidthPx,
actualBitmapHeightPx,
canvasWidthPx.floatValue,
@ -1323,8 +1353,7 @@ internal fun PdfPageComposable(
virtualPage,
isActivePage
) {
val needsTiling = (effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000) && (isVerticalScroll || isActivePage)
if (!needsTiling) {
if (!needsTilingNow) {
if (tiles.isNotEmpty()) {
val oldTiles = tiles
tiles = emptyList()
@ -1358,24 +1387,37 @@ internal fun PdfPageComposable(
snapshotFlow {
val rect = visibleScreenRect()
if (rect == null) null
else {
val observedScale = latestEffectiveScale
if (isVerticalScroll && rect != null) {
val qTop = rect.top / (tileSizePx / 2)
val qLeft = rect.left / (tileSizePx / 2)
val qBottom = rect.bottom / (tileSizePx / 2)
val qRight = rect.right / (tileSizePx / 2)
listOf(qTop, qLeft, qBottom, qRight)
listOf(qTop, qLeft, qBottom, qRight, (observedScale * 10f).roundToInt())
} else if (!isVerticalScroll) {
val observedOffset = latestEffectiveOffset
val pivotX = screenWidth / 2f
val pivotY = screenHeight / 2f
val pxTl = (((0 - observedOffset.x) - pivotX) / observedScale + pivotX) - centeringOffsetX
val pyTl = (((0 - observedOffset.y) - pivotY) / observedScale + pivotY) - centeringOffsetY
val pxBr = (((screenWidth - observedOffset.x) - pivotX) / observedScale + pivotX) - centeringOffsetX
val pyBr = (((screenHeight - observedOffset.y) - pivotY) / observedScale + pivotY) - centeringOffsetY
val qTop = pyTl.toInt() / (tileSizePx / 2)
val qLeft = pxTl.toInt() / (tileSizePx / 2)
val qBottom = pyBr.toInt() / (tileSizePx / 2)
val qRight = pxBr.toInt() / (tileSizePx / 2)
listOf(qTop, qLeft, qBottom, qRight, (observedScale * 10f).roundToInt())
} else {
null
}
}.conflate().collectLatest { _ ->
delay(150)
val tileCalcStart = System.nanoTime()
if (!isActive) return@collectLatest
if (isScrolling && effectiveScale > 1f) {
return@collectLatest
}
val renderScale = latestEffectiveScale
val renderOffset = latestEffectiveOffset
val currentVisibleRect = visibleScreenRect()
@ -1404,14 +1446,14 @@ internal fun PdfPageComposable(
val pivotX = screenWidth / 2f
val pivotY = screenHeight / 2f
pxTl = (((0 - effectiveOffset.x) - pivotX) / effectiveScale + pivotX) - centeringOffsetX
pyTl = (((0 - effectiveOffset.y) - pivotY) / effectiveScale + pivotY) - centeringOffsetY
pxBr = (((screenWidth - effectiveOffset.x) - pivotX) / effectiveScale + pivotX) - centeringOffsetX
pyBr = (((screenHeight - effectiveOffset.y) - pivotY) / effectiveScale + pivotY) - centeringOffsetY
pxTl = (((0 - renderOffset.x) - pivotX) / renderScale + pivotX) - centeringOffsetX
pyTl = (((0 - renderOffset.y) - pivotY) / renderScale + pivotY) - centeringOffsetY
pxBr = (((screenWidth - renderOffset.x) - pivotX) / renderScale + pivotX) - centeringOffsetX
pyBr = (((screenHeight - renderOffset.y) - pivotY) / renderScale + pivotY) - centeringOffsetY
}
val visibleBitmapRect = Rect(pxTl.toInt(), pyTl.toInt(), pxBr.toInt(), pyBr.toInt())
val inset = if (effectiveScale > 2f) 0 else -tileSizePx
val inset = if (renderScale > 2f) 0 else -tileSizePx
visibleBitmapRect.inset(inset, inset)
val requiredTileIds = mutableSetOf<Int>()
@ -1431,8 +1473,10 @@ internal fun PdfPageComposable(
val currentTileIds = tiles.map { it.tileId }.toSet()
val scaleTolerance = 0.05f
val validCurrentTileIds = tiles.filter { abs(it.renderScale - effectiveScale) <= scaleTolerance }.map { it.tileId }.toSet()
val scaleTolerance = PDF_TILE_SCALE_TOLERANCE
val validCurrentTileIds = tiles.filter { abs(it.renderScale - renderScale) <= scaleTolerance }.map { it.tileId }.toSet()
val tilesToRenderIds = requiredTileIds - validCurrentTileIds
val tilesToRecycleIds = currentTileIds - requiredTileIds
val duration = (System.nanoTime() - tileCalcStart) / 1_000_000f
if (duration > 2f) {
@ -1441,21 +1485,26 @@ internal fun PdfPageComposable(
)
}
if (requiredTileIds != validCurrentTileIds) {
val tilesToRenderIds = requiredTileIds - validCurrentTileIds
val tilesToRecycleIds = currentTileIds - requiredTileIds
if (tilesToRecycleIds.isNotEmpty()) {
val (tilesToRecycle, tilesToKeep) = tiles.partition { it.tileId in tilesToRecycleIds }
tiles = tilesToKeep
withContext(Dispatchers.IO) {
tilesToRecycle.forEach { PdfBitmapPool.recycle(it.bitmap) }
}
if (tilesToRecycleIds.isNotEmpty()) {
val (tilesToRecycle, tilesToKeep) = tiles.partition { it.tileId in tilesToRecycleIds }
tiles = tilesToKeep
withContext(Dispatchers.IO) {
tilesToRecycle.forEach { PdfBitmapPool.recycle(it.bitmap) }
}
}
if (isScrolling && renderScale > 1f) {
return@collectLatest
}
if (requiredTileIds != validCurrentTileIds) {
if (tilesToRenderIds.isNotEmpty()) {
withContext(Dispatchers.IO) {
delay(PDF_TILE_IDLE_RENDER_DELAY_MS)
if (!isActive) return@collectLatest
if (isScrolling && latestEffectiveScale > 1f) return@collectLatest
val renderedTiles = withContext(Dispatchers.IO) {
val newTiles = mutableListOf<PdfTile>()
tilesToRenderIds.forEach { tileId ->
if (!isActive) return@forEach
@ -1469,14 +1518,18 @@ internal fun PdfPageComposable(
(col + 1) * tileSizePx,
(row + 1) * tileSizePx
)
val tileRenderSize = (tileSizePx * effectiveScale).toInt().coerceAtLeast(1)
val tileRenderScale = min(
renderScale,
PDF_MAX_TILE_BITMAP_SIZE_PX.toFloat() / tileSizePx.toFloat()
)
val tileRenderSize = (tileSizePx * tileRenderScale).toInt().coerceAtLeast(1)
val tileBitmap = PdfBitmapPool.get(tileRenderSize)
val fullPageRenderWidth = (actualBitmapWidthPx * effectiveScale).toInt()
val fullPageRenderHeight = (actualBitmapHeightPx * effectiveScale).toInt()
val tileRenderX = (col * tileSizePx * effectiveScale).toInt()
val tileRenderY = (row * tileSizePx * effectiveScale).toInt()
val fullPageRenderWidth = (actualBitmapWidthPx * tileRenderScale).toInt()
val fullPageRenderHeight = (actualBitmapHeightPx * tileRenderScale).toInt()
val tileRenderX = (col * tileSizePx * tileRenderScale).toInt()
val tileRenderY = (row * tileSizePx * tileRenderScale).toInt()
page?.renderPageBitmap(
bitmap = tileBitmap,
@ -1487,24 +1540,26 @@ internal fun PdfPageComposable(
renderAnnot = true
)
val newTile = PdfTile(tileBitmap, tileRect, tileId, effectiveScale)
var handedOver = false
try {
withContext(Dispatchers.Main) {
val oldTile = tiles.find { it.tileId == tileId }
tiles = tiles.filter { it.tileId != tileId } + newTile
handedOver = true
newTiles += PdfTile(tileBitmap, tileRect, tileId, renderScale)
}
newTiles
}
oldTile?.let {
coroutineScope.launch(Dispatchers.IO) {
PdfBitmapPool.recycle(it.bitmap)
}
}
}
} finally {
if (!handedOver) {
PdfBitmapPool.recycle(tileBitmap)
}
if (!isActive) {
withContext(Dispatchers.IO) {
renderedTiles.forEach { PdfBitmapPool.recycle(it.bitmap) }
}
return@collectLatest
}
if (renderedTiles.isNotEmpty()) {
val renderedIds = renderedTiles.map { it.tileId }.toSet()
val replacedTiles = tiles.filter { it.tileId in renderedIds }
tiles = tiles.filterNot { it.tileId in renderedIds } + renderedTiles
if (replacedTiles.isNotEmpty()) {
coroutineScope.launch(Dispatchers.IO) {
replacedTiles.forEach { PdfBitmapPool.recycle(it.bitmap) }
}
}
}
@ -2799,7 +2854,7 @@ internal fun PdfPageComposable(
Timber.tag("PdfLinkDiagnostic").i("Extracted docPtr: $docPtr | pagePtr: $pagePtr")
val linkInfo = NativePdfiumBridge.getLinkInfoAtPoint(
val linkInfo = PdfiumEngineProvider.bridge.getLinkInfoAtPoint(
docPtr, pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble()
)
@ -2818,7 +2873,7 @@ internal fun PdfPageComposable(
}
}
val clickHandled = NativePdfiumBridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
val clickHandled = PdfiumEngineProvider.bridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
if (clickHandled) {
return@withContext 2
}
@ -3007,25 +3062,21 @@ internal fun PdfPageComposable(
awaitEachGesture {
@Suppress("UnusedVariable", "Unused") val down =
awaitFirstDown(requireUnconsumed = false)
paginationPanFlingJob?.cancel()
paginationPanFlingJob = null
velocityTracker.resetTracking()
var mode = 0
var accumulatedZoom = 1f
var accumulatedPan = Offset.Zero
var swipeAccumulatorX = 0f
var velocityAccumulator = Offset.Zero
do {
val event = awaitPointerEvent()
val canceled = event.changes.any { it.isConsumed }
val pointerCount = event.changes.size
val currentCentroid = event.calculateCentroid(useCurrent = true)
if (pointerCount > 0 && currentCentroid != Offset.Unspecified) {
velocityTracker.addPosition(
event.changes[0].uptimeMillis, currentCentroid
)
}
if (!canceled) {
val rawPanChange = event.calculatePan()
val panChange = if (isScrollLocked && pointerCount == 1) {
@ -3077,6 +3128,14 @@ internal fun PdfPageComposable(
Timber.tag("PdfZoomDebug").v("Panning: Offset $offset -> $newX, $newY (Max: $maxOffsetX, $maxOffsetY)")
offset = Offset(newX, newY)
if (event.changes.isNotEmpty() && panChange != Offset.Zero) {
velocityAccumulator += panChange
velocityTracker.addPosition(
event.changes[0].uptimeMillis,
velocityAccumulator
)
}
event.changes.forEach {
if (it.positionChanged()) it.consume()
}
@ -3189,38 +3248,54 @@ internal fun PdfPageComposable(
}
}
} else if (mode == 1 && scale > 1f) {
val velocity = velocityTracker.calculateVelocity()
val contentWidth = actualBitmapWidthPx * scale
val contentHeight = actualBitmapHeightPx * scale
val maxOffsetX = (contentWidth - size.width).coerceAtLeast(0f) / 2f
val maxOffsetY = (contentHeight - size.height).coerceAtLeast(0f) / 2f
val startX = offset.x
val startY = offset.y
val velocity = velocityTracker.calculateVelocity()
val flingX = if (!isScrollLocked && abs(velocity.x) > PDF_PAGINATION_PAN_FLING_MIN_VELOCITY) {
velocity.x * PDF_PAGINATION_PAN_FLING_MULTIPLIER
} else {
0f
}
val flingY = if (abs(velocity.y) > PDF_PAGINATION_PAN_FLING_MIN_VELOCITY) {
velocity.y * PDF_PAGINATION_PAN_FLING_MULTIPLIER
} else {
0f
}
coroutineScope.launch {
coroutineScope {
launch {
if (!isScrollLocked) {
Animatable(startX).animateDecay(
velocity.x, decay
) {
val newX = value.coerceIn(
-maxOffsetX, maxOffsetX
)
offset = offset.copy(x = newX)
if (flingX == 0f && flingY == 0f) {
offset = Offset(
x = offset.x.coerceIn(-maxOffsetX, maxOffsetX),
y = offset.y.coerceIn(-maxOffsetY, maxOffsetY)
)
} else {
val startOffset = offset
paginationPanFlingJob = coroutineScope.launch {
try {
coroutineScope {
launch {
if (flingX != 0f) {
Animatable(startOffset.x).animateDecay(flingX, decay) {
offset = offset.copy(
x = value.coerceIn(-maxOffsetX, maxOffsetX)
)
}
}
}
launch {
if (flingY != 0f) {
Animatable(startOffset.y).animateDecay(flingY, decay) {
offset = offset.copy(
y = value.coerceIn(-maxOffsetY, maxOffsetY)
)
}
}
}
}
}
launch {
Animatable(startY).animateDecay(
velocity.y, decay
) {
val newY = value.coerceIn(
-maxOffsetY, maxOffsetY
)
offset = offset.copy(y = newY)
}
} finally {
paginationPanFlingJob = null
}
}
}
@ -3256,7 +3331,7 @@ internal fun PdfPageComposable(
}
val buttons = currentEvent.buttons
Timber.tag("StylusEraserDiagnostic").d(
Timber.tag("StylusDebug").d(
"Page $pageIndex | Type: ${down.type} | isPrimary: ${buttons.isPrimaryPressed} | isSecondary: ${buttons.isSecondaryPressed} | isTertiary: ${buttons.isTertiaryPressed} | buttonsString: $buttons"
)
@ -3881,7 +3956,10 @@ internal fun PdfPageComposable(
stableColorFilter,
isDarkMode,
excludeImages,
stableImageRects
stableImageRects,
textureBitmap,
effectiveTextureAlpha,
textureBlendMode
) {
Timber.tag("PdfDrawPerf").v(
"STATIC DATA GENERATED: Scale=$effectiveScale, Tiles=${stableTiles.item.size}"
@ -3899,7 +3977,10 @@ internal fun PdfPageComposable(
colorFilter = stableColorFilter,
isDarkMode = isDarkMode,
excludeImages = excludeImages,
imageRects = stableImageRects
imageRects = stableImageRects,
textureBitmap = StableHolder(textureBitmap),
textureAlpha = effectiveTextureAlpha,
textureBlendMode = textureBlendMode
)
}
@ -4263,7 +4344,10 @@ private fun PdfBitmapLayer(
colorFilter: ColorFilter? = null,
isDarkMode: Boolean = false,
excludeImages: Boolean = false,
imageRects: List<android.graphics.Rect> = emptyList()
imageRects: List<android.graphics.Rect> = emptyList(),
textureBitmap: ImageBitmap? = null,
textureAlpha: Float = 0f,
textureBlendMode: BlendMode = BlendMode.Multiply
) {
Canvas(modifier = Modifier.fillMaxSize().graphicsLayer()) {
translate(left = centeringOffsetX, top = centeringOffsetY) {
@ -4363,6 +4447,15 @@ private fun PdfBitmapLayer(
}
}
}
if (textureBitmap != null && textureAlpha > 0f) {
drawRect(
brush = ShaderBrush(ImageShader(textureBitmap, TileMode.Repeated, TileMode.Repeated)),
size = Size(dstW.toFloat(), dstH.toFloat()),
blendMode = textureBlendMode,
alpha = textureAlpha
)
}
}
}
}
@ -4884,7 +4977,10 @@ private fun PdfPageStaticLayer(data: PageStaticData) {
colorFilter = data.colorFilter.item,
isDarkMode = data.isDarkMode,
excludeImages = data.excludeImages,
imageRects = data.imageRects.item
imageRects = data.imageRects.item,
textureBitmap = data.textureBitmap.item,
textureAlpha = data.textureAlpha,
textureBlendMode = data.textureBlendMode
)
}

View file

@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.core.content.edit
import com.aryan.reader.BuildConfig
import com.aryan.reader.ReaderTheme
import com.aryan.reader.ReaderTexture
import com.aryan.reader.epubreader.SystemUiMode
internal const val VERTICAL_SCROLL_TAG = "PdfVerticalScroll"
@ -39,6 +40,8 @@ private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
private const val PDF_THEME_KEY = "pdf_reader_theme"
private const val PDF_KEEP_SCREEN_ON_KEY = "pdf_keep_screen_on_enabled"
private const val PDF_HIDDEN_TOOLS_KEY = "pdf_hidden_tools"
private const val PDF_TOOL_ORDER_KEY = "pdf_tool_order"
private const val PDF_BOTTOM_TOOLS_KEY = "pdf_bottom_tools"
private const val PDF_SYSTEM_UI_MODE_KEY = "pdf_system_ui_mode"
internal const val PDF_LAYOUT_DEBUG_TAG = "PdfLayoutDebug"
@ -76,7 +79,13 @@ val PdfBuiltInThemes = listOf(
ReaderTheme("dark", "Dark", Color(0xFF121212), Color(0xFFE0E0E0), true),
ReaderTheme("sepia", "Sepia", Color(0xFFFBF0D9), Color(0xFF5F4B32), false),
ReaderTheme("slate", "Slate", Color(0xFF2E3440), Color(0xFFECEFF4), true),
ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true)
ReaderTheme("oled", "OLED", Color(0xFF000000), Color(0xFFB0B0B0), true),
ReaderTheme("pdf_natural_white_texture", "Natural White", Color(0xFFF7F1E5), Color(0xFF1D1B18), false, textureId = ReaderTexture.NATURAL_WHITE.id),
ReaderTheme("pdf_retina_texture", "Retina", Color(0xFFF1E4CD), Color(0xFF2A2119), false, textureId = ReaderTexture.RETINA_WOOD.id),
ReaderTheme("pdf_veneer_texture", "Veneer", Color(0xFFF4E7CF), Color(0xFF2A2119), false, textureId = ReaderTexture.LIGHT_VENEER.id),
ReaderTheme("pdf_grey_wash_texture", "Grey Wash", Color(0xFF202124), Color(0xFFFFFFFF), true, textureId = ReaderTexture.GREY_WASH.id),
ReaderTheme("pdf_fabric_texture", "Fabric", Color(0xFF262626), Color(0xFFE8E2D8), true, textureId = ReaderTexture.CLASSY_FABRIC.id),
ReaderTheme("pdf_retro_texture", "Retro", Color(0xFFF6ECD8), Color(0xFF2F2118), false, textureId = ReaderTexture.RETRO_INTRO.id)
)
internal fun loadPdfHiddenTools(context: Context): Set<String> {
@ -89,6 +98,32 @@ internal fun savePdfHiddenTools(context: Context, hiddenTools: Set<String>) {
prefs.edit { putStringSet(PDF_HIDDEN_TOOLS_KEY, hiddenTools) }
}
internal fun loadPdfToolOrder(context: Context): List<PdfReaderTool> {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
val savedTools = prefs.getString(PDF_TOOL_ORDER_KEY, null)
?.split(',')
?.filter { it.isNotBlank() }
?.mapNotNull { name -> PdfReaderTool.entries.firstOrNull { it.name == name } }
.orEmpty()
return (savedTools + PdfReaderTool.entries.filterNot { it in savedTools }).distinct()
}
internal fun savePdfToolOrder(context: Context, toolOrder: List<PdfReaderTool>) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putString(PDF_TOOL_ORDER_KEY, toolOrder.joinToString(",") { it.name }) }
}
internal fun loadPdfBottomTools(context: Context): Set<String> {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
val defaultBottomTools = PdfReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
return prefs.getStringSet(PDF_BOTTOM_TOOLS_KEY, defaultBottomTools) ?: defaultBottomTools
}
internal fun savePdfBottomTools(context: Context, bottomTools: Set<String>) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putStringSet(PDF_BOTTOM_TOOLS_KEY, bottomTools) }
}
internal fun loadCustomHighlightColors(context: Context): Map<PdfHighlightColor, Color> {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return PdfHighlightColor.entries.associateWith {
@ -139,7 +174,7 @@ internal fun loadPdfThemeId(context: Context): String {
}
internal fun loadUseOnlineDict(context: Context): Boolean {
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE) return false
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(PREF_USE_ONLINE_DICT, true)
}

View file

@ -1,8 +1,9 @@
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)
@file:OptIn(ExperimentalMaterial3Api::class)
package com.aryan.reader.pdf
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -11,94 +12,346 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.zIndex
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.aryan.reader.R
import com.aryan.reader.epubreader.OptionSegmentedControl
import com.aryan.reader.epubreader.SystemUiMode
enum class PdfFlatItemType { SECTION_HEADER, TOOL, EMPTY_PLACEHOLDER, MORE_HEADER, MORE_TOOL }
data class PdfFlatToolItem(
val id: String,
val type: PdfFlatItemType,
val tool: PdfReaderTool? = null,
val section: PdfToolbarSection? = null,
val title: String? = null
)
fun sanitizePdfPlaceholders(list: List<PdfFlatToolItem>): List<PdfFlatToolItem> {
val result = mutableListOf<PdfFlatToolItem>()
val sectionMap = mutableMapOf<PdfToolbarSection, MutableList<PdfFlatToolItem>>()
PdfToolbarSection.entries.forEach { sectionMap[it] = mutableListOf() }
list.forEach { item ->
if (item.type == PdfFlatItemType.TOOL) {
item.section?.let { sectionMap[it]?.add(item) }
}
}
PdfToolbarSection.entries.forEach { section ->
result.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, title = section.title))
val tools = sectionMap[section] ?: emptyList()
if (tools.isEmpty()) {
result.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
} else {
result.addAll(tools)
}
}
list.filter { it.type == PdfFlatItemType.MORE_HEADER || it.type == PdfFlatItemType.MORE_TOOL }.forEach {
result.add(it)
}
return result
}
class PdfDragDropState(
val lazyListState: LazyListState,
val onMove: (String, String) -> Unit
) {
var draggedItemId by mutableStateOf<String?>(null)
var dragOffset by mutableStateOf(Offset.Zero)
fun onDragStart(id: String) { draggedItemId = id; dragOffset = Offset.Zero }
fun onDrag(delta: Offset) {
val draggedId = draggedItemId ?: return
dragOffset += delta
val visibleItems = lazyListState.layoutInfo.visibleItemsInfo
val currentItem = visibleItems.find { it.key == draggedId } ?: return
val center = currentItem.offset + dragOffset.y + currentItem.size / 2f
val targetItem = visibleItems.find { it.key != draggedId && center >= it.offset && center <= (it.offset + it.size) }
if (targetItem != null) {
onMove(draggedId, targetItem.key.toString())
dragOffset = dragOffset.copy(y = dragOffset.y - (targetItem.offset - currentItem.offset))
}
}
fun onDragEnd() { draggedItemId = null; dragOffset = Offset.Zero }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PdfCustomizeToolsSheet(
hiddenTools: Set<String>,
toolOrder: List<PdfReaderTool>,
bottomTools: Set<String>,
onUpdate: (Set<String>) -> Unit,
onOrderUpdate: (List<PdfReaderTool>) -> Unit,
onPlacementUpdate: (Set<String>) -> Unit,
onDismiss: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
contentWindowInsets = { WindowInsets.navigationBars }
) {
Column(modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 24.dp)) {
Text(
text = stringResource(R.string.title_customize_toolbar),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.desc_customize_toolbar),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
val reorderableToolbarTools = setOf(
PdfReaderTool.DICTIONARY, PdfReaderTool.THEME, PdfReaderTool.LOCK_PANNING,
PdfReaderTool.SLIDER, PdfReaderTool.TOC, PdfReaderTool.SEARCH,
PdfReaderTool.HIGHLIGHT_ALL, PdfReaderTool.AI_FEATURES,
PdfReaderTool.EDIT_MODE, PdfReaderTool.TTS_CONTROLS
)
LazyColumn(modifier = Modifier.fillMaxWidth()) {
PdfReaderTool.entries.groupBy { it.category }.forEach { (category, tools) ->
item {
Text(
text = category,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
var localHiddenTools by remember { mutableStateOf(hiddenTools) }
var flatItems by remember {
mutableStateOf<List<PdfFlatToolItem>>(
run {
val toolbarTools = toolOrder.filter { it in reorderableToolbarTools }
val topTools = toolbarTools.filter { !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
val bottomToolsList = toolbarTools.filter { bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
val hiddenToolsList = toolbarTools.filter { hiddenTools.contains(it.name) }
val moreTools = toolOrder.filter { it !in reorderableToolbarTools }
val list = mutableListOf<PdfFlatToolItem>()
PdfToolbarSection.entries.forEach { section ->
val tools = when(section) {
PdfToolbarSection.TOP -> topTools
PdfToolbarSection.BOTTOM -> bottomToolsList
PdfToolbarSection.HIDDEN -> hiddenToolsList
}
items(tools) { tool ->
Row(
list.add(PdfFlatToolItem("header_${section.name}", PdfFlatItemType.SECTION_HEADER, section = section, title = section.title))
if (tools.isEmpty()) {
list.add(PdfFlatToolItem("empty_${section.name}", PdfFlatItemType.EMPTY_PLACEHOLDER, section = section))
} else {
tools.forEach { tool ->
list.add(PdfFlatToolItem("tool_${tool.name}", PdfFlatItemType.TOOL, tool = tool, section = section))
}
}
}
list.add(PdfFlatToolItem("more_header", PdfFlatItemType.MORE_HEADER, title = "More menu"))
moreTools.forEach { tool ->
list.add(PdfFlatToolItem("more_${tool.name}", PdfFlatItemType.MORE_TOOL, tool = tool))
}
list
}
)
}
val commitDragDrop = {
val newHidden = localHiddenTools.filter { toolName ->
toolOrder.find { it.name == toolName } !in reorderableToolbarTools
}.toMutableSet()
val newBottom = mutableSetOf<String>()
val newOrder = mutableListOf<PdfReaderTool>()
flatItems.forEach { item ->
if (item.type == PdfFlatItemType.TOOL && item.tool != null) {
newOrder.add(item.tool)
if (item.section == PdfToolbarSection.HIDDEN) newHidden.add(item.tool.name)
if (item.section == PdfToolbarSection.BOTTOM) newBottom.add(item.tool.name)
}
}
val moreTools = flatItems.filter { it.type == PdfFlatItemType.MORE_TOOL }.mapNotNull { it.tool }
newOrder.addAll(moreTools)
localHiddenTools = newHidden
onUpdate(newHidden)
onPlacementUpdate(newBottom)
onOrderUpdate(newOrder)
}
val lazyListState = rememberLazyListState()
val dragDropState = remember {
PdfDragDropState(lazyListState) { fromKey, toKey ->
val fromIndex = flatItems.indexOfFirst { it.id == fromKey }
val toIndex = flatItems.indexOfFirst { it.id == toKey }
if (fromIndex == -1 || toIndex == -1 || fromIndex == toIndex) return@PdfDragDropState
val fromItem = flatItems[fromIndex]
if (fromItem.type != PdfFlatItemType.TOOL) return@PdfDragDropState
val toItem = flatItems[toIndex]
if (toItem.type == PdfFlatItemType.MORE_HEADER || toItem.type == PdfFlatItemType.MORE_TOOL) return@PdfDragDropState
val newList = flatItems.toMutableList()
val movedItem = newList.removeAt(fromIndex)
val newToIndex = newList.indexOfFirst { it.id == toKey }
val insertIndex = if (fromIndex < toIndex) newToIndex + 1 else newToIndex
newList.add(insertIndex, movedItem)
var actualSection = movedItem.section
for (i in insertIndex downTo 0) {
val item = newList[i]
if (item.type == PdfFlatItemType.SECTION_HEADER) {
actualSection = item.section
break
}
}
newList[insertIndex] = movedItem.copy(section = actualSection)
flatItems = newList
}
}
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
androidx.compose.material3.Surface(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.navigationBars),
color = MaterialTheme.colorScheme.surface
) {
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp)) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.title_customize_toolbar),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close))
}
}
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 24.dp)
) {
items(flatItems, key = { it.id }) { item ->
val isDragged = item.id == dragDropState.draggedItemId
val zIndex = if (isDragged) 1f else 0f
val elevation = if (isDragged) 8.dp else 0.dp
val scale = if (isDragged) 1.03f else 1f
val translationY = if (isDragged) dragDropState.dragOffset.y else 0f
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable {
val newSet = hiddenTools.toMutableSet()
if (newSet.contains(tool.name)) newSet.remove(tool.name)
else newSet.add(tool.name)
onUpdate(newSet)
.then(if (isDragged) Modifier else Modifier.animateItem())
.zIndex(zIndex)
.graphicsLayer {
this.translationY = translationY
this.scaleX = scale
this.scaleY = scale
this.shadowElevation = elevation.toPx()
}
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = tool.title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Switch(
checked = !hiddenTools.contains(tool.name),
onCheckedChange = { isVisible ->
val newSet = hiddenTools.toMutableSet()
if (isVisible) newSet.remove(tool.name) else newSet.add(tool.name)
onUpdate(newSet)
when (item.type) {
PdfFlatItemType.SECTION_HEADER -> {
Text(
text = item.title ?: "",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp, start = 4.dp)
)
}
)
PdfFlatItemType.EMPTY_PLACEHOLDER -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(vertical = 4.dp)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
Text("Drop tools here", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
PdfFlatItemType.TOOL -> {
PdfToolbarDragRow(
tool = item.tool!!,
isDragging = isDragged,
onDragStart = { dragDropState.onDragStart(item.id) },
onDrag = { dragDropState.onDrag(it) },
onDragEnd = {
dragDropState.onDragEnd()
flatItems = sanitizePdfPlaceholders(flatItems).toList()
commitDragDrop()
}
)
}
PdfFlatItemType.MORE_HEADER -> {
Text(
text = item.title ?: "More menu",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 4.dp)
)
}
PdfFlatItemType.MORE_TOOL -> {
PdfMoreToolVisibilityRow(
title = item.tool!!.title,
visible = !localHiddenTools.contains(item.tool.name),
onToggle = {
localHiddenTools = if (localHiddenTools.contains(item.tool.name)) {
localHiddenTools - item.tool.name
} else {
localHiddenTools + item.tool.name
}
onUpdate(localHiddenTools)
}
)
}
}
}
}
}
@ -107,6 +360,154 @@ fun PdfCustomizeToolsSheet(
}
}
@Composable
private fun PdfToolbarDragRow(
tool: PdfReaderTool,
isDragging: Boolean,
onDragStart: () -> Unit,
onDrag: (Offset) -> Unit,
onDragEnd: () -> Unit
) {
androidx.compose.material3.Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
shape = RoundedCornerShape(12.dp),
color = if (isDragging) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
) {
Row(
modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
PdfToolPreviewIcon(tool)
Spacer(Modifier.width(16.dp))
Text(
text = tool.title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Icon(
Icons.Default.Menu,
contentDescription = "Drag to reorder",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.size(48.dp)
.padding(12.dp)
.pointerInput(tool) {
detectDragGestures(
onDragStart = { onDragStart() },
onDrag = { change, dragAmount ->
change.consume()
onDrag(dragAmount)
},
onDragEnd = onDragEnd,
onDragCancel = onDragEnd
)
}
)
}
}
}
@Composable
private fun PdfToolbarDragRow(
tool: PdfReaderTool,
isDragging: Boolean,
onBounds: (Rect) -> Unit,
onDragStart: (Offset) -> Unit,
onDrag: (Offset) -> Unit,
onDragEnd: () -> Unit
) {
var bounds by remember { mutableStateOf<Rect?>(null) }
androidx.compose.material3.Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.onGloballyPositioned {
bounds = it.boundsInWindow()
onBounds(it.boundsInWindow())
}
.pointerInput(tool) {
detectDragGesturesAfterLongPress(
onDragStart = { onDragStart(bounds?.center ?: Offset.Zero) },
onDragEnd = onDragEnd,
onDragCancel = onDragEnd,
onDrag = { change, dragAmount ->
change.consume()
onDrag(dragAmount)
}
)
},
shape = RoundedCornerShape(12.dp),
color = if (isDragging) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
PdfToolPreviewIcon(tool)
Spacer(Modifier.width(12.dp))
Text(
text = tool.title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Icon(Icons.Default.Menu, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
@Composable
private fun PdfMoreToolVisibilityRow(
title: String,
visible: Boolean,
onToggle: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onToggle)
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
if (visible) {
Icon(Icons.Default.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
}
}
}
enum class PdfToolbarSection(val title: String) {
TOP("Top Bar"),
BOTTOM("Bottom Bar"),
HIDDEN("Hidden Tools")
}
@Composable
private fun PdfToolPreviewIcon(tool: PdfReaderTool) {
when (tool) {
PdfReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.LOCK_PANNING -> Icon(Icons.Default.LockOpen, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.HIGHLIGHT_ALL -> Icon(painterResource(id = R.drawable.highlight_text), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.EDIT_MODE -> Icon(Icons.Default.Edit, contentDescription = tool.title, modifier = Modifier.size(20.dp))
PdfReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = tool.title, modifier = Modifier.size(20.dp))
else -> Icon(Icons.Default.MoreVert, contentDescription = tool.title, modifier = Modifier.size(20.dp))
}
}
@Composable
fun PdfVisualOptionsSheet(
systemUiMode: SystemUiMode,
@ -150,4 +551,4 @@ fun PdfVisualOptionsSheet(
)
}
}
}
}

View file

@ -34,7 +34,9 @@ object PdfToHtmlGenerator {
}
try {
val doc = pdfiumCore.newDocument(pfd)
val doc = PdfiumEngineProvider.withPdfium {
pdfiumCore.newDocument(pfd)
}
val totalPages = doc.getPageCount()
Timber.tag(TAG).d("Document loaded. Total pages: $totalPages")
@ -56,7 +58,9 @@ object PdfToHtmlGenerator {
writer.write(buildGlobalHtmlFooter())
}
doc.close()
PdfiumEngineProvider.withPdfium {
doc.close()
}
pfd.close()
Timber.tag(TAG).d("generateHtmlFile SUCCESS | ${System.currentTimeMillis() - t0}ms")
return@withContext true
@ -137,14 +141,14 @@ object PdfToHtmlGenerator {
val textPagePtr = getNativePointer(textPage)
val imageElements = mutableListOf<ImageElement>()
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
val objCount = PdfiumEngineProvider.bridge.getPageObjectCount(pagePtr)
for (i in 0 until objCount) {
if (NativePdfiumBridge.getPageObjectType(pagePtr, i) == 3) {
if (PdfiumEngineProvider.bridge.getPageObjectType(pagePtr, i) == 3) {
val bbox = FloatArray(4)
if (NativePdfiumBridge.getPageObjectBoundingBox(pagePtr, i, bbox)) {
if (PdfiumEngineProvider.bridge.getPageObjectBoundingBox(pagePtr, i, bbox)) {
val topY = bbox[3]
val dimens = IntArray(2)
val pixels = NativePdfiumBridge.extractImagePixels(pagePtr, i, dimens)
val pixels = PdfiumEngineProvider.bridge.extractImagePixels(pagePtr, i, dimens)
if (pixels != null && dimens[0] > 0 && dimens[1] > 0) {
try {
val bmp = Bitmap.createBitmap(pixels, dimens[0], dimens[1], Bitmap.Config.ARGB_8888)
@ -175,11 +179,11 @@ object PdfToHtmlGenerator {
val flags: IntArray?
val charBoxes: FloatArray?
synchronized(NativePdfiumBridge::class.java) {
sizes = NativePdfiumBridge.getPageFontSizes(textPagePtr, actualCount)
weights = NativePdfiumBridge.getPageFontWeights(textPagePtr, actualCount)
flags = NativePdfiumBridge.getPageFontFlags(textPagePtr, actualCount)
charBoxes = NativePdfiumBridge.getPageCharBoxes(textPagePtr, actualCount)
synchronized(PdfiumEngineProvider.lock) {
sizes = PdfiumEngineProvider.bridge.getPageFontSizes(textPagePtr, actualCount)
weights = PdfiumEngineProvider.bridge.getPageFontWeights(textPagePtr, actualCount)
flags = PdfiumEngineProvider.bridge.getPageFontFlags(textPagePtr, actualCount)
charBoxes = PdfiumEngineProvider.bridge.getPageCharBoxes(textPagePtr, actualCount)
}
if (sizes == null || weights == null || flags == null) {
@ -547,4 +551,4 @@ object PdfToHtmlGenerator {
}
return 0L
}
}
}

View file

@ -18,13 +18,14 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
@ -33,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.sp
import com.aryan.reader.BuildConfig
@ -41,9 +43,23 @@ import com.aryan.reader.R
import com.aryan.reader.SearchState
import com.aryan.reader.SearchTopBar
import com.aryan.reader.TooltipIconButton
import com.aryan.reader.areReaderAiFeaturesEnabled
import com.aryan.reader.epubreader.SystemUiMode
import kotlin.collections.isNotEmpty
private val pdfToolbarTools = setOf(
PdfReaderTool.DICTIONARY,
PdfReaderTool.THEME,
PdfReaderTool.LOCK_PANNING,
PdfReaderTool.SLIDER,
PdfReaderTool.TOC,
PdfReaderTool.SEARCH,
PdfReaderTool.HIGHLIGHT_ALL,
PdfReaderTool.AI_FEATURES,
PdfReaderTool.EDIT_MODE,
PdfReaderTool.TTS_CONTROLS
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun PdfTopBar(
@ -60,6 +76,8 @@ internal fun PdfTopBar(
totalPages: Int,
pagerStatePageCount: Int,
hiddenTools: Set<String>,
toolOrder: List<PdfReaderTool>,
bottomTools: Set<String>,
isScrollLocked: Boolean,
isEditMode: Boolean,
displayMode: DisplayMode,
@ -83,6 +101,16 @@ internal fun PdfTopBar(
onShowCustomizeTools: () -> Unit,
onShowOcrLanguage: () -> Unit,
onShowVisualOptions: () -> Unit,
onShowSlider: () -> Unit,
onShowToc: () -> Unit,
onSearchClick: () -> Unit,
onToggleHighlights: () -> Unit,
onShowAiHub: () -> Unit,
onToggleEditMode: () -> Unit,
onToggleTts: () -> Unit,
isTtsPlayingOrLoading: Boolean,
showAllTextHighlights: Boolean,
isHighlightingLoading: Boolean,
tapToNavigateEnabled: Boolean,
onToggleTapToNavigate: () -> Unit,
onChangeDisplayMode: (DisplayMode) -> Unit,
@ -153,35 +181,89 @@ internal fun PdfTopBar(
modifier = Modifier.padding(start = 12.dp).weight(1f).testTag("PageNumberIndicator")
)
if (!hiddenTools.contains(PdfReaderTool.THEME.name)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_theme),
description = stringResource(R.string.tooltip_theme_desc),
onClick = onShowThemePanel
) {
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant)
toolOrder
.filter { it in pdfToolbarTools && !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
.forEach { tool ->
when (tool) {
PdfReaderTool.THEME -> TooltipIconButton(
text = stringResource(R.string.tooltip_theme),
description = stringResource(R.string.tooltip_theme_desc),
onClick = onShowThemePanel
) {
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.LOCK_PANNING -> TooltipIconButton(
text = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan),
description = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan_desc) else stringResource(R.string.tooltip_lock_pan_desc),
onClick = onToggleScrollLock
) {
Icon(if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen, contentDescription = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.DICTIONARY -> TooltipIconButton(
text = stringResource(R.string.tooltip_dictionary),
description = stringResource(R.string.tooltip_dictionary_desc),
onClick = onShowDictionarySettings
) {
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.SLIDER -> TooltipIconButton(
text = stringResource(R.string.tooltip_slider),
description = stringResource(R.string.tooltip_slider_desc),
onClick = onShowSlider,
enabled = !isTtsPlayingOrLoading
) {
Icon(painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
}
PdfReaderTool.TOC -> TooltipIconButton(
text = stringResource(R.string.tooltip_toc),
description = stringResource(R.string.tooltip_toc_desc),
onClick = onShowToc,
enabled = !isTtsPlayingOrLoading
) {
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
}
PdfReaderTool.SEARCH -> TooltipIconButton(
text = stringResource(R.string.tooltip_search),
description = stringResource(R.string.tooltip_search_desc),
onClick = onSearchClick,
enabled = !isTtsPlayingOrLoading
) {
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
}
PdfReaderTool.HIGHLIGHT_ALL -> TooltipIconButton(
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off) else stringResource(R.string.tooltip_highlights),
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc) else stringResource(R.string.tooltip_highlights_desc),
onClick = onToggleHighlights
) {
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.AI_FEATURES -> if (areReaderAiFeaturesEnabled(LocalContext.current)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_ai),
description = stringResource(R.string.tooltip_ai_desc),
onClick = onShowAiHub
) {
Icon(painterResource(id = R.drawable.ai), contentDescription = stringResource(R.string.tooltip_ai))
}
}
PdfReaderTool.EDIT_MODE -> TooltipIconButton(
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit) else stringResource(R.string.tooltip_edit_mode),
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
onClick = onToggleEditMode
) {
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.TTS_CONTROLS -> TooltipIconButton(
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop) else stringResource(R.string.tooltip_tts_start),
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
onClick = onToggleTts
) {
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
else -> Unit
}
}
}
if (!hiddenTools.contains(PdfReaderTool.LOCK_PANNING.name)) {
TooltipIconButton(
text = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan),
description = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan_desc) else stringResource(R.string.tooltip_lock_pan_desc),
onClick = onToggleScrollLock
) {
Icon(if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen, contentDescription = if (isScrollLocked) stringResource(R.string.tooltip_unlock_pan) else stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
if (!hiddenTools.contains(PdfReaderTool.DICTIONARY.name)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_dictionary),
description = stringResource(R.string.tooltip_dictionary_desc),
onClick = onShowDictionarySettings
) {
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
if (BuildConfig.DEBUG) {
TooltipIconButton(text = stringResource(R.string.tooltip_demo_annotations), onClick = onGenerateDemoAnnotations) {
@ -197,14 +279,25 @@ internal fun PdfTopBar(
Box {
var showMoreMenu by remember { mutableStateOf(false) }
var showHiddenToolsExpanded by remember { mutableStateOf(false) }
TooltipIconButton(
text = stringResource(R.string.tooltip_more_options),
description = stringResource(R.string.tooltip_more_options_desc),
onClick = { showMoreMenu = true }) {
onClick = {
showHiddenToolsExpanded = false
showMoreMenu = true
}) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.tooltip_more_options))
}
DropdownMenu(expanded = showMoreMenu, onDismissRequest = { showMoreMenu = false }) {
DropdownMenu(
expanded = showMoreMenu,
onDismissRequest = {
showHiddenToolsExpanded = false
showMoreMenu = false
}
) {
val hiddenToolbarTools = toolOrder.filter { it in pdfToolbarTools && hiddenTools.contains(it.name) }
DropdownMenuItem(
text = { Text(stringResource(R.string.title_customize_toolbar)) },
onClick = { showMoreMenu = false; onShowCustomizeTools() },
@ -212,6 +305,47 @@ internal fun PdfTopBar(
)
HorizontalDivider()
if (hiddenToolbarTools.isNotEmpty()) {
DropdownMenuItem(
text = { Text("Hidden tools") },
onClick = { showHiddenToolsExpanded = !showHiddenToolsExpanded },
trailingIcon = {
Icon(
Icons.Default.ArrowDropDown,
contentDescription = null,
modifier = Modifier.rotate(if (showHiddenToolsExpanded) 180f else 0f)
)
}
)
if (showHiddenToolsExpanded) {
hiddenToolbarTools.forEach { tool ->
HiddenPdfToolMenuItem(
tool = tool,
isTtsPlayingOrLoading = isTtsPlayingOrLoading,
showAllTextHighlights = showAllTextHighlights,
isHighlightingLoading = isHighlightingLoading,
isEditMode = isEditMode,
isTtsSessionActive = isTtsSessionActive,
closeMenu = {
showHiddenToolsExpanded = false
showMoreMenu = false
},
onShowThemePanel = onShowThemePanel,
onToggleScrollLock = onToggleScrollLock,
onShowDictionarySettings = onShowDictionarySettings,
onShowSlider = onShowSlider,
onShowToc = onShowToc,
onSearchClick = onSearchClick,
onToggleHighlights = onToggleHighlights,
onShowAiHub = onShowAiHub,
onToggleEditMode = onToggleEditMode,
onToggleTts = onToggleTts
)
}
}
HorizontalDivider()
}
if (BuildConfig.IS_PRO && !hiddenTools.contains(PdfReaderTool.OCR_LANGUAGE.name)) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_ocr_language)) },
@ -402,6 +536,72 @@ internal fun PdfTopBar(
}
}
@Composable
private fun HiddenPdfToolMenuItem(
tool: PdfReaderTool,
isTtsPlayingOrLoading: Boolean,
showAllTextHighlights: Boolean,
isHighlightingLoading: Boolean,
isEditMode: Boolean,
isTtsSessionActive: Boolean,
closeMenu: () -> Unit,
onShowThemePanel: () -> Unit,
onToggleScrollLock: () -> Unit,
onShowDictionarySettings: () -> Unit,
onShowSlider: () -> Unit,
onShowToc: () -> Unit,
onSearchClick: () -> Unit,
onToggleHighlights: () -> Unit,
onShowAiHub: () -> Unit,
onToggleEditMode: () -> Unit,
onToggleTts: () -> Unit
) {
val enabled = when (tool) {
PdfReaderTool.SLIDER,
PdfReaderTool.TOC,
PdfReaderTool.SEARCH -> !isTtsPlayingOrLoading
else -> true
}
DropdownMenuItem(
text = { Text(tool.title) },
enabled = enabled,
onClick = {
closeMenu()
when (tool) {
PdfReaderTool.THEME -> onShowThemePanel()
PdfReaderTool.LOCK_PANNING -> onToggleScrollLock()
PdfReaderTool.DICTIONARY -> onShowDictionarySettings()
PdfReaderTool.SLIDER -> onShowSlider()
PdfReaderTool.TOC -> onShowToc()
PdfReaderTool.SEARCH -> onSearchClick()
PdfReaderTool.HIGHLIGHT_ALL -> onToggleHighlights()
PdfReaderTool.AI_FEATURES -> onShowAiHub()
PdfReaderTool.EDIT_MODE -> onToggleEditMode()
PdfReaderTool.TTS_CONTROLS -> onToggleTts()
else -> Unit
}
},
leadingIcon = {
when (tool) {
PdfReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.LOCK_PANNING -> Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.HIGHLIGHT_ALL -> {
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(20.dp))
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = null, modifier = Modifier.size(20.dp), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = null, modifier = Modifier.size(20.dp))
PdfReaderTool.EDIT_MODE -> Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(20.dp), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
PdfReaderTool.TTS_CONTROLS -> Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = null, modifier = Modifier.size(20.dp), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
else -> Icon(Icons.Default.MoreVert, contentDescription = null, modifier = Modifier.size(20.dp))
}
}
)
}
@Composable
fun ReflowProgressOverlay(
modifier: Modifier = Modifier,
@ -447,6 +647,89 @@ fun ReflowProgressOverlay(
}
}
@Composable
fun PdfJumpHistoryBar(
modifier: Modifier = Modifier,
showStandardBars: Boolean,
searchStateActive: Boolean,
backPage: Int?,
forwardPage: Int?,
onBack: () -> Unit,
onForward: () -> Unit,
onClear: () -> Unit
) {
AnimatedVisibility(
visible = showStandardBars && !searchStateActive && (backPage != null || forwardPage != null),
enter = slideInVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeIn(animationSpec = tween(200)),
exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200)),
modifier = modifier
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceContainer,
tonalElevation = 3.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(
onClick = onBack,
enabled = backPage != null,
modifier = Modifier.weight(1f)
) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.content_desc_jump_back),
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
Text(
text = backPage?.let { stringResource(R.string.pdf_page_short, it + 1) } ?: "",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
TextButton(
onClick = onClear,
modifier = Modifier.weight(1f)
) {
Icon(
Icons.Default.Close,
contentDescription = stringResource(R.string.action_clear),
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.action_clear), maxLines = 1)
}
TextButton(
onClick = onForward,
enabled = forwardPage != null,
modifier = Modifier.weight(1f)
) {
Text(
text = forwardPage?.let { stringResource(R.string.pdf_page_short, it + 1) } ?: "",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(Modifier.width(4.dp))
Icon(
Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = stringResource(R.string.content_desc_jump_forward),
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
@Composable
fun PdfBottomBar(
modifier: Modifier = Modifier,
@ -455,14 +738,17 @@ fun PdfBottomBar(
systemUiMode: SystemUiMode,
navBarHeightDp: Dp,
hiddenTools: Set<String>,
toolOrder: List<PdfReaderTool>,
bottomTools: Set<String>,
isTtsPlayingOrLoading: Boolean,
showAllTextHighlights: Boolean,
isHighlightingLoading: Boolean,
isEditMode: Boolean,
isTtsSessionActive: Boolean,
ttsErrorMessage: String?,
jumpBackPage: Int?,
onJumpBack: () -> Unit,
onShowThemePanel: () -> Unit,
onToggleScrollLock: () -> Unit,
onShowDictionarySettings: () -> Unit,
onShowSlider: () -> Unit,
onShowToc: () -> Unit,
onSearchClick: () -> Unit,
@ -490,106 +776,91 @@ fun PdfBottomBar(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
if (jumpBackPage != null) {
TooltipIconButton(
text = stringResource(R.string.action_jump_back_to_page, jumpBackPage + 1),
description = stringResource(R.string.desc_return_to_previous_page),
onClick = onJumpBack
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.AutoMirrored.Filled.Undo,
contentDescription = stringResource(R.string.content_desc_jump_back),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp)
)
Text(
text = "${jumpBackPage + 1}",
fontSize = 10.sp,
lineHeight = 10.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
toolOrder
.filter { it in pdfToolbarTools && bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
.forEach { tool ->
when (tool) {
PdfReaderTool.THEME -> TooltipIconButton(
text = stringResource(R.string.tooltip_theme),
description = stringResource(R.string.tooltip_theme_desc),
onClick = onShowThemePanel
) {
Icon(painterResource(id = R.drawable.palette), contentDescription = stringResource(R.string.tooltip_theme_desc), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.LOCK_PANNING -> TooltipIconButton(
text = stringResource(R.string.tooltip_lock_pan),
description = stringResource(R.string.tooltip_lock_pan_desc),
onClick = onToggleScrollLock
) {
Icon(Icons.Default.LockOpen, contentDescription = stringResource(R.string.tooltip_lock_pan), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.DICTIONARY -> TooltipIconButton(
text = stringResource(R.string.tooltip_dictionary),
description = stringResource(R.string.tooltip_dictionary_desc),
onClick = onShowDictionarySettings
) {
Icon(painterResource(id = R.drawable.dictionary), contentDescription = stringResource(R.string.content_desc_dictionary_settings), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.SLIDER -> TooltipIconButton(
text = stringResource(R.string.tooltip_slider),
description = stringResource(R.string.tooltip_slider_desc),
onClick = onShowSlider,
enabled = !isTtsPlayingOrLoading
) {
Icon(painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
}
PdfReaderTool.TOC -> TooltipIconButton(
text = stringResource(R.string.tooltip_toc),
description = stringResource(R.string.tooltip_toc_desc),
onClick = onShowToc,
enabled = !isTtsPlayingOrLoading,
modifier = Modifier.testTag("TocButton")
) {
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
}
PdfReaderTool.SEARCH -> TooltipIconButton(
text = stringResource(R.string.tooltip_search),
description = stringResource(R.string.tooltip_search_desc),
onClick = onSearchClick,
enabled = !isTtsPlayingOrLoading,
modifier = Modifier.testTag("SearchButton")
) {
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
}
PdfReaderTool.HIGHLIGHT_ALL -> TooltipIconButton(
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off) else stringResource(R.string.tooltip_highlights),
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc) else stringResource(R.string.tooltip_highlights_desc),
onClick = onToggleHighlights
) {
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.AI_FEATURES -> if (areReaderAiFeaturesEnabled(LocalContext.current)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_ai),
description = stringResource(R.string.tooltip_ai_desc),
onClick = onShowAiHub
) {
Icon(painterResource(id = R.drawable.ai), contentDescription = stringResource(R.string.tooltip_ai))
}
}
PdfReaderTool.EDIT_MODE -> TooltipIconButton(
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit) else stringResource(R.string.tooltip_edit_mode),
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
onClick = onToggleEditMode
) {
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
PdfReaderTool.TTS_CONTROLS -> TooltipIconButton(
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop) else stringResource(R.string.tooltip_tts_start),
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
onClick = onToggleTts
) {
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
else -> Unit
}
}
}
if (!hiddenTools.contains(PdfReaderTool.SLIDER.name)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_slider),
description = stringResource(R.string.tooltip_slider_desc),
onClick = onShowSlider,
enabled = !isTtsPlayingOrLoading
) {
Icon(painterResource(id = R.drawable.slider), contentDescription = stringResource(R.string.content_desc_navigate_slider))
}
}
if (!hiddenTools.contains(PdfReaderTool.TOC.name)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_toc),
description = stringResource(R.string.tooltip_toc_desc),
onClick = onShowToc,
enabled = !isTtsPlayingOrLoading,
modifier = Modifier.testTag("TocButton")
) {
Icon(Icons.Default.Menu, contentDescription = stringResource(R.string.content_desc_table_of_contents))
}
}
if (!hiddenTools.contains(PdfReaderTool.SEARCH.name)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_search),
description = stringResource(R.string.tooltip_search_desc),
onClick = onSearchClick,
enabled = !isTtsPlayingOrLoading,
modifier = Modifier.testTag("SearchButton")
) {
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
}
}
if (!hiddenTools.contains(PdfReaderTool.HIGHLIGHT_ALL.name)) {
TooltipIconButton(
text = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off) else stringResource(R.string.tooltip_highlights),
description = if (showAllTextHighlights) stringResource(R.string.tooltip_highlights_off_desc) else stringResource(R.string.tooltip_highlights_desc),
onClick = onToggleHighlights
) {
if (isHighlightingLoading) CircularProgressIndicator(Modifier.size(24.dp))
else Icon(painterResource(id = R.drawable.highlight_text), contentDescription = stringResource(R.string.content_desc_highlight_all_text), tint = if (showAllTextHighlights) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
}
if (BuildConfig.FLAVOR != "oss" && !hiddenTools.contains(PdfReaderTool.AI_FEATURES.name)) {
TooltipIconButton(
text = stringResource(R.string.tooltip_ai),
description = stringResource(R.string.tooltip_ai_desc),
onClick = onShowAiHub
) {
Icon(painterResource(id = R.drawable.ai), contentDescription = stringResource(R.string.tooltip_ai))
}
}
if (!hiddenTools.contains(PdfReaderTool.EDIT_MODE.name)) {
TooltipIconButton(
text = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit) else stringResource(R.string.tooltip_edit_mode),
description = if (isEditMode) stringResource(R.string.tooltip_edit_mode_exit_desc) else stringResource(R.string.tooltip_edit_mode_desc),
onClick = onToggleEditMode
) {
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.content_desc_toggle_editing_mode), tint = if (isEditMode) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
}
if (!hiddenTools.contains(PdfReaderTool.TTS_CONTROLS.name)) {
TooltipIconButton(
text = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop) else stringResource(R.string.tooltip_tts_start),
description = if (isTtsSessionActive) stringResource(R.string.tooltip_tts_stop_desc) else stringResource(R.string.tooltip_tts_start_desc),
onClick = onToggleTts
) {
Icon(if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech), contentDescription = if (isTtsSessionActive) stringResource(R.string.content_desc_stop_tts) else stringResource(R.string.content_desc_start_tts), tint = if (isTtsSessionActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
}
if (BuildConfig.FLAVOR != "oss") {
TooltipIconButton(

View file

@ -193,6 +193,7 @@ internal fun PdfVerticalReader(
state: VerticalPdfReaderState,
pdfDocument: StableHolder<ReaderDocument>,
activeTheme: com.aryan.reader.ReaderTheme,
activeTextureAlpha: Float = 0.55f,
excludeImages: Boolean = false,
totalPages: Int,
virtualPages: List<VirtualPage> = emptyList(),
@ -988,7 +989,7 @@ internal fun PdfVerticalReader(
}
val buttons = currentEvent.buttons
Timber.tag("StylusEraserDiagnostic").d(
Timber.tag("StylusDebug").d(
"VerticalReader | Type: ${down.type} | isPrimary: ${buttons.isPrimaryPressed} | isSecondary: ${buttons.isSecondaryPressed} | isTertiary: ${buttons.isTertiaryPressed} | buttonsString: $buttons"
)
@ -1688,6 +1689,7 @@ internal fun PdfVerticalReader(
virtualPage = virtualPage,
totalPages = totalPages,
activeTheme = activeTheme,
activeTextureAlpha = activeTextureAlpha,
excludeImages = excludeImages,
externalScale = highResScale,
onScaleChanged = {},

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,120 @@
package com.aryan.reader.pdf
import com.aryan.reader.shared.pdf.PdfiumBridge
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal object PdfiumEngineProvider {
private val pdfiumMutex = Mutex()
val bridge: PdfiumBridge
get() = AndroidPdfiumBridge
val lock: Any = this
suspend fun <T> withPdfium(block: suspend () -> T): T =
pdfiumMutex.withLock { block() }
fun <T> withPdfiumBlocking(block: () -> T): T =
runBlocking {
pdfiumMutex.withLock { block() }
}
}
private object AndroidPdfiumBridge : PdfiumBridge {
override fun getFontSize(textPagePtr: Long, index: Int): Double =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getFontSize(textPagePtr, index)
}
override fun getFontWeight(textPagePtr: Long, index: Int): Int =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getFontWeight(textPagePtr, index)
}
override fun getPageFontSizes(textPagePtr: Long, count: Int): FloatArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageFontSizes(textPagePtr, count)
}
override fun getPageFontWeights(textPagePtr: Long, count: Int): IntArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageFontWeights(textPagePtr, count)
}
override fun getPageFontFlags(textPagePtr: Long, count: Int): IntArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageFontFlags(textPagePtr, count)
}
override fun getPageCharBoxes(textPagePtr: Long, count: Int): FloatArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageCharBoxes(textPagePtr, count)
}
override fun getAnnotCount(pagePtr: Long): Int =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getAnnotCount(pagePtr)
}
override fun getAnnotSubtype(pagePtr: Long, index: Int): Int =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getAnnotSubtype(pagePtr, index)
}
override fun getAnnotRect(pagePtr: Long, index: Int): FloatArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getAnnotRect(pagePtr, index)
}
override fun getAnnotString(pagePtr: Long, index: Int, key: String): String? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getAnnotString(pagePtr, index, key)
}
override fun getPageObjectCount(pagePtr: Long): Int =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageObjectCount(pagePtr)
}
override fun getPageObjectType(pagePtr: Long, index: Int): Int =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageObjectType(pagePtr, index)
}
override fun getPageObjectBoundingBox(pagePtr: Long, index: Int, outRect: FloatArray): Boolean =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getPageObjectBoundingBox(pagePtr, index, outRect)
}
override fun extractImagePixels(pagePtr: Long, index: Int, dimens: IntArray): IntArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.extractImagePixels(pagePtr, index, dimens)
}
override fun performClick(pagePtr: Long, x: Double, y: Double): Boolean =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.performClick(pagePtr, x, y)
}
override fun getLinkInfoAtPoint(docPtr: Long, pagePtr: Long, x: Double, y: Double): String? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getLinkInfoAtPoint(docPtr, pagePtr, x, y)
}
override fun getAnnotSubtypeAtPoint(pagePtr: Long, x: Double, y: Double): Int =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getAnnotSubtypeAtPoint(pagePtr, x, y)
}
override fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray? =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.getAnnotRectAtPoint(pagePtr, x, y)
}
override fun checkActionSupport(): Boolean =
PdfiumEngineProvider.withPdfiumBlocking {
NativePdfiumBridge.checkActionSupport()
}
}

View file

@ -28,6 +28,7 @@ import me.zhanghai.android.libarchive.ArchiveException
import okhttp3.Request
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.ZipFile
import androidx.core.graphics.createBitmap
@ -88,42 +89,97 @@ object DocumentFactory {
ArchiveDocumentWrapper(cacheFile)
} else {
val pfd = context.contentResolver.openFileDescriptor(uri, "r") ?: throw Exception("Failed to open PDF")
PdfDocumentWrapper(pdfiumCore.newDocument(pfd, password))
PdfDocumentWrapper(PdfiumEngineProvider.withPdfium { pdfiumCore.newDocument(pfd, password) })
}
}
}
// ================= PDF IMPLEMENTATION =================
private inline fun closePdfiumResource(tag: String, closeBlock: () -> Unit) {
try {
closeBlock()
} catch (e: IllegalStateException) {
if (e.message == "Already closed") {
Timber.tag(tag).d(e, "Ignoring duplicate Pdfium close")
} else {
throw e
}
}
}
class PdfDocumentWrapper(val pdfDocument: PdfDocumentKt) : ReaderDocument {
override suspend fun getPageCount() = pdfDocument.getPageCount()
private val isClosed = AtomicBoolean(false)
override suspend fun getPageCount() = PdfiumEngineProvider.withPdfium {
pdfDocument.getPageCount()
}
override suspend fun openPage(pageIndex: Int): ReaderPage? {
val page = pdfDocument.openPage(pageIndex) ?: return null
if (isClosed.get()) return null
val page = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) null else pdfDocument.openPage(pageIndex)
} ?: return null
return PdfPageWrapper(page)
}
override suspend fun getTableOfContents() = pdfDocument.getFixedTableOfContents()
override fun close() { pdfDocument.close() }
override suspend fun getTableOfContents() = PdfiumEngineProvider.withPdfium {
pdfDocument.getFixedTableOfContents()
}
override fun close() {
if (!isClosed.compareAndSet(false, true)) return
PdfiumEngineProvider.withPdfiumBlocking {
closePdfiumResource("PdfDocumentWrapper") { pdfDocument.close() }
}
}
}
class PdfPageWrapper(val pdfPage: PdfPageKt) : ReaderPage {
override suspend fun getPageWidthPoint() = pdfPage.getPageWidthPoint()
override suspend fun getPageHeightPoint() = pdfPage.getPageHeightPoint()
override suspend fun getPageRotation() = pdfPage.getPageRotation()
private val isClosed = AtomicBoolean(false)
override suspend fun getPageWidthPoint() = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else pdfPage.getPageWidthPoint()
}
override suspend fun getPageHeightPoint() = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else pdfPage.getPageHeightPoint()
}
override suspend fun getPageRotation() = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else pdfPage.getPageRotation()
}
override suspend fun renderPageBitmap(bitmap: Bitmap, startX: Int, startY: Int, drawSizeX: Int, drawSizeY: Int, renderAnnot: Boolean) {
pdfPage.renderPageBitmap(bitmap, startX, startY, drawSizeX, drawSizeY, renderAnnot)
PdfiumEngineProvider.withPdfium {
if (!isClosed.get()) {
pdfPage.renderPageBitmap(bitmap, startX, startY, drawSizeX, drawSizeY, renderAnnot)
}
}
}
override suspend fun mapRectToDevice(startX: Int, startY: Int, sizeX: Int, sizeY: Int, rotate: Int, coords: RectF) =
pdfPage.mapRectToDevice(startX, startY, sizeX, sizeY, rotate, coords)
PdfiumEngineProvider.withPdfium {
if (isClosed.get()) Rect() else pdfPage.mapRectToDevice(startX, startY, sizeX, sizeY, rotate, coords)
}
override suspend fun mapDeviceCoordsToPage(startX: Int, startY: Int, sizeX: Int, sizeY: Int, rotate: Int, deviceX: Int, deviceY: Int) =
pdfPage.mapDeviceCoordsToPage(startX, startY, sizeX, sizeY, rotate, deviceX, deviceY)
PdfiumEngineProvider.withPdfium {
if (isClosed.get()) PointF() else pdfPage.mapDeviceCoordsToPage(startX, startY, sizeX, sizeY, rotate, deviceX, deviceY)
}
override suspend fun openTextPage(): ReaderTextPage = PdfTextPageWrapper(pdfPage.openTextPage())
override suspend fun openTextPage(): ReaderTextPage = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) DummyTextPage() else PdfTextPageWrapper(pdfPage.openTextPage())
}
override suspend fun getLinks(): List<ReaderLink> {
return pdfPage.getPageLinks().map { ReaderLink(it.uri, it.destPageIdx, it.bounds) }
return PdfiumEngineProvider.withPdfium {
if (isClosed.get()) {
emptyList()
} else {
pdfPage.getPageLinks().map { ReaderLink(it.uri, it.destPageIdx, it.bounds) }
}
}
}
override fun getNativePointer(): Long {
@ -159,29 +215,80 @@ class PdfPageWrapper(val pdfPage: PdfPageKt) : ReaderPage {
return 0L
}
override fun close() { pdfPage.close() }
override fun close() {
if (!isClosed.compareAndSet(false, true)) return
PdfiumEngineProvider.withPdfiumBlocking {
closePdfiumResource("PdfPageWrapper") { pdfPage.close() }
}
}
}
class PdfTextPageWrapper(private val textPage: PdfTextPageKt) : ReaderTextPage {
override suspend fun textPageCountChars() = textPage.textPageCountChars()
override suspend fun textPageGetText(startIndex: Int, count: Int) = textPage.textPageGetText(startIndex, count)
override suspend fun textPageGetRectsForRanges(ranges: IntArray) = textPage.textPageGetRectsForRanges(ranges)?.map { ReaderTextRect(it.rect) }
override suspend fun textPageGetCharIndexAtPos(x: Double, y: Double, xTolerance: Double, yTolerance: Double) = textPage.textPageGetCharIndexAtPos(x, y, xTolerance, yTolerance)
override suspend fun textPageGetCharBox(index: Int) = textPage.textPageGetCharBox(index)
override suspend fun textPageGetUnicode(index: Int): Int {
return textPage.textPageGetUnicode(index).code
private val isClosed = AtomicBoolean(false)
override suspend fun textPageCountChars() = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else textPage.textPageCountChars()
}
override suspend fun loadWebLink(): ReaderWebLinks? {
val links = textPage.loadWebLink() ?: return null
return object : ReaderWebLinks {
override suspend fun countWebLinks() = links.countWebLinks()
override suspend fun getURL(linkIndex: Int, maxLength: Int) = links.getURL(linkIndex, maxLength)
override suspend fun countRects(linkIndex: Int) = links.countRects(linkIndex)
override suspend fun getRect(linkIndex: Int, rectIndex: Int) = links.getRect(linkIndex, rectIndex)
override fun close() { links.close() }
override suspend fun textPageGetText(startIndex: Int, count: Int) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) null else textPage.textPageGetText(startIndex, count)
}
override suspend fun textPageGetRectsForRanges(ranges: IntArray) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) null else textPage.textPageGetRectsForRanges(ranges)?.map { ReaderTextRect(it.rect) }
}
override suspend fun textPageGetCharIndexAtPos(x: Double, y: Double, xTolerance: Double, yTolerance: Double) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) -1 else textPage.textPageGetCharIndexAtPos(x, y, xTolerance, yTolerance)
}
override suspend fun textPageGetCharBox(index: Int) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) null else textPage.textPageGetCharBox(index)
}
override suspend fun textPageGetUnicode(index: Int): Int {
return PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else textPage.textPageGetUnicode(index).code
}
}
override suspend fun loadWebLink(): ReaderWebLinks? {
val links = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) null else textPage.loadWebLink()
} ?: return null
return object : ReaderWebLinks {
private val isClosed = AtomicBoolean(false)
override suspend fun countWebLinks() = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else links.countWebLinks()
}
override suspend fun getURL(linkIndex: Int, maxLength: Int) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) null else links.getURL(linkIndex, maxLength)
}
override suspend fun countRects(linkIndex: Int) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) 0 else links.countRects(linkIndex)
}
override suspend fun getRect(linkIndex: Int, rectIndex: Int) = PdfiumEngineProvider.withPdfium {
if (isClosed.get()) RectF() else links.getRect(linkIndex, rectIndex)
}
override fun close() {
if (!isClosed.compareAndSet(false, true)) return
PdfiumEngineProvider.withPdfiumBlocking {
closePdfiumResource("PdfWebLinksWrapper") { links.close() }
}
}
}
}
override fun close() {
if (!isClosed.compareAndSet(false, true)) return
PdfiumEngineProvider.withPdfiumBlocking {
closePdfiumResource("PdfTextPageWrapper") { textPage.close() }
}
}
override fun close() { textPage.close() }
}
// ================= CBZ, CBR, CB7 IMPLEMENTATION =================

View file

@ -34,6 +34,7 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.flatMap
import com.aryan.reader.SearchResult
import com.aryan.reader.pdf.PdfiumEngineProvider
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@ -171,13 +172,15 @@ class PdfTextRepository(context: Context) {
var ocrUsed = false
try {
document.openPage(pageIndex)?.use { page ->
page.openTextPage().use { textPage ->
val count = textPage.textPageCountChars()
if (count > 0) {
val nativeText = textPage.textPageGetText(0, count)
if (!nativeText.isNullOrBlank()) {
text = nativeText
PdfiumEngineProvider.withPdfium {
document.openPage(pageIndex)?.use { page ->
page.openTextPage().use { textPage ->
val count = textPage.textPageCountChars()
if (count > 0) {
val nativeText = textPage.textPageGetText(0, count)
if (!nativeText.isNullOrBlank()) {
text = nativeText
}
}
}
}
@ -187,26 +190,35 @@ class PdfTextRepository(context: Context) {
}
if (text.isBlank()) {
var bitmap: android.graphics.Bitmap? = null
try {
document.openPage(pageIndex)?.use { page ->
val targetWidth = 1080
val ptrWidth = page.getPageWidthPoint()
val ptrHeight = page.getPageHeightPoint()
PdfiumEngineProvider.withPdfium {
document.openPage(pageIndex)?.use { page ->
val targetWidth = 1080
val ptrWidth = page.getPageWidthPoint()
val ptrHeight = page.getPageHeightPoint()
if (ptrWidth > 0 && ptrHeight > 0) {
val aspectRatio = ptrWidth.toFloat() / ptrHeight.toFloat()
val targetHeight = (targetWidth / aspectRatio).toInt().coerceAtLeast(1)
if (ptrWidth > 0 && ptrHeight > 0) {
val aspectRatio = ptrWidth.toFloat() / ptrHeight.toFloat()
val targetHeight = (targetWidth / aspectRatio).toInt().coerceAtLeast(1)
val bitmap = createBitmap(targetWidth, targetHeight)
page.renderPageBitmap(bitmap, 0, 0, targetWidth, targetHeight, false)
val visionText = OcrHelper.extractTextFromBitmap(bitmap, onOcrModelDownloading)
bitmap = createBitmap(targetWidth, targetHeight)
page.renderPageBitmap(bitmap!!, 0, 0, targetWidth, targetHeight, false)
}
}
}
bitmap?.let {
try {
val visionText = OcrHelper.extractTextFromBitmap(it, onOcrModelDownloading)
text = visionText?.text ?: ""
bitmap.recycle()
ocrUsed = true
} finally {
it.recycle()
bitmap = null
}
}
} catch (e: Exception) {
bitmap?.recycle()
Timber.tag(TAG).e(e, "OCR failed for page $pageIndex")
}
}
@ -270,11 +282,13 @@ class PdfTextRepository(context: Context) {
suspend fun hasNativeText(document: PdfDocumentKt, pageIndex: Int): Boolean {
return withContext(Dispatchers.IO) {
try {
document.openPage(pageIndex)?.use { page ->
page.openTextPage().use { textPage ->
textPage.textPageCountChars() > 0
}
} ?: false
PdfiumEngineProvider.withPdfium {
document.openPage(pageIndex)?.use { page ->
page.openTextPage().use { textPage ->
textPage.textPageCountChars() > 0
}
} ?: false
}
} catch (_: Exception) {
false
}
@ -585,4 +599,4 @@ class PdfTextRepository(context: Context) {
return null
}
}
}