diff --git a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt index 8530bd3..69185f3 100644 --- a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt +++ b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt @@ -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 @@ -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, - private val executors: Array, ) { actual val pageCount: Int = PdfiumBridge.nGetPageCount(handles[0]) actual val metadata: PdfMetadata = PdfMetadata( @@ -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) @@ -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) @@ -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 "" @@ -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 @@ -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 @@ -169,7 +165,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. PdfiumBridge.nCloseFormEnv(formHandles[i]) PdfiumBridge.nCloseDocument(handles[i]) @@ -177,7 +173,6 @@ internal actual class PdfDocument internal constructor( } } PdfiumBridge.nFreeBuffer(bufferAddr) - executors.forEach { it.shutdown() } } private fun RenderQuality.toFlags(): Int = when (this) { @@ -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 diff --git a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt index c4238c1..82380ad 100644 --- a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt +++ b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt @@ -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 { - 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 - } } diff --git a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt index e570657..92e2c30 100644 --- a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt +++ b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt @@ -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 @@ -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, - private val executors: Array, ) { actual val pageCount: Int = PdfiumBridge.nGetPageCount(handles[0]) actual val metadata: PdfMetadata = PdfMetadata( @@ -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) @@ -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) } @@ -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 "" @@ -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 @@ -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 @@ -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]) @@ -199,7 +195,6 @@ internal actual class PdfDocument internal constructor( } } PdfiumBridge.nFreeBuffer(bufferAddr) - executors.forEach { it.shutdown() } } private fun RenderQuality.toFlags(): Int = when (this) { @@ -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 diff --git a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt index 0f332d0..cf8e6ca 100644 --- a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt +++ b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/Pdfium.kt @@ -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 { - 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 - } }