Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import androidx.compose.ui.graphics.asImageBitmap
import dev.nucleusframework.pdfium.jvm.FPDF_ANNOT
import dev.nucleusframework.pdfium.jvm.Pdfium
import dev.nucleusframework.pdfium.jvm.PdfiumBridge
import java.util.concurrent.ExecutorService
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

Expand All @@ -19,8 +17,6 @@ internal actual class PdfDocument internal constructor(
// Per-document FPDF_FORMHANDLE (parallel to [handles]). May be 0 if PDFium refused;
// render code falls back to no widget overlay in that case.
private val formHandles: LongArray,
private val dispatchers: Array<CoroutineDispatcher>,
private val executors: Array<ExecutorService>,
) {
actual val pageCount: Int = PdfiumBridge.nGetPageCount(handles[0])
actual val metadata: PdfMetadata = PdfMetadata(
Expand All @@ -37,7 +33,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageSize(pageIndex: Int): PageSize {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext PageSize(0f, 0f)
Expand All @@ -59,7 +55,7 @@ internal actual class PdfDocument internal constructor(
quality: RenderQuality,
): ImageBitmap {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
Expand All @@ -85,7 +81,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageText(pageIndex: Int): String {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext ""
Expand All @@ -99,7 +95,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageTextLayout(pageIndex: Int): PageTextLayout {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext PageTextLayout.Empty
Expand Down Expand Up @@ -143,7 +139,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageLinks(pageIndex: Int): PageLinks {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext PageLinks.Empty
Expand All @@ -169,15 +165,14 @@ internal actual class PdfDocument internal constructor(
actual fun close() {
runBlocking {
for (i in handles.indices) {
withContext(dispatchers[i]) {
withContext(Pdfium.sharedDispatcher) {
// Form-fill env must be torn down BEFORE its underlying document.
PdfiumBridge.nCloseFormEnv(formHandles[i])
PdfiumBridge.nCloseDocument(handles[i])
}
}
}
PdfiumBridge.nFreeBuffer(bufferAddr)
executors.forEach { it.shutdown() }
}

private fun RenderQuality.toFlags(): Int = when (this) {
Expand Down Expand Up @@ -206,11 +201,8 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?)
}
handles[i] = h
}
val pairs = Array(POOL_SIZE) { Pdfium.newDispatcher() }
val dispatchers = Array(POOL_SIZE) { pairs[it].first }
val executors = Array(POOL_SIZE) { pairs[it].second }
val formHandles = LongArray(POOL_SIZE) { PdfiumBridge.nInitFormEnv(handles[it]) }
PdfDocument(bufferAddr, bytes.size, handles, formHandles, dispatchers, executors)
PdfDocument(bufferAddr, bytes.size, handles, formHandles)
} catch (t: Throwable) {
PdfiumBridge.nFreeBuffer(bufferAddr)
throw t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,10 @@ package dev.nucleusframework.pdfium.jvm

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger

internal object Pdfium {
private val counter = AtomicInteger()

val sharedDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "pdfium-shared").apply { isDaemon = true }
}.asCoroutineDispatcher()

fun newDispatcher(): Pair<CoroutineDispatcher, ExecutorService> {
val id = counter.incrementAndGet()
val exec = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "pdfium-worker-$id").apply {
isDaemon = true
priority = Thread.NORM_PRIORITY
}
}
return exec.asCoroutineDispatcher() to exec
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import androidx.compose.ui.graphics.asComposeImageBitmap
import dev.nucleusframework.pdfium.jvm.FPDF_ANNOT
import dev.nucleusframework.pdfium.jvm.Pdfium
import dev.nucleusframework.pdfium.jvm.PdfiumBridge
import java.util.concurrent.ExecutorService
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jetbrains.skia.Bitmap
Expand All @@ -29,8 +27,6 @@ internal actual class PdfDocument internal constructor(
// Per-document FPDF_FORMHANDLE (parallel to [handles]). May be 0 if PDFium refused to
// init the form-fill environment for that handle; render code falls back to no overlay.
private val formHandles: LongArray,
private val dispatchers: Array<CoroutineDispatcher>,
private val executors: Array<ExecutorService>,
) {
actual val pageCount: Int = PdfiumBridge.nGetPageCount(handles[0])
actual val metadata: PdfMetadata = PdfMetadata(
Expand All @@ -47,7 +43,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageSize(pageIndex: Int): PageSize {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext PageSize(0f, 0f)
Expand All @@ -69,7 +65,7 @@ internal actual class PdfDocument internal constructor(
quality: RenderQuality,
): ImageBitmap {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val info = ImageInfo.makeN32(widthPx, heightPx, ColorAlphaType.PREMUL)
val bitmap = Bitmap().apply { allocPixels(info) }
Expand Down Expand Up @@ -99,7 +95,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageText(pageIndex: Int): String {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext ""
Expand All @@ -113,7 +109,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageTextLayout(pageIndex: Int): PageTextLayout {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext PageTextLayout.Empty
Expand Down Expand Up @@ -159,7 +155,7 @@ internal actual class PdfDocument internal constructor(

actual suspend fun pageLinks(pageIndex: Int): PageLinks {
val slot = pickSlot()
return withContext(dispatchers[slot]) {
return withContext(Pdfium.sharedDispatcher) {
val handle = handles[slot]
val page = PdfiumBridge.nLoadPage(handle, pageIndex)
if (page == 0L) return@withContext PageLinks.Empty
Expand Down Expand Up @@ -190,7 +186,7 @@ internal actual class PdfDocument internal constructor(
actual fun close() {
runBlocking {
for (i in handles.indices) {
withContext(dispatchers[i]) {
withContext(Pdfium.sharedDispatcher) {
// Form-fill env must be torn down BEFORE its underlying document —
// FPDFDOC_ExitFormFillEnvironment dereferences the FPDF_DOCUMENT.
PdfiumBridge.nCloseFormEnv(formHandles[i])
Expand All @@ -199,7 +195,6 @@ internal actual class PdfDocument internal constructor(
}
}
PdfiumBridge.nFreeBuffer(bufferAddr)
executors.forEach { it.shutdown() }
}

private fun RenderQuality.toFlags(): Int = when (this) {
Expand Down Expand Up @@ -231,14 +226,11 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?)
}
handles[i] = h
}
val pairs = Array(POOL_SIZE) { Pdfium.newDispatcher() }
val dispatchers = Array(POOL_SIZE) { pairs[it].first }
val executors = Array(POOL_SIZE) { pairs[it].second }
// Init one form-fill env per document handle. PDFium tolerates docs without forms
// (the handle still returns non-zero) — FPDF_FFLDraw is a no-op for pages with
// no widgets, so eager init costs only a small struct allocation per document.
val formHandles = LongArray(POOL_SIZE) { PdfiumBridge.nInitFormEnv(handles[it]) }
PdfDocument(bufferAddr, bytes.size, handles, formHandles, dispatchers, executors)
PdfDocument(bufferAddr, bytes.size, handles, formHandles)
} catch (t: Throwable) {
PdfiumBridge.nFreeBuffer(bufferAddr)
throw t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,16 @@ package dev.nucleusframework.pdfium.jvm

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger

/**
* PDFium is not thread-safe. A single-threaded dispatcher per document gives us:
* - per-call serialization for a given document (no concurrent clobber of PDFium state)
* - independent docs render in parallel (no global lock)
* PDFium is not thread-safe, including across documents (FreeType's `FT_Library` is a
* process-wide singleton). Every PDFium call runs on [sharedDispatcher], which gives us:
* - global serialization (no concurrent clobber of PDFium or FreeType state)
* - stable carrier thread (useful for any thread-affine PDFium state)
*/
internal object Pdfium {
private val counter = AtomicInteger()

/**
* Shared dispatcher for library-level operations (init, open, close). A shared single
* thread prevents races during PDFium's global state setup. Per-document dispatchers
* (from [newDispatcher]) are used for everything else so documents render in parallel.
*/
val sharedDispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "pdfium-shared").apply { isDaemon = true }
}.asCoroutineDispatcher()

/** Create a fresh single-threaded dispatcher + the underlying executor. Caller owns shutdown. */
fun newDispatcher(): Pair<CoroutineDispatcher, ExecutorService> {
val id = counter.incrementAndGet()
val exec = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "pdfium-worker-$id").apply {
isDaemon = true
priority = Thread.NORM_PRIORITY
}
}
return exec.asCoroutineDispatcher() to exec
}
}