Desktop app (#308)

* Implement build profiles and feature policy for offline desktop builds

* Introduce unified cross-platform Settings Hub

* Refactor main settings into a hierarchical page-based navigation model

* Refactor library projection to use shared multiplatform logic

* Refactor UI state consumption by removing intermediate screen models

* Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic

* Refactor state management for tabs, selection, and pinning to use shared bridge logic

* Refactor file type management and validation into a centralized shared module

* Centralize file type resolution and improve handling of unknown types

* Centralize book import logic with SharedImportPlanner

* Refactor magnifier geometry logic and coordinate mapping

* Properly handle orientation changes in scroll-locked PDF reader

* Add screen orientation controls to EPUB and PDF readers

* Implement right-to-left (RTL) pagination support and refactor reader menus

* Separate right-to-left pagination settings for PDF and EPUB

* Ensure PDF page data is scoped by document key for multi tab support

* Implement theme-aware link styling for the epub reader

* Implement jump history for back and forward navigation in the epub reader

* Improve locator handling and navigation logic in paginated reader mode

* Implement stable pagination navigation and location tracking

* Centralize banner message management and auto-dismiss logic in MainViewModel

* Implement zoom and pan state preservation for PDF pan lock mode

* Enhance reader navigation UI and workspace layout management in desktop app

* Refactor reader navigation sidebar and relocate search controls in desktop app

* Enhance reader UI with redesigned selection menus and bottom sheet overlays

* Implement custom highlight palettes and reader theme customization in desktop app

* Implement cross-platform modal layer and refine reader UI styling

* Improve highlight accuracy and implement metadata enrichment on book open in desktop app

* Implement two-page spread layout for paginated reader on desktop

* Implement persistent caching for book loading and pagination in desktop app

* Implement persistent caching for book loading and pagination in desktop app

* Optimize reader settings updates by separating layout and appearance changes in desktop app

* Improve desktop window branding and native Windows styling

* Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app

* Refine selection handle positioning and interaction logic

* Implement EPUB selection debug logging and improve handle targeting

* Optimize desktop book loading performance and UI responsiveness

* Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer.

* Implement smooth zoom preview for the PDF reader in desktop app

* Optimize PDF rendering performance and responsiveness in the desktop reader

* Implement conditional diagnostic logging and update desktop build configuration

* Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling

* Added management options for annotations and highlights in the sidebar in desktop app

* Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability.

* Refined library filters and enhanced OPDS functionality in desktop app

* Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app

* Added PPTX support including document parsing, rendering, and indexing

* Improved PPTX rendering and layout accuracy

* Implemented text autofit support for PPTX rendering

* Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity

* Improved EPUB pagination accuracy and added layout telemetry in desktop app

* Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app

* Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling

* Implemented billing account obfuscation

* Implemented hierarchical folder navigation and improved library selection functionality in desktop app

* Implemented platform-aware directory resolution and multi-platform native library support for desktop

* Added full-screen mode for the reader workspace

* Added PDF zoom indicator and interactive vertical scrollbar with page tooltips

* Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management

* Updated PDF indexing to replace existing page text and removed search result item keys

* Implemented "preparing" foreground notification for TTS service

* Optimized PDF rendering performance by pre-calculating page-specific annotations

* Refactored desktop packaging tasks and improved distribution configuration

* Optimized EPUB parser memory usage and added path traversal protection

* Refactored WorkManager monitoring logic and added work pruning

* Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks

* Implemented bitmap size limits and scaling to prevent canvas rendering errors

* Split long text paragraphs into multiple semantic blocks during HTML parsing

* Implemented local ActionMode for text selection to prevent platform crashes

* Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic

* Added desktop startup splash screen and deferred WebView initialization

* Reorganized settings hub and added separate PDF reader defaults

* Implemented embedded cover extraction and metadata support for MOBI and FB2 formats

* Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance.

* Implemented procedurally generated book covers and replaced static placeholders

* Redesigned search UI with a top bar and results overlay in desktop app

* Added PDF page gap and overlay visibility options and implemented DesktopBookImporter

* Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop

* Implemented PDF viewport persistence for zoom and scroll positions in desktop app

* Improved desktop fullscreen implementation and state restoration

* Implemented desktop window state persistence

* Implemented flavor-based branding and ProGuard configuration for desktop builds

* Implemented precise reader positioning and improved highlight rendering logic in desktop app

* Added support for user-editable book metadata

* Enhanced book metadata support and integrated info/edit dialogs

* Implemented embedded EPUB metadata editing

* Improved highlight mapping and added custom scrollbar styling for the reader.

* Reduced desktop WebView bundle size by excluding unused locales and runtime files

* Added neutral pan mode as the default PDF interaction state.

* Refactored library empty states and updated primary navigation tabs in desktop app

* Implemented native paginated reader and unified content rendering architecture in desktop epub reader

* Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing.

* Improved pagination overflow detection in desktop

* Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
This commit is contained in:
Aryan 2026-05-15 22:36:51 +05:30 committed by GitHub
parent c0d0e57e79
commit b20ade9946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
247 changed files with 43321 additions and 7087 deletions

View file

@ -123,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.isCanvasSafeBitmap
import com.aryan.reader.loadReaderTextureBitmap
import com.aryan.reader.ml.SpeechBubble
import com.aryan.reader.pdf.data.PdfAnnotation
@ -135,7 +136,6 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.conflate
@ -171,6 +171,47 @@ enum class InkType {
PEN, HIGHLIGHTER, HIGHLIGHTER_ROUND, ERASER, FOUNTAIN_PEN, PENCIL, TEXT
}
internal fun shouldReportPdfPageCamera(
isZoomEnabled: Boolean,
isVerticalScroll: Boolean,
isScrollLocked: Boolean,
lockedState: Triple<Float, Float, Float>?,
hasAppliedLockedState: Boolean
): Boolean {
return !isZoomEnabled ||
isVerticalScroll ||
!isScrollLocked ||
lockedState == null ||
hasAppliedLockedState
}
internal fun initialPdfPageCamera(
isZoomEnabled: Boolean,
isVerticalScroll: Boolean,
isScrollLocked: Boolean,
lockedState: Triple<Float, Float, Float>?
): Pair<Float, Offset> {
return if (isZoomEnabled && !isVerticalScroll && isScrollLocked && lockedState != null) {
lockedState.first to Offset(lockedState.second, lockedState.third)
} else {
1f to Offset.Zero
}
}
internal fun shouldResetPdfZoomAfterBubbleZoomCleanup(
isBubbleZoomModeActive: Boolean,
scale: Float,
isVerticalScroll: Boolean,
isZoomEnabled: Boolean,
isScrollLocked: Boolean
): Boolean {
return !isBubbleZoomModeActive &&
scale > 1f &&
!isVerticalScroll &&
isZoomEnabled &&
!isScrollLocked
}
data class EmbeddedAnnotation(
val index: Int,
val subtype: Int,
@ -186,8 +227,25 @@ 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)
internal fun pdfRenderPageId(documentKey: String, pageIndex: Int, virtualPage: VirtualPage?): String {
val sourcePageId = when (virtualPage) {
is VirtualPage.BlankPage -> "BLANK_${virtualPage.id}"
is VirtualPage.PdfPage -> "PDF_${virtualPage.pdfIndex}"
null -> "PDF_$pageIndex"
}
return "$documentKey:$sourcePageId"
}
private fun Throwable.readablePdfErrorDetail(): String {
return localizedMessage?.takeIf { it.isNotBlank() }
?: javaClass.simpleName.takeIf { it.isNotBlank() }
?: "Unknown error"
}
private const val PDF_TILE_SIZE_DP = 256
private const val PDF_MAX_TILE_BITMAP_SIZE_PX = 3072
private const val PDF_MAX_DRAW_BITMAP_BYTES = 64L * 1024L * 1024L
private const val PDF_MAX_DRAW_BITMAP_DIMENSION_PX = 4096
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
@ -255,17 +313,22 @@ private suspend fun renderExpandedBubbleBitmap(
}
document.openPage(pageIndex)?.use { page ->
val cropWidth = (bubbleBounds.width() * renderScale).roundToInt().coerceAtLeast(1)
val cropHeight = (bubbleBounds.height() * renderScale).roundToInt().coerceAtLeast(1)
val safeRenderScale = safePdfBitmapRenderScale(
contentWidth = bubbleBounds.width(),
contentHeight = bubbleBounds.height(),
requestedScale = renderScale
)
val cropWidth = (bubbleBounds.width() * safeRenderScale).roundToInt().coerceAtLeast(1)
val cropHeight = (bubbleBounds.height() * safeRenderScale).roundToInt().coerceAtLeast(1)
val bitmap = createBitmap(cropWidth, cropHeight)
try {
page.renderPageBitmap(
bitmap = bitmap,
startX = (-bubbleBounds.left * renderScale).roundToInt(),
startY = (-bubbleBounds.top * renderScale).roundToInt(),
drawSizeX = (pageWidth * renderScale).roundToInt().coerceAtLeast(cropWidth),
drawSizeY = (pageHeight * renderScale).roundToInt().coerceAtLeast(cropHeight),
startX = (-bubbleBounds.left * safeRenderScale).roundToInt(),
startY = (-bubbleBounds.top * safeRenderScale).roundToInt(),
drawSizeX = (pageWidth * safeRenderScale).roundToInt().coerceAtLeast(cropWidth),
drawSizeY = (pageHeight * safeRenderScale).roundToInt().coerceAtLeast(cropHeight),
renderAnnot = true
)
bitmap
@ -277,6 +340,23 @@ private suspend fun renderExpandedBubbleBitmap(
}
}
private fun safePdfBitmapRenderScale(
contentWidth: Float,
contentHeight: Float,
requestedScale: Float
): Float {
if (contentWidth <= 0f || contentHeight <= 0f || requestedScale <= 0f) return 1f
val requestedWidth = contentWidth * requestedScale
val requestedHeight = contentHeight * requestedScale
val requestedBytes = requestedWidth.toDouble() * requestedHeight.toDouble() * 4.0
val byteScale = sqrt(PDF_MAX_DRAW_BITMAP_BYTES.toDouble() / requestedBytes.coerceAtLeast(1.0))
val dimensionScale = PDF_MAX_DRAW_BITMAP_DIMENSION_PX.toDouble() /
max(requestedWidth, requestedHeight).toDouble().coerceAtLeast(1.0)
val limiter = min(1.0, min(byteScale, dimensionScale)).coerceAtLeast(0.01)
return (requestedScale.toDouble() * limiter).coerceAtLeast(0.01).toFloat()
}
object PdfInkGeometry {
fun calculateFountainPenPoints(
points: List<PdfPoint>, baseWidth: Float, pageWidth: Float, pageHeight: Float
@ -380,16 +460,15 @@ internal object PdfBitmapPool {
fun get(size: Int): Bitmap = get(size, size)
fun recycle(bitmap: Bitmap) {
// Overflow bitmaps are left for GC; HWUI may still reference recently drawn bitmaps.
if (!bitmap.isRecycled && pool.size < MAX_POOL_SIZE) {
pool.offer(bitmap)
} else {
bitmap.recycle()
}
}
fun clear() {
while (!pool.isEmpty()) {
pool.poll()?.recycle()
pool.poll()
}
}
}
@ -400,20 +479,20 @@ internal object PdfThumbnailCache {
private data class CacheEntry(val bitmap: Bitmap, val sizeKb: Int)
private val memoryCache = object : LruCache<Int, CacheEntry>(cacheSize) {
override fun sizeOf(key: Int, entry: CacheEntry): Int {
private val memoryCache = object : LruCache<String, CacheEntry>(cacheSize) {
override fun sizeOf(key: String, entry: CacheEntry): Int {
return entry.sizeKb
}
}
fun get(pageIndex: Int): Bitmap? {
return memoryCache.get(pageIndex)?.bitmap?.takeUnless { it.isRecycled }
fun get(pageId: String): Bitmap? {
return memoryCache.get(pageId)?.bitmap?.takeUnless { it.isRecycled }
}
fun put(pageIndex: Int, bitmap: Bitmap) {
if (get(pageIndex) == null) {
fun put(pageId: String, bitmap: Bitmap) {
if (get(pageId) == null) {
val sizeKb = (bitmap.allocationByteCount / 1024).coerceAtLeast(1)
memoryCache.put(pageIndex, CacheEntry(bitmap, sizeKb))
memoryCache.put(pageId, CacheEntry(bitmap, sizeKb))
}
}
@ -475,6 +554,7 @@ data class PageSelectionData(
@Composable
internal fun PdfPageComposable(
pdfDocument: StableHolder<ReaderDocument>,
documentKey: String,
pageIndex: Int,
totalPages: Int,
modifier: Modifier = Modifier,
@ -507,6 +587,7 @@ internal fun PdfPageComposable(
isScrolling: Boolean = false,
lazyListState: LazyListState? = null,
isVerticalScroll: Boolean = false,
showPageNumberOverlay: Boolean = true,
visualScaleProvider: () -> Float = { 1f },
clearSelectionTrigger: Long = 0L,
resetZoomTrigger: Long = 0L,
@ -558,18 +639,13 @@ internal fun PdfPageComposable(
onShowPanelPopup: (Bitmap) -> Unit = {}
) {
val pdfDocumentItem = pdfDocument.item
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
var currentRenderedPageId by remember { mutableStateOf<String?>(null) }
val targetPageId = remember(virtualPage, pageIndex) {
when (virtualPage) {
is VirtualPage.BlankPage -> "BLANK_${virtualPage.id}"
is VirtualPage.PdfPage -> "PDF_${virtualPage.pdfIndex}"
null -> "PDF_$pageIndex"
}
val targetPageId = remember(documentKey, virtualPage, pageIndex) {
pdfRenderPageId(documentKey, pageIndex, virtualPage)
}
var isLoadingPage by remember { mutableStateOf(true) }
var pageErrorMessage by remember { mutableStateOf<String?>(null) }
var bitmapState by remember(targetPageId) { mutableStateOf(PdfThumbnailCache.get(targetPageId)) }
var currentRenderedPageId by remember(targetPageId) { mutableStateOf<String?>(null) }
var isLoadingPage by remember(targetPageId) { mutableStateOf(true) }
var pageErrorMessage by remember(targetPageId) { mutableStateOf<String?>(null) }
val density = LocalDensity.current
val context = LocalContext.current
val viewConfiguration = LocalViewConfiguration.current
@ -581,12 +657,30 @@ internal fun PdfPageComposable(
var ocrRipplePosition by remember { mutableStateOf<Offset?>(null) }
var isTransforming by remember { mutableStateOf(false) }
var scale by remember { mutableFloatStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val initialCamera = initialPdfPageCamera(
isZoomEnabled = isZoomEnabled,
isVerticalScroll = isVerticalScroll,
isScrollLocked = isScrollLocked,
lockedState = lockedState
)
var scale by remember(targetPageId) { mutableFloatStateOf(initialCamera.first) }
var offset by remember(targetPageId) { mutableStateOf(initialCamera.second) }
var paginationPanFlingJob by remember { mutableStateOf<Job?>(null) }
var hasAppliedLockedPaginationState by remember(targetPageId) {
mutableStateOf(initialCamera.second != Offset.Zero || initialCamera.first != 1f)
}
val shouldReportCamera = shouldReportPdfPageCamera(
isZoomEnabled = isZoomEnabled,
isVerticalScroll = isVerticalScroll,
isScrollLocked = isScrollLocked,
lockedState = lockedState,
hasAppliedLockedState = hasAppliedLockedPaginationState
)
LaunchedEffect(scale, offset) {
onZoomAndPanChanged?.invoke(scale, offset)
LaunchedEffect(scale, offset, shouldReportCamera) {
if (shouldReportCamera) {
onZoomAndPanChanged?.invoke(scale, offset)
}
}
val currentOnSingleTap by rememberUpdatedState(onSingleTap)
@ -609,7 +703,7 @@ internal fun PdfPageComposable(
val isPdfPage = virtualPage == null || virtualPage is VirtualPage.PdfPage
val pdfPageIndex = (virtualPage as? VirtualPage.PdfPage)?.pdfIndex ?: pageIndex
var tiles by remember { mutableStateOf<List<PdfTile>>(emptyList()) }
var tiles by remember(targetPageId) { mutableStateOf<List<PdfTile>>(emptyList()) }
val tileSizeDp = PDF_TILE_SIZE_DP.dp
val tileSizePx = with(LocalDensity.current) { tileSizeDp.toPx().toInt() }
val latestEffectiveScale by rememberUpdatedState(effectiveScale)
@ -645,15 +739,15 @@ internal fun PdfPageComposable(
}
}
val selectionCharRange = remember { mutableStateOf<Pair<Int, Int>?>(null) }
var activeDraggingHandle by remember { mutableStateOf<Handle?>(null) }
var selectedWordScreenRects by remember { mutableStateOf<List<Rect>>(emptyList()) }
val startHandleContentPosition = remember { mutableStateOf<Offset?>(null) }
val endHandleContentPosition = remember { mutableStateOf<Offset?>(null) }
val selectionCharRange = remember(targetPageId) { mutableStateOf<Pair<Int, Int>?>(null) }
var activeDraggingHandle by remember(targetPageId) { mutableStateOf<Handle?>(null) }
var selectedWordScreenRects by remember(targetPageId) { mutableStateOf<List<Rect>>(emptyList()) }
val startHandleContentPosition = remember(targetPageId) { mutableStateOf<Offset?>(null) }
val endHandleContentPosition = remember(targetPageId) { mutableStateOf<Offset?>(null) }
var actualBitmapWidthPx by remember { mutableIntStateOf(0) }
var actualBitmapHeightPx by remember { mutableIntStateOf(0) }
var currentPageRotation by remember { mutableIntStateOf(0) }
var actualBitmapWidthPx by remember(targetPageId) { mutableIntStateOf(0) }
var actualBitmapHeightPx by remember(targetPageId) { mutableIntStateOf(0) }
var currentPageRotation by remember(targetPageId) { mutableIntStateOf(0) }
val needsTilingNow = (effectiveScale > 1f || actualBitmapWidthPx > 3000 || actualBitmapHeightPx > 3000) && (isVerticalScroll || isActivePage)
@ -736,7 +830,7 @@ internal fun PdfPageComposable(
var magnifierBitmapCenterTarget by remember { mutableStateOf(Offset.Zero) }
val magnifierZoomFactor = 2.0f
var customMenuState by remember { mutableStateOf<CustomPdfMenuState?>(null) }
var customMenuState by remember(targetPageId) { mutableStateOf<CustomPdfMenuState?>(null) }
val inputScale = if (isZoomEnabled && !isVerticalScroll) scale else 1f
val inputOffset = if (isZoomEnabled && !isVerticalScroll) offset else Offset.Zero
@ -826,7 +920,15 @@ internal fun PdfPageComposable(
expandedBubbleIndex = -1
expandedBubbleRender?.bitmap?.takeUnless { it.isRecycled }?.recycle()
expandedBubbleRender = null
if (!isBubbleZoomModeActive && scale > 1f && !isVerticalScroll && isZoomEnabled) {
if (
shouldResetPdfZoomAfterBubbleZoomCleanup(
isBubbleZoomModeActive = isBubbleZoomModeActive,
scale = scale,
isVerticalScroll = isVerticalScroll,
isZoomEnabled = isZoomEnabled,
isScrollLocked = isScrollLocked
)
) {
coroutineScope.launch {
Animatable(scale).animateTo(1f, tween(300)) {
scale = this.value
@ -881,10 +983,10 @@ internal fun PdfPageComposable(
}
}
DisposableEffect(Unit) {
DisposableEffect(targetPageId) {
onDispose {
val currentBitmap = bitmapState
val cachedBitmap = PdfThumbnailCache.get(pageIndex)
val cachedBitmap = PdfThumbnailCache.get(targetPageId)
if (currentBitmap != null && !currentBitmap.isRecycled && currentBitmap !== cachedBitmap) {
currentBitmap.recycle()
}
@ -895,16 +997,16 @@ internal fun PdfPageComposable(
@Suppress("DEPRECATION") val clipboardManager = LocalClipboardManager.current
// OCR
var ocrVisionTextForSelection by remember { mutableStateOf<OcrResult?>(null) }
var ocrVisionTextForSelection by remember(targetPageId) { mutableStateOf<OcrResult?>(null) }
var isPerformingOcrForSelection by remember { mutableStateOf(false) }
var selectionMethodUsed by remember { mutableStateOf(PdfSelectionMethod.PDFIUM) }
var ocrSelectionSymbolIndices by remember { mutableStateOf<Pair<Int, Int>?>(null) }
var allOcrSymbolsForSelection by remember { mutableStateOf<List<OcrSymbolInfo>>(emptyList()) }
var ocrSelectionSymbolIndices by remember(targetPageId) { mutableStateOf<Pair<Int, Int>?>(null) }
var allOcrSymbolsForSelection by remember(targetPageId) { mutableStateOf<List<OcrSymbolInfo>>(emptyList()) }
var highlightedTextScreenRects by remember { mutableStateOf<List<Rect>>(emptyList()) }
var highlightedTextScreenRects by remember(targetPageId) { mutableStateOf<List<Rect>>(emptyList()) }
val ttsHighlightColor = Color(0xFFFFECB3).copy(alpha = 0.4f)
var allTextPageHighlightRects by remember { mutableStateOf<List<Rect>>(emptyList()) }
var allTextPageHighlightRects by remember(targetPageId) { mutableStateOf<List<Rect>>(emptyList()) }
var accumulatedKeyboardOffset by remember { mutableFloatStateOf(0f) }
@ -937,7 +1039,7 @@ internal fun PdfPageComposable(
val mergedSearchHighlightRects =
remember(searchHighlightRects) { mergeRectsIntoLines(searchHighlightRects) }
var pageLinks by remember { mutableStateOf<List<PageLink>>(emptyList()) }
var pageLinks by remember(targetPageId) { mutableStateOf<List<PageLink>>(emptyList()) }
val linkHighlightColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)
val linkVerticalPaddingPx = remember(density) { with(density) { 10.dp.toPx().toInt() } }
@ -1091,9 +1193,9 @@ internal fun PdfPageComposable(
onHighlightLoading(false)
}
@Suppress("VariableNeverRead") var embeddedAnnotations by remember { mutableStateOf<List<EmbeddedAnnotation>>(emptyList()) }
var standardAnnotScreenRects by remember { mutableStateOf<List<Pair<EmbeddedAnnotation, Rect>>>(emptyList()) }
var imageScreenRects by remember { mutableStateOf<List<android.graphics.Rect>>(emptyList()) }
@Suppress("VariableNeverRead") var embeddedAnnotations by remember(targetPageId) { mutableStateOf<List<EmbeddedAnnotation>>(emptyList()) }
var standardAnnotScreenRects by remember(targetPageId) { mutableStateOf<List<Pair<EmbeddedAnnotation, Rect>>>(emptyList()) }
var imageScreenRects by remember(targetPageId) { mutableStateOf<List<android.graphics.Rect>>(emptyList()) }
LaunchedEffect(pageIndex, pdfDocumentItem, actualBitmapWidthPx, actualBitmapHeightPx, virtualPage) {
if (!isPdfPage || actualBitmapWidthPx == 0 || actualBitmapHeightPx == 0) {
@ -1216,84 +1318,80 @@ internal fun PdfPageComposable(
val count = PdfiumEngineProvider.bridge.getAnnotCount(pagePtr)
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
if (count > 0) {
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 = PdfiumEngineProvider.bridge.getAnnotSubtype(pagePtr, i)
if (subtype == annotLink) return@mapNotNull null
val allAnnots = (0 until count).mapNotNull { i ->
val subtype = PdfiumEngineProvider.bridge.getAnnotSubtype(pagePtr, i)
if (subtype == annotLink) return@mapNotNull null
var contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "Contents")
if (contents.isNullOrBlank()) {
contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "RC")
}
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 = PdfiumEngineProvider.bridge.getAnnotRect(pagePtr, i)
val pdfRectF = if (pdfRectArray != null) {
android.graphics.RectF(
min(pdfRectArray[0], pdfRectArray[2]),
max(pdfRectArray[1], pdfRectArray[3]),
max(pdfRectArray[0], pdfRectArray[2]),
min(pdfRectArray[1], pdfRectArray[3])
)
} else android.graphics.RectF()
EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt)
var contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "Contents")
if (contents.isNullOrBlank()) {
contents = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "RC")
}
val annotMap = allAnnots.associateBy { it.name }
val orphans = mutableListOf<EmbeddedAnnotation>()
val name = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "NM")
val irt = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "IRT")
val author = PdfiumEngineProvider.bridge.getAnnotString(pagePtr, i, "T")
allAnnots.forEach { annot ->
if (!annot.inReplyTo.isNullOrBlank() && annotMap.containsKey(annot.inReplyTo)) {
Timber.tag("PdfCommentDebug").i("Linking: ${annot.name} is a reply to ${annot.inReplyTo}")
annotMap[annot.inReplyTo]?.replies?.add(annot)
} else {
orphans.add(annot)
}
}
Timber.tag("PdfCommentDebug").d("After ID linking: Orphans count = ${orphans.size}")
val groupedRoots = mutableListOf<MutableList<EmbeddedAnnotation>>()
orphans.forEach { annot ->
val match = groupedRoots.find { group ->
val root = group.first()
val inflatedRoot = android.graphics.RectF(root.rect).apply { inset(-10f, -10f) }
android.graphics.RectF.intersects(inflatedRoot, annot.rect)
}
if (match != null) {
Timber.tag("PdfCommentDebug").w("Geometric grouping triggered for ${annot.name} with ${match.first().name}. This might flatten nested replies!")
match.add(annot)
} else {
groupedRoots.add(mutableListOf(annot))
}
}
val rootsWithReplies = groupedRoots.map { group ->
val root = group.first()
if (group.size > 1) {
root.replies.addAll(group.drop(1))
}
root
}
finalDisplayList = rootsWithReplies.filter {
!it.contents.isNullOrBlank() || it.replies.any { r -> !r.contents.isNullOrBlank() }
}
mappedAnnots = finalDisplayList.map { annot ->
val screenRect = pageWrapper.mapRectToDevice(
0, 0, actualBitmapWidthPx, actualBitmapHeightPx,
currentPageRotation, annot.rect
val pdfRectArray = PdfiumEngineProvider.bridge.getAnnotRect(pagePtr, i)
val pdfRectF = if (pdfRectArray != null) {
android.graphics.RectF(
min(pdfRectArray[0], pdfRectArray[2]),
max(pdfRectArray[1], pdfRectArray[3]),
max(pdfRectArray[0], pdfRectArray[2]),
min(pdfRectArray[1], pdfRectArray[3])
)
annot to screenRect
} else android.graphics.RectF()
EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt)
}
val annotMap = allAnnots.associateBy { it.name }
val orphans = mutableListOf<EmbeddedAnnotation>()
allAnnots.forEach { annot ->
if (!annot.inReplyTo.isNullOrBlank() && annotMap.containsKey(annot.inReplyTo)) {
Timber.tag("PdfCommentDebug").i("Linking: ${annot.name} is a reply to ${annot.inReplyTo}")
annotMap[annot.inReplyTo]?.replies?.add(annot)
} else {
orphans.add(annot)
}
}
Timber.tag("PdfCommentDebug").d("After ID linking: Orphans count = ${orphans.size}")
val groupedRoots = mutableListOf<MutableList<EmbeddedAnnotation>>()
orphans.forEach { annot ->
val match = groupedRoots.find { group ->
val root = group.first()
val inflatedRoot = android.graphics.RectF(root.rect).apply { inset(-10f, -10f) }
android.graphics.RectF.intersects(inflatedRoot, annot.rect)
}
if (match != null) {
Timber.tag("PdfCommentDebug").w("Geometric grouping triggered for ${annot.name} with ${match.first().name}. This might flatten nested replies!")
match.add(annot)
} else {
groupedRoots.add(mutableListOf(annot))
}
}
val rootsWithReplies = groupedRoots.map { group ->
val root = group.first()
if (group.size > 1) {
root.replies.addAll(group.drop(1))
}
root
}
finalDisplayList = rootsWithReplies.filter {
!it.contents.isNullOrBlank() || it.replies.any { r -> !r.contents.isNullOrBlank() }
}
mappedAnnots = finalDisplayList.map { annot ->
val screenRect = pageWrapper.mapRectToDevice(
0, 0, actualBitmapWidthPx, actualBitmapHeightPx,
currentPageRotation, annot.rect
)
annot to screenRect
}
}
} else {
Timber.tag("PdfCommentDebug").w("Page $pageIndex: Failed to resolve native page pointer.")
@ -1323,7 +1421,7 @@ internal fun PdfPageComposable(
Timber.d("Page $pageIndex hidden. Releasing bitmap to save memory.")
val old = bitmapState
bitmapState = null
@Suppress("ControlFlowWithEmptyBody") if (old != null && old !== PdfThumbnailCache.get(pageIndex)) { }
@Suppress("ControlFlowWithEmptyBody") if (old != null && old !== PdfThumbnailCache.get(targetPageId)) { }
}
}
}
@ -1584,10 +1682,10 @@ internal fun PdfPageComposable(
}
}
var searchFocusedRects by remember { mutableStateOf<List<Rect>>(emptyList()) }
var searchAllRects by remember { mutableStateOf<List<Rect>>(emptyList()) }
var searchFocusedRects by remember(targetPageId) { mutableStateOf<List<Rect>>(emptyList()) }
var searchAllRects by remember(targetPageId) { mutableStateOf<List<Rect>>(emptyList()) }
var keyboardAdjustmentOriginalOffset by remember { mutableStateOf<Float?>(null) }
var keyboardAdjustmentOriginalOffset by remember(targetPageId) { mutableStateOf<Float?>(null) }
val mergedSearchFocusedRects = remember(searchFocusedRects) { searchFocusedRects }
val mergedSearchAllRects = remember(searchAllRects) { searchAllRects }
@ -1932,10 +2030,6 @@ internal fun PdfPageComposable(
}
}
val errorSelection = stringResource(R.string.error_selection)
val errorOcrSelection = stringResource(R.string.error_ocr_selection)
val errorProcessingPage = stringResource(R.string.error_processing_page)
BoxWithConstraints(
modifier = modifier
.onGloballyPositioned { layoutCoordinates = it }
@ -2706,7 +2800,10 @@ internal fun PdfPageComposable(
Timber.e(
e, "Long press: Error during OCR text selection"
)
pageErrorMessage = errorOcrSelection
pageErrorMessage = context.getString(
R.string.error_ocr_selection,
e.readablePdfErrorDetail()
)
} finally {
isPerformingOcrForSelection = false
ocrRipplePosition = null
@ -2727,7 +2824,10 @@ internal fun PdfPageComposable(
e,
"Error during long press text selection on page $pageIndex"
)
pageErrorMessage = errorSelection
pageErrorMessage = context.getString(
R.string.error_selection,
e.readablePdfErrorDetail()
)
customMenuState = null
selectionCharRange.value = null
selectedWordScreenRects = emptyList()
@ -2774,7 +2874,8 @@ internal fun PdfPageComposable(
selectedTool,
isStylusOnlyMode,
userHighlightScreenRects,
bubbleTapSlopPx
bubbleTapSlopPx,
isScrollLocked
) {
val isTapDetectionAllowed = !isEditMode ||
selectedTool == InkType.TEXT ||
@ -3040,7 +3141,7 @@ internal fun PdfPageComposable(
onScaleChanged(scale)
}
}
} else if (isVerticalScroll && currentOnDoubleTap != null) {
} else if (isVerticalScroll && !isScrollLocked && currentOnDoubleTap != null) {
currentOnDoubleTap!!(tapOffset)
}
})
@ -3276,7 +3377,7 @@ internal fun PdfPageComposable(
val startOffset = offset
paginationPanFlingJob = coroutineScope.launch {
try {
coroutineScope {
kotlinx.coroutines.coroutineScope {
launch {
if (flingX != 0f) {
Animatable(startOffset.x).animateDecay(flingX, decay) {
@ -3543,15 +3644,37 @@ internal fun PdfPageComposable(
}
}
var previousLockedViewportSize by remember { mutableStateOf<Pair<Dp, Dp>?>(null) }
LaunchedEffect(
pageIndex, this@BoxWithConstraints.maxWidth, this@BoxWithConstraints.maxHeight,
isScrollLocked, lockedState
) {
if (isScrollLocked && !isVerticalScroll && lockedState != null) {
scale = lockedState.first
offset = Offset(lockedState.second, lockedState.third)
val currentViewportSize = this@BoxWithConstraints.maxWidth to this@BoxWithConstraints.maxHeight
val previousViewportSize = previousLockedViewportSize
val orientationChanged = previousViewportSize != null &&
(previousViewportSize.first > previousViewportSize.second) !=
(currentViewportSize.first > currentViewportSize.second)
previousLockedViewportSize = currentViewportSize
if (isScrollLocked && !isVerticalScroll) {
if (orientationChanged) {
scale = 1f
offset = Offset.Zero
hasAppliedLockedPaginationState = true
Timber.tag("PdfLockDiagnostic").i(
"Orientation changed while locked; reset paginated zoom to fit on page $pageIndex"
)
} else if (lockedState != null) {
scale = lockedState.first
offset = Offset(lockedState.second, lockedState.third)
hasAppliedLockedPaginationState = true
} else {
hasAppliedLockedPaginationState = true
}
onScaleChanged(scale)
} else if (!isScrollLocked && !isVerticalScroll) {
hasAppliedLockedPaginationState = false
scale = 1f
offset = Offset.Zero
onScaleChanged(1f)
@ -3699,10 +3822,12 @@ internal fun PdfPageComposable(
currentContainerMaxHeight,
density,
virtualPage,
targetPageId,
isVisible,
currentRenderedPageId
) {
if (!isVisible && !isVerticalScroll) return@LaunchedEffect
pageErrorMessage = null
val viewContainerWidthPx = with(density) { currentContainerMaxWidth.toPx().toInt() }
val viewContainerHeightPx =
@ -3721,12 +3846,18 @@ internal fun PdfPageComposable(
1f / 1.414f
}
var scaledWidth = viewContainerWidthPx
var scaledHeight = (scaledWidth / pageAspect).toInt()
val (scaledWidth, scaledHeight) = if (isVerticalScroll) {
viewContainerWidthPx to viewContainerHeightPx
} else {
var fittedWidth = viewContainerWidthPx
var fittedHeight = (fittedWidth / pageAspect).toInt()
if (scaledHeight > viewContainerHeightPx) {
scaledHeight = viewContainerHeightPx
scaledWidth = (scaledHeight * pageAspect).toInt()
if (fittedHeight > viewContainerHeightPx) {
fittedHeight = viewContainerHeightPx
fittedWidth = (fittedHeight * pageAspect).toInt()
}
fittedWidth to fittedHeight
}
if (scaledWidth == actualBitmapWidthPx &&
@ -3760,7 +3891,7 @@ internal fun PdfPageComposable(
val old = bitmapState
if (old != null && old !== finalBitmap) {
if (old !== PdfThumbnailCache.get(pageIndex)) {
if (old !== PdfThumbnailCache.get(targetPageId)) {
old.recycle()
}
}
@ -3771,7 +3902,7 @@ internal fun PdfPageComposable(
return@LaunchedEffect
}
coroutineScope.launch {
kotlinx.coroutines.coroutineScope {
var localBitmap: Bitmap? = null
try {
val renderResult = withContext(Dispatchers.IO) {
@ -3783,62 +3914,68 @@ internal fun PdfPageComposable(
return@withContext null
}
val page = pdfDocumentItem.openPage(pdfPageIndex) ?: return@withContext null
val rotation = page.getPageRotation()
val screenDpi = (density.density * 160).roundToInt()
val originalWidthPdfUnits = page.getPageWidthPoint()
val originalHeightPdfUnits = page.getPageHeightPoint()
try {
val rotation = page.getPageRotation()
val originalWidthPdfUnits = page.getPageWidthPoint()
val originalHeightPdfUnits = page.getPageHeightPoint()
if (originalWidthPdfUnits <= 0 || originalHeightPdfUnits <= 0) {
if (originalWidthPdfUnits <= 0 || originalHeightPdfUnits <= 0) {
throw Exception("Invalid page dimensions")
}
val aspectRatio =
originalWidthPdfUnits.toFloat() / originalHeightPdfUnits.toFloat()
val (scaledWidth, scaledHeight) = if (isVerticalScroll) {
viewContainerWidthPx to viewContainerHeightPx
} else {
var fittedWidth = viewContainerWidthPx
var fittedHeight = (fittedWidth / aspectRatio).toInt()
if (fittedHeight > viewContainerHeightPx) {
fittedHeight = viewContainerHeightPx
fittedWidth = (fittedHeight * aspectRatio).toInt()
}
fittedWidth to fittedHeight
}
if (scaledWidth == actualBitmapWidthPx &&
scaledHeight == actualBitmapHeightPx &&
bitmapState != null &&
currentRenderedPageId == targetPageId
) {
return@withContext null
}
val MAX_BASE_DIMEN = 3000
val baseRenderScale = 1.5f
var baseW = (scaledWidth * baseRenderScale).toInt()
var baseH = (scaledHeight * baseRenderScale).toInt()
if (baseW > MAX_BASE_DIMEN || baseH > MAX_BASE_DIMEN) {
val downScale = MAX_BASE_DIMEN.toFloat() / maxOf(baseW, baseH)
baseW = (baseW * downScale).toInt().coerceAtLeast(1)
baseH = (baseH * downScale).toInt().coerceAtLeast(1)
}
Timber.d(
"Rendering page $pageIndex at ${baseW}x${baseH} (logical: ${scaledWidth}x${scaledHeight})"
)
val newBitmap = createBitmap(baseW, baseH)
localBitmap = newBitmap
page.renderPageBitmap(
newBitmap,
0, 0,
baseW, baseH,
true
)
Triple(newBitmap, rotation, Pair(scaledWidth, scaledHeight))
} finally {
page.close()
throw Exception("Invalid page dimensions")
}
val aspectRatio =
originalWidthPdfUnits.toFloat() / originalHeightPdfUnits.toFloat()
var scaledWidth = viewContainerWidthPx
var scaledHeight = (scaledWidth / aspectRatio).toInt()
if (scaledHeight > viewContainerHeightPx) {
scaledHeight = viewContainerHeightPx
scaledWidth = (scaledHeight * aspectRatio).toInt()
}
if (scaledWidth == actualBitmapWidthPx &&
scaledHeight == actualBitmapHeightPx &&
bitmapState != null &&
currentRenderedPageId == targetPageId
) {
page.close()
return@withContext null
}
val MAX_BASE_DIMEN = 3000
val baseRenderScale = 1.5f
var baseW = (scaledWidth * baseRenderScale).toInt()
var baseH = (scaledHeight * baseRenderScale).toInt()
if (baseW > MAX_BASE_DIMEN || baseH > MAX_BASE_DIMEN) {
val downScale = MAX_BASE_DIMEN.toFloat() / maxOf(baseW, baseH)
baseW = (baseW * downScale).toInt().coerceAtLeast(1)
baseH = (baseH * downScale).toInt().coerceAtLeast(1)
}
Timber.d(
"Rendering page $pageIndex at ${baseW}x${baseH} (logical: ${scaledWidth}x${scaledHeight})"
)
val newBitmap = createBitmap(baseW, baseH)
localBitmap = newBitmap
page.renderPageBitmap(
newBitmap,
0, 0,
baseW, baseH,
true
)
page.close()
Triple(newBitmap, rotation, Pair(scaledWidth, scaledHeight))
}
if (renderResult != null) {
@ -3856,7 +3993,7 @@ internal fun PdfPageComposable(
withContext(Dispatchers.IO) {
if (old != null && old !== newBitmap && !old.isRecycled) {
val cached = PdfThumbnailCache.get(pageIndex)
val cached = PdfThumbnailCache.get(targetPageId)
if (old !== cached) {
old.recycle()
}
@ -3866,14 +4003,17 @@ internal fun PdfPageComposable(
val thumbHeight = newBitmap.height / 2
if (thumbWidth > 0 && thumbHeight > 0) {
PdfThumbnailCache.put(
pageIndex, newBitmap.scale(thumbWidth, thumbHeight)
targetPageId, newBitmap.scale(thumbWidth, thumbHeight)
)
}
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
pageErrorMessage = errorProcessingPage
pageErrorMessage = context.getString(
R.string.error_processing_page,
e.readablePdfErrorDetail()
)
} finally {
isLoadingPage = false
localBitmap?.recycle()
@ -4265,6 +4405,7 @@ internal fun PdfPageComposable(
contentToScreenCoordinates = contentToScreenCoordinates,
density = density,
isVerticalScroll = isVerticalScroll,
showPageNumberOverlay = showPageNumberOverlay,
isScrolling = isScrolling,
isEditMode = isEditMode,
selectedTool = selectedTool,
@ -4296,7 +4437,7 @@ internal fun PdfPageComposable(
else -> {
Text(
text = stringResource(R.string.error_unable_to_display_page),
text = stringResource(R.string.error_unable_to_display_page, pageIndex + 1),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Center)
@ -4355,7 +4496,13 @@ private fun PdfBitmapLayer(
Canvas(modifier = Modifier.fillMaxSize().graphicsLayer()) {
translate(left = centeringOffsetX, top = centeringOffsetY) {
clipRect(left = 0f, top = 0f, right = targetWidth.toFloat(), bottom = targetHeight.toFloat()) {
if (bitmapState != null && !bitmapState.isRecycled) {
if (
bitmapState != null &&
bitmapState.isCanvasSafeBitmap(
maxBytes = PDF_MAX_DRAW_BITMAP_BYTES,
maxDimension = PDF_MAX_DRAW_BITMAP_DIMENSION_PX
)
) {
val dstW = if (targetWidth > 0) targetWidth else bitmapState.width
val dstH = if (targetHeight > 0) targetHeight else bitmapState.height
val srcSize = IntSize(bitmapState.width, bitmapState.height)
@ -4400,7 +4547,12 @@ private fun PdfBitmapLayer(
val needsTiling = effectiveScale > 1f || targetWidth > 3000 || targetHeight > 3000
if (needsTiling) {
tiles.forEach { tile ->
if (!tile.bitmap.isRecycled) {
if (
tile.bitmap.isCanvasSafeBitmap(
maxBytes = PDF_MAX_DRAW_BITMAP_BYTES,
maxDimension = PDF_MAX_DRAW_BITMAP_DIMENSION_PX
)
) {
drawImage(
image = tile.bitmap.asImageBitmap(),
srcOffset = IntOffset.Zero,
@ -5086,6 +5238,7 @@ private fun PdfPageRenderer(
contentToScreenCoordinates: (Offset) -> Offset,
density: Density,
isVerticalScroll: Boolean,
showPageNumberOverlay: Boolean,
isScrolling: Boolean,
isEditMode: Boolean,
selectedTool: InkType,
@ -5282,7 +5435,7 @@ private fun PdfPageRenderer(
}
// Layer 4: Page Number Indicator
if (totalPages > 0) {
if (showPageNumberOverlay && totalPages > 0) {
val pageNumColor = if (staticData.isDarkMode) {
Color.White
} else {
@ -5439,10 +5592,12 @@ private fun PdfPageRenderer(
tiles = if (effectiveScale > 1f) staticData.tiles.item else emptyList(),
currentScale = effectiveScale,
magnifierCenterOnBitmap = magnifierCenterTarget,
contentWidthPx = staticData.targetWidth,
contentHeightPx = staticData.targetHeight,
magnifierWidth = magnifierWidth,
magnifierHeight = magnifierHeight,
zoomFactor = effectiveZoomFactor,
selectionRectsInBitmapCoords = selectionData.mergedSelectionRects.item,
selectionRectsInContentCoords = selectionData.mergedSelectionRects.item,
highlightColor = Color(0x6633B5E5),
colorFilter = staticData.colorFilter.item,
modifier = Modifier
@ -5593,6 +5748,21 @@ private fun PdfPageRenderer(
}
if (animatingBubbleIndex in detectedBubbles.indices && staticData.bitmap.item != null && bubbleExpansionProgress > 0f) {
val baseBitmap = staticData.bitmap.item ?: return@Canvas
if (
!baseBitmap.isCanvasSafeBitmap(
maxBytes = PDF_MAX_DRAW_BITMAP_BYTES,
maxDimension = PDF_MAX_DRAW_BITMAP_DIMENSION_PX
)
) {
return@Canvas
}
val safeExpandedBubbleRender = expandedBubbleRender?.takeIf {
it.bitmap.isCanvasSafeBitmap(
maxBytes = PDF_MAX_DRAW_BITMAP_BYTES,
maxDimension = PDF_MAX_DRAW_BITMAP_DIMENSION_PX
)
}
val bubble = detectedBubbles[animatingBubbleIndex]
val left = bubble.bounds.left + staticData.centeringOffsetX
val top = bubble.bounds.top + staticData.centeringOffsetY
@ -5600,7 +5770,7 @@ private fun PdfPageRenderer(
val logicalHeight = bubble.bounds.height()
val pivotX = left + logicalWidth / 2f
val pivotY = top + logicalHeight / 2f
val targetZoomFactor = expandedBubbleRender?.zoomFactor ?: computeDynamicBubbleZoomFactor(
val targetZoomFactor = safeExpandedBubbleRender?.zoomFactor ?: computeDynamicBubbleZoomFactor(
bubbleBounds = bubble.bounds,
viewportWidth = staticData.canvasWidth,
viewportHeight = staticData.canvasHeight
@ -5613,8 +5783,8 @@ private fun PdfPageRenderer(
val dstOffset = IntOffset(left.toInt(), top.toInt())
val dstSize = IntSize(logicalWidth.toInt(), logicalHeight.toInt())
val renderScaleX = staticData.bitmap.item.width.toFloat() / staticData.targetWidth.toFloat()
val renderScaleY = staticData.bitmap.item.height.toFloat() / staticData.targetHeight.toFloat()
val renderScaleX = baseBitmap.width.toFloat() / staticData.targetWidth.toFloat()
val renderScaleY = baseBitmap.height.toFloat() / staticData.targetHeight.toFloat()
val srcOffset = IntOffset(
(bubble.bounds.left * renderScaleX).toInt(),
@ -5651,12 +5821,12 @@ private fun PdfPageRenderer(
)
drawContext.canvas.saveLayer(rect, androidx.compose.ui.graphics.Paint())
drawImage(
image = (expandedBubbleRender?.bitmap ?: staticData.bitmap.item).asImageBitmap(),
srcOffset = if (expandedBubbleRender != null) IntOffset.Zero else srcOffset,
srcSize = if (expandedBubbleRender != null) {
image = (safeExpandedBubbleRender?.bitmap ?: baseBitmap).asImageBitmap(),
srcOffset = if (safeExpandedBubbleRender != null) IntOffset.Zero else srcOffset,
srcSize = if (safeExpandedBubbleRender != null) {
IntSize(
expandedBubbleRender.bitmap.width,
expandedBubbleRender.bitmap.height)
safeExpandedBubbleRender.bitmap.width,
safeExpandedBubbleRender.bitmap.height)
} else {
srcSize
},
@ -5675,12 +5845,12 @@ private fun PdfPageRenderer(
} else {
clipRect(left, top, left + logicalWidth, top + logicalHeight) {
drawImage(
image = (expandedBubbleRender?.bitmap ?: staticData.bitmap.item).asImageBitmap(),
srcOffset = if (expandedBubbleRender != null) IntOffset.Zero else srcOffset,
srcSize = if (expandedBubbleRender != null) {
image = (safeExpandedBubbleRender?.bitmap ?: baseBitmap).asImageBitmap(),
srcOffset = if (safeExpandedBubbleRender != null) IntOffset.Zero else srcOffset,
srcSize = if (safeExpandedBubbleRender != null) {
IntSize(
expandedBubbleRender.bitmap.width,
expandedBubbleRender.bitmap.height)
safeExpandedBubbleRender.bitmap.width,
safeExpandedBubbleRender.bitmap.height)
} else {
srcSize
},