* 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)
239 lines
8.7 KiB
Kotlin
239 lines
8.7 KiB
Kotlin
package com.aryan.reader.desktop
|
|
|
|
import androidx.compose.ui.graphics.ImageBitmap
|
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
|
import com.aryan.reader.shared.pdf.PdfZoomSpec
|
|
import com.sun.jna.Library
|
|
import com.sun.jna.Memory
|
|
import com.sun.jna.Native
|
|
import com.sun.jna.Pointer
|
|
import java.awt.image.BufferedImage
|
|
import java.io.File
|
|
import java.nio.ByteOrder
|
|
import kotlin.math.roundToInt
|
|
|
|
data class DesktopPdfDocument(
|
|
val path: String,
|
|
val title: String,
|
|
val pageCount: Int,
|
|
val pageSizes: List<DesktopPdfPageSize>,
|
|
val textPages: List<String>
|
|
) {
|
|
fun close() {
|
|
DesktopPdfium.closeDocument(path)
|
|
}
|
|
}
|
|
|
|
data class DesktopPdfPageSize(
|
|
val width: Float,
|
|
val height: Float
|
|
)
|
|
|
|
data class DesktopPdfPageRender(
|
|
val image: ImageBitmap,
|
|
val width: Int,
|
|
val height: Int
|
|
)
|
|
|
|
object DesktopPdfium {
|
|
private const val FPDF_ANNOT = 0x01
|
|
private const val FPDF_LCD_TEXT = 0x02
|
|
private const val FPDF_RENDER_NO_SMOOTHTEXT = 0x1000
|
|
private const val FPDF_BITMAP_BGRA = 4
|
|
|
|
private val pdfiumDll: File by lazy(::resolvePdfiumDll)
|
|
private val zoomSpec = PdfZoomSpec()
|
|
private val api: PdfiumLibrary by lazy {
|
|
require(pdfiumDll.exists()) {
|
|
"Missing Pdfium DLL. Expected pdfium-v8-win-x64 under third_party/pdfium/win-x64-v8/bin/pdfium.dll."
|
|
}
|
|
Native.load(pdfiumDll.absolutePath, PdfiumLibrary::class.java)
|
|
}
|
|
|
|
private var initialized = false
|
|
private val openDocuments = LinkedHashMap<String, Pointer>()
|
|
|
|
fun isAvailable(): Boolean = pdfiumDll.exists()
|
|
|
|
fun load(file: File, password: String? = null): DesktopPdfDocument {
|
|
initLibrary()
|
|
val document = api.FPDF_LoadDocument(file.absolutePath, password)
|
|
?: error("Pdfium could not open ${file.name}. It may be encrypted or unsupported.")
|
|
val pageCount = api.FPDF_GetPageCount(document)
|
|
openDocuments[file.absolutePath] = document
|
|
|
|
val pageSizes = (0 until pageCount).map { pageIndex ->
|
|
loadPage(document, pageIndex).usePointer { page ->
|
|
DesktopPdfPageSize(
|
|
width = api.FPDF_GetPageWidthF(page),
|
|
height = api.FPDF_GetPageHeightF(page)
|
|
)
|
|
}
|
|
}
|
|
|
|
val textPages = (0 until pageCount).map { pageIndex ->
|
|
extractPageText(document, pageIndex)
|
|
}
|
|
|
|
return DesktopPdfDocument(
|
|
path = file.absolutePath,
|
|
title = file.nameWithoutExtension,
|
|
pageCount = pageCount,
|
|
pageSizes = pageSizes,
|
|
textPages = textPages
|
|
)
|
|
}
|
|
|
|
fun closeDocument(path: String) {
|
|
openDocuments.remove(path)?.let(api::FPDF_CloseDocument)
|
|
}
|
|
|
|
fun renderPage(
|
|
document: DesktopPdfDocument,
|
|
pageIndex: Int,
|
|
scale: Float,
|
|
renderAnnotations: Boolean = true
|
|
): DesktopPdfPageRender {
|
|
val nativeDocument = openDocuments[document.path] ?: error("PDF document is not open.")
|
|
val pageSize = document.pageSizes.getOrNull(pageIndex) ?: error("Invalid PDF page index $pageIndex.")
|
|
val safeScale = zoomSpec.safeRenderScale(pageSize.width, pageSize.height, scale)
|
|
val width = (pageSize.width * safeScale).roundToInt().coerceAtLeast(1)
|
|
val height = (pageSize.height * safeScale).roundToInt().coerceAtLeast(1)
|
|
val stride = width * 4
|
|
val memory = Memory((stride * height).toLong())
|
|
memory.clear(memory.size())
|
|
|
|
val bitmap = api.FPDFBitmap_CreateEx(width, height, FPDF_BITMAP_BGRA, memory, stride)
|
|
?: error("Pdfium could not allocate render bitmap.")
|
|
|
|
try {
|
|
api.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, -1)
|
|
loadPage(nativeDocument, pageIndex).usePointer { page ->
|
|
val flags = FPDF_LCD_TEXT or
|
|
(if (renderAnnotations) FPDF_ANNOT else FPDF_RENDER_NO_SMOOTHTEXT)
|
|
api.FPDF_RenderPageBitmap(bitmap, page, 0, 0, width, height, 0, flags)
|
|
}
|
|
return DesktopPdfPageRender(
|
|
image = memory.toBufferedImage(width, height, stride).toComposeImageBitmap(),
|
|
width = width,
|
|
height = height
|
|
)
|
|
} finally {
|
|
api.FPDFBitmap_Destroy(bitmap)
|
|
}
|
|
}
|
|
|
|
private fun extractPageText(document: Pointer, pageIndex: Int): String {
|
|
return runCatching {
|
|
loadPage(document, pageIndex).usePointer { page ->
|
|
val textPage = api.FPDFText_LoadPage(page) ?: return@usePointer ""
|
|
try {
|
|
val charCount = api.FPDFText_CountChars(textPage)
|
|
if (charCount <= 0) return@usePointer ""
|
|
val buffer = Memory(((charCount + 1) * 2L))
|
|
val written = api.FPDFText_GetText(textPage, 0, charCount, buffer)
|
|
if (written <= 0) {
|
|
""
|
|
} else {
|
|
buffer.getCharArray(0, written).concatToString().trimEnd('\u0000')
|
|
}
|
|
} finally {
|
|
api.FPDFText_ClosePage(textPage)
|
|
}
|
|
}
|
|
}.getOrDefault("")
|
|
}
|
|
|
|
private fun loadPage(document: Pointer, pageIndex: Int): PointerResource {
|
|
val page = api.FPDF_LoadPage(document, pageIndex)
|
|
?: error("Pdfium could not open page ${pageIndex + 1}.")
|
|
return PointerResource(page, api::FPDF_ClosePage)
|
|
}
|
|
|
|
private fun initLibrary() {
|
|
if (!initialized) {
|
|
api.FPDF_InitLibrary()
|
|
initialized = true
|
|
}
|
|
}
|
|
|
|
private fun resolvePdfiumDll(): File {
|
|
val overridePath = System.getProperty("reader.pdfium.dll")
|
|
?: System.getenv("READER_PDFIUM_DLL")
|
|
if (!overridePath.isNullOrBlank()) {
|
|
return File(overridePath).absoluteFile
|
|
}
|
|
|
|
val relativePath = listOf("third_party", "pdfium", "win-x64-v8", "bin", "pdfium.dll")
|
|
.joinToString(File.separator)
|
|
val roots = generateSequence(File(System.getProperty("user.dir")).absoluteFile) { it.parentFile }
|
|
.take(6)
|
|
.toList()
|
|
|
|
return roots
|
|
.map { File(it, relativePath).absoluteFile }
|
|
.firstOrNull { it.exists() }
|
|
?: File(File(System.getProperty("user.dir")).absoluteFile, relativePath).absoluteFile
|
|
}
|
|
|
|
private fun Memory.toBufferedImage(width: Int, height: Int, stride: Int): BufferedImage {
|
|
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
|
val buffer = getByteBuffer(0, size()).order(ByteOrder.LITTLE_ENDIAN)
|
|
val pixels = IntArray(width * height)
|
|
for (y in 0 until height) {
|
|
buffer.position(y * stride)
|
|
for (x in 0 until width) {
|
|
val b = buffer.get().toInt() and 0xFF
|
|
val g = buffer.get().toInt() and 0xFF
|
|
val r = buffer.get().toInt() and 0xFF
|
|
val a = buffer.get().toInt() and 0xFF
|
|
pixels[y * width + x] = (a shl 24) or (r shl 16) or (g shl 8) or b
|
|
}
|
|
}
|
|
image.setRGB(0, 0, width, height, pixels, 0, width)
|
|
return image
|
|
}
|
|
|
|
private class PointerResource(
|
|
private val pointer: Pointer,
|
|
private val closer: (Pointer) -> Unit
|
|
) {
|
|
fun <T> usePointer(block: (Pointer) -> T): T {
|
|
try {
|
|
return block(pointer)
|
|
} finally {
|
|
closer(pointer)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Suppress("FunctionName")
|
|
private interface PdfiumLibrary : Library {
|
|
fun FPDF_InitLibrary()
|
|
fun FPDF_LoadDocument(filePath: String, password: String?): Pointer?
|
|
fun FPDF_CloseDocument(document: Pointer)
|
|
fun FPDF_GetPageCount(document: Pointer): Int
|
|
fun FPDF_LoadPage(document: Pointer, pageIndex: Int): Pointer?
|
|
fun FPDF_ClosePage(page: Pointer)
|
|
fun FPDF_GetPageWidthF(page: Pointer): Float
|
|
fun FPDF_GetPageHeightF(page: Pointer): Float
|
|
fun FPDFBitmap_CreateEx(width: Int, height: Int, format: Int, firstScan: Pointer, stride: Int): Pointer?
|
|
fun FPDFBitmap_FillRect(bitmap: Pointer, left: Int, top: Int, width: Int, height: Int, color: Int)
|
|
fun FPDFBitmap_Destroy(bitmap: Pointer)
|
|
fun FPDF_RenderPageBitmap(
|
|
bitmap: Pointer,
|
|
page: Pointer,
|
|
startX: Int,
|
|
startY: Int,
|
|
sizeX: Int,
|
|
sizeY: Int,
|
|
rotate: Int,
|
|
flags: Int
|
|
)
|
|
|
|
fun FPDFText_LoadPage(page: Pointer): Pointer?
|
|
fun FPDFText_ClosePage(textPage: Pointer)
|
|
fun FPDFText_CountChars(textPage: Pointer): Int
|
|
fun FPDFText_GetText(textPage: Pointer, startIndex: Int, count: Int, result: Pointer): Int
|
|
}
|
|
}
|