├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── drawable │ │ │ ├── outline_file_open_24.xml │ │ │ └── ic_launcher_background.xml │ │ ├── xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ │ ├── values-night │ │ │ └── themes.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ └── io │ │ │ └── legere │ │ │ └── pdfiumandroidkt │ │ │ ├── App.kt │ │ │ └── ui │ │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Type.kt │ │ │ └── Theme.kt │ │ │ ├── PdfiumFetcher.kt │ │ │ └── PdfViewer.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── pdfiumandroid ├── .gitignore ├── arrow │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── legere │ │ │ │ └── pdfiumandroid │ │ │ │ └── arrow │ │ │ │ ├── PdfiumArrorwExt.kt │ │ │ │ ├── PdfiumKtFErrors.kt │ │ │ │ ├── FindResultKtF.kt │ │ │ │ ├── PdfPageLinkKtF.kt │ │ │ │ ├── PdfiumCoreKtF.kt │ │ │ │ ├── PdfPageKtFCache.kt │ │ │ │ ├── PdfTextPageKtF.kt │ │ │ │ └── PdfDocumentKtF.kt │ │ └── androidTest │ │ │ ├── assets │ │ │ ├── f01.pdf │ │ │ └── pdf-test.pdf │ │ │ └── java │ │ │ └── io │ │ │ └── legere │ │ │ └── pdfiumandroid │ │ │ └── arrow │ │ │ ├── base │ │ │ ├── ByteArrayPdfiumSource.kt │ │ │ └── BasePDFTest.kt │ │ │ ├── PdfiumCoreKtFTest.kt │ │ │ ├── PdfPageLinkKtFTest.kt │ │ │ └── PdfDocumentKtFTest.kt │ ├── gradle.properties │ ├── proguard-rules.pro │ └── build.gradle.kts ├── gradle.properties ├── src │ ├── androidTest │ │ ├── assets │ │ │ ├── f01.pdf │ │ │ └── pdf-test.pdf │ │ └── java │ │ │ └── io │ │ │ └── legere │ │ │ └── pdfiumandroid │ │ │ ├── base │ │ │ ├── ByteArrayPdfiumSource.kt │ │ │ └── BasePDFTest.kt │ │ │ ├── PdfiumCoreTest.kt │ │ │ ├── suspend │ │ │ ├── PdfiumCoreKtTest.kt │ │ │ ├── PdfPageLinkKtTest.kt │ │ │ ├── PdfDocumentKtTest.kt │ │ │ └── PdfTextPageKtTest.kt │ │ │ ├── PdfPageLinkTest.kt │ │ │ └── PdfDocumentTest.kt │ ├── main │ │ ├── jniLibs │ │ │ ├── x86 │ │ │ │ └── libpdfium.so │ │ │ ├── x86_64 │ │ │ │ └── libpdfium.so │ │ │ ├── arm64-v8a │ │ │ │ └── libpdfium.so │ │ │ └── armeabi-v7a │ │ │ │ └── libpdfium.so │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── io │ │ │ │ └── legere │ │ │ │ └── pdfiumandroid │ │ │ │ ├── PdfPasswordException.kt │ │ │ │ ├── util │ │ │ │ ├── Size.kt │ │ │ │ ├── InitLock.kt │ │ │ │ ├── Config.kt │ │ │ │ ├── PdfiumNativeSourceBridge.kt │ │ │ │ └── PdfPageCacheBase.kt │ │ │ │ ├── PdfWriteCallback.kt │ │ │ │ ├── PdfiumSource.kt │ │ │ │ ├── suspend │ │ │ │ ├── FindResultKt.kt │ │ │ │ ├── PdfPageLinkKt.kt │ │ │ │ ├── PdfPageSuspendCacheBase.kt │ │ │ │ ├── PdfiumCoreKt.kt │ │ │ │ ├── PdfPageKtCache.kt │ │ │ │ ├── PdfTextPageKt.kt │ │ │ │ └── PdfDocumentKt.kt │ │ │ │ ├── FindResult.kt │ │ │ │ ├── Logger.kt │ │ │ │ ├── PdfPageCache.kt │ │ │ │ └── PdfPageLink.kt │ │ └── cpp │ │ │ ├── util.h │ │ │ ├── include │ │ │ ├── fpdf_catalog.h │ │ │ ├── fpdf_flatten.h │ │ │ ├── fpdf_searchex.h │ │ │ ├── fpdf_thumbnail.h │ │ │ ├── cpp │ │ │ │ ├── fpdf_scopers.h │ │ │ │ └── fpdf_deleters.h │ │ │ ├── utils │ │ │ │ ├── Errors.h │ │ │ │ └── Mutex.h │ │ │ ├── fpdf_javascript.h │ │ │ ├── fpdf_save.h │ │ │ ├── fpdf_ext.h │ │ │ ├── fpdf_ppo.h │ │ │ └── fpdf_fwlevent.h │ │ │ └── CMakeLists.txt │ └── test │ │ └── java │ │ └── io │ │ └── legere │ │ └── pdfiumandroid │ │ └── util │ │ └── PdfiumNativeSourceBridgeTest.kt ├── consumer-rules.pro └── proguard-rules.pro ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .idea └── kotlinc.xml ├── settings.gradle.kts ├── .github └── workflows │ ├── android.yml │ └── gradle-publish.yml ├── check ├── deploy.sh ├── gradle.properties ├── LICENSE ├── gradlew.bat ├── README.md ├── .gitignore └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /pdfiumandroid/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /pdfiumandroid/arrow/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /pdfiumandroid/arrow/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pdfiumandroid/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=pdfiumandroid 2 | POM_ARTIFACT_ID=pdfiumandroid 3 | POM_PACKAGING=aar 4 | 5 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=pdfiumandroid 2 | POM_ARTIFACT_ID=pdfiumandroid 3 | POM_PACKAGING=aar 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/assets/f01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/src/androidTest/assets/f01.pdf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pdfiumandroid/src/main/jniLibs/x86/libpdfium.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/src/main/jniLibs/x86/libpdfium.so -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/assets/pdf-test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/src/androidTest/assets/pdf-test.pdf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/assets/f01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/arrow/src/androidTest/assets/f01.pdf -------------------------------------------------------------------------------- /pdfiumandroid/src/main/jniLibs/x86_64/libpdfium.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/src/main/jniLibs/x86_64/libpdfium.so -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /pdfiumandroid/src/main/jniLibs/arm64-v8a/libpdfium.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/src/main/jniLibs/arm64-v8a/libpdfium.so -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/assets/pdf-test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/arrow/src/androidTest/assets/pdf-test.pdf -------------------------------------------------------------------------------- /pdfiumandroid/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/jniLibs/armeabi-v7a/libpdfium.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johngray1965/PdfiumAndroidKt/HEAD/pdfiumandroid/src/main/jniLibs/armeabi-v7a/libpdfium.so -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PdfiumAndroidKt 3 | MainActivity 4 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 06 14:34:08 EDT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfPasswordException.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid 4 | 5 | import java.io.IOException 6 | 7 | /** 8 | * PdfPasswordException is thrown when a password is required to open a document 9 | */ 10 | class PdfPasswordException( 11 | msg: String? = null, 12 | ) : IOException(msg) 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/legere/pdfiumandroidkt/App.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroidkt 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | import timber.log.Timber 6 | 7 | @HiltAndroidApp 8 | class App : Application() { 9 | override fun onCreate() { 10 | super.onCreate() 11 | Timber.plant(Timber.DebugTree()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/util/Size.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.util 2 | 3 | import androidx.annotation.Keep 4 | 5 | /** 6 | * Size is a simple value class that represents a width and height. 7 | * @property width the width 8 | * @property height the height 9 | */ 10 | @Keep 11 | data class Size( 12 | val width: Int, 13 | val height: Int, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/legere/pdfiumandroidkt/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("MagicNumber") 2 | 3 | package io.legere.pdfiumandroidkt.ui.theme 4 | 5 | import androidx.compose.ui.graphics.Color 6 | 7 | val Purple80 = Color(0xFFD0BCFF) 8 | val PurpleGrey80 = Color(0xFFCCC2DC) 9 | val Pink80 = Color(0xFFEFB8C8) 10 | 11 | val Purple40 = Color(0xFF6650a4) 12 | val PurpleGrey40 = Color(0xFF625b71) 13 | val Pink40 = Color(0xFF7D5260) 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_file_open_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfWriteCallback.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | /** 4 | * PdfWriteCallback is the calback interface for saveAsCopy 5 | */ 6 | interface PdfWriteCallback { 7 | /** 8 | * WriteBlock is called by native code to write a block of data 9 | * @param data the data to write 10 | * 11 | * note: The name need to be exactly what it is. 12 | * The native call is looking for is as WriteBlock 13 | * 14 | */ 15 | @Suppress("FunctionNaming", "FunctionName") 16 | fun WriteBlock(data: ByteArray?): Int 17 | } 18 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfiumArrorwExt.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | import arrow.core.Either 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.withContext 6 | 7 | suspend inline fun wrapEither( 8 | dispatcher: CoroutineDispatcher, 9 | crossinline block: () -> T, 10 | ): Either = 11 | withContext(dispatcher) { 12 | Either 13 | .catch { 14 | block() 15 | }.mapLeft { 16 | exceptionToPdfiumKtFError(it) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/util/InitLock.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.util 2 | 3 | import java.util.concurrent.Semaphore 4 | 5 | class InitLock { 6 | private val semaphore = Semaphore(0) 7 | private var isInitialized = false 8 | 9 | fun markReady() { 10 | isInitialized = true 11 | semaphore.release() 12 | } 13 | 14 | // We use a mutex to make sure only the 15 | // first thread waits on the semaphore 16 | @Synchronized 17 | fun waitForReady() { 18 | if (!isInitialized) { 19 | semaphore.acquire() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | @Suppress("UnstableApiUsage") 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | @Suppress("UnstableApiUsage") 12 | repositories { 13 | google() 14 | mavenLocal() 15 | mavenCentral() 16 | // maven(url = "https://s01.oss.sonatype.org/content/groups/staging/") 17 | } 18 | } 19 | 20 | rootProject.name = "pdfiumandroid" 21 | include(":app") 22 | include(":pdfiumandroid") 23 | include(":pdfiumandroid:arrow") 24 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | - name: Setup Android SDK 23 | uses: android-actions/setup-android@v2 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/io/legere/pdfiumandroidkt/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroidkt.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = 11 | Typography( 12 | bodyLarge = 13 | TextStyle( 14 | fontFamily = FontFamily.Default, 15 | fontWeight = FontWeight.Normal, 16 | fontSize = 16.sp, 17 | lineHeight = 24.sp, 18 | letterSpacing = 0.5.sp, 19 | ), 20 | ) 21 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfiumKtFErrors.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | sealed class PdfiumKtFErrors { 4 | data class RuntimeException( 5 | val message: String, 6 | ) : PdfiumKtFErrors() 7 | 8 | data class AlreadyClosed( 9 | val message: String, 10 | ) : PdfiumKtFErrors() 11 | 12 | data object ConstraintError : PdfiumKtFErrors() 13 | } 14 | 15 | fun exceptionToPdfiumKtFError(e: Throwable): PdfiumKtFErrors = 16 | if (e is IllegalStateException && e.message?.contains("Already closed") == true) { 17 | PdfiumKtFErrors.AlreadyClosed(e.message ?: "Unknown error") 18 | } else { 19 | PdfiumKtFErrors.RuntimeException(e.message ?: "Unknown error") 20 | } 21 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/util.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by John Gray on 6/6/23. 3 | // 4 | 5 | #ifndef PDFIUMANDROIDKT_UTIL_H 6 | #define PDFIUMANDROIDKT_UTIL_H 7 | 8 | #include 9 | extern "C" { 10 | #include 11 | } 12 | 13 | #include 14 | 15 | #define JNI_FUNC(retType, bindClass, name) JNIEXPORT retType JNICALL Java_com_shockwave_pdfium_##bindClass##_##name 16 | #define JNI_ARGS JNIEnv *env, jobject thiz 17 | 18 | #define LOG_TAG "jniPdfium" 19 | #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) 20 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) 21 | #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) 22 | 23 | #endif //PDFIUMANDROIDKT_UTIL_H 24 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/base/ByteArrayPdfiumSource.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.base 2 | 3 | import io.legere.pdfiumandroid.PdfiumSource 4 | 5 | class ByteArrayPdfiumSource( 6 | private val array: ByteArray, 7 | ) : PdfiumSource { 8 | override val length: Long 9 | get() = array.size.toLong() 10 | 11 | override fun read( 12 | position: Long, 13 | buffer: ByteArray, 14 | size: Int, 15 | ): Int { 16 | array.copyInto( 17 | destination = buffer, 18 | destinationOffset = 0, 19 | startIndex = position.toInt(), 20 | endIndex = position.toInt() + size, 21 | ) 22 | return size 23 | } 24 | 25 | override fun close() { 26 | // nothing to close 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/java/io/legere/pdfiumandroid/arrow/base/ByteArrayPdfiumSource.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow.base 2 | 3 | import io.legere.pdfiumandroid.PdfiumSource 4 | 5 | class ByteArrayPdfiumSource( 6 | private val array: ByteArray, 7 | ) : PdfiumSource { 8 | override val length: Long 9 | get() = array.size.toLong() 10 | 11 | override fun read( 12 | position: Long, 13 | buffer: ByteArray, 14 | size: Int, 15 | ): Int { 16 | array.copyInto( 17 | destination = buffer, 18 | destinationOffset = 0, 19 | startIndex = position.toInt(), 20 | endIndex = position.toInt() + size, 21 | ) 22 | return size 23 | } 24 | 25 | override fun close() { 26 | // nothing to close 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/util/Config.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.util 2 | 3 | import androidx.annotation.Keep 4 | import io.legere.pdfiumandroid.DefaultLogger 5 | import io.legere.pdfiumandroid.LoggerInterface 6 | 7 | var pdfiumConfig = Config() 8 | 9 | @Keep 10 | enum class AlreadyClosedBehavior { 11 | EXCEPTION, 12 | IGNORE, 13 | } 14 | 15 | @Keep 16 | data class Config( 17 | val logger: LoggerInterface = DefaultLogger(), 18 | val alreadyClosedBehavior: AlreadyClosedBehavior = AlreadyClosedBehavior.EXCEPTION, 19 | ) 20 | 21 | fun handleAlreadyClosed(isClosed: Boolean): Boolean { 22 | if (isClosed) { 23 | when (pdfiumConfig.alreadyClosedBehavior) { 24 | AlreadyClosedBehavior.EXCEPTION -> error("Already closed") 25 | AlreadyClosedBehavior.IGNORE -> 26 | pdfiumConfig.logger.d( 27 | "PdfiumCore", 28 | "Already closed", 29 | ) 30 | } 31 | } 32 | return isClosed 33 | } 34 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/java/io/legere/pdfiumandroid/arrow/base/BasePDFTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow.base 2 | 3 | import android.graphics.RectF 4 | import android.util.Log 5 | import androidx.test.platform.app.InstrumentationRegistry 6 | 7 | @Suppress("unused") 8 | open class BasePDFTest { 9 | // set to true to skip tests that are not implemented yet 10 | // set to false to force unimplemented tests to fail 11 | val notImplementedAssetValue = false 12 | 13 | val noResultRect = RectF(-1f, -1f, -1f, -1f) 14 | 15 | fun getPdfBytes(filename: String): ByteArray? { 16 | val appContext = InstrumentationRegistry.getInstrumentation().context 17 | val assetManager = appContext.assets 18 | try { 19 | val input = assetManager.open(filename) 20 | return input.readBytes() 21 | } catch (e: Exception) { 22 | Log.e(BasePDFTest::class.simpleName, "Ugh", e) 23 | } 24 | assetManager.close() 25 | return null 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/base/BasePDFTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.base 2 | 3 | import android.graphics.RectF 4 | import android.util.Log 5 | import androidx.test.platform.app.InstrumentationRegistry 6 | import io.legere.pdfiumandroid.PdfiumCoreTest 7 | 8 | @Suppress("unused") 9 | open class BasePDFTest { 10 | // set to true to skip tests that are not implemented yet 11 | // set to false to force unimplemented tests to fail 12 | val notImplementedAssetValue = false 13 | 14 | val noResultRect = RectF(-1f, -1f, -1f, -1f) 15 | 16 | fun getPdfBytes(filename: String): ByteArray? { 17 | val appContext = InstrumentationRegistry.getInstrumentation().context 18 | val assetManager = appContext.assets 19 | try { 20 | val input = assetManager.open(filename) 21 | return input.readBytes() 22 | } catch (e: Exception) { 23 | Log.e(PdfiumCoreTest::class.simpleName, "Ugh", e) 24 | } 25 | assetManager.close() 26 | return null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfiumSource.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | /** 4 | * An interface for providing custom data source to Pdfium. 5 | */ 6 | interface PdfiumSource : AutoCloseable { 7 | /** 8 | * Data length, in bytes 9 | */ 10 | val length: Long 11 | 12 | /** 13 | * Read data from the source. 14 | * 15 | * The position and size will never go out of range of the data source [length]. 16 | * It may be possible for Pdfium to call this function multiple times for the same position. 17 | * 18 | * @param position byte offset from the beginning of the data source 19 | * @param buffer the buffer to read data into. Always have enough space to read [size] bytes. 20 | * It should be filled starting from index 0. 21 | * @param size the number of bytes to read. Never 0. 22 | * @return number of bytes that was read, or a negative value to indicate an error. 23 | */ 24 | fun read( 25 | position: Long, 26 | buffer: ByteArray, 27 | size: Int, 28 | ): Int 29 | } 30 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/PdfiumCoreTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.google.common.truth.Truth.assertThat 5 | import io.legere.pdfiumandroid.base.BasePDFTest 6 | import io.legere.pdfiumandroid.base.ByteArrayPdfiumSource 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | @RunWith(AndroidJUnit4::class) 11 | class PdfiumCoreTest : BasePDFTest() { 12 | @Test 13 | fun newDocument() { 14 | val pdfBytes = getPdfBytes("f01.pdf") 15 | 16 | assertThat(pdfBytes).isNotNull() 17 | 18 | val pdfiumCore = PdfiumCore() 19 | val pdfDocument = pdfiumCore.newDocument(pdfBytes) 20 | 21 | assertThat(pdfDocument).isNotNull() 22 | } 23 | 24 | @Test 25 | fun newDocumentWithCustomSource() { 26 | val pdfBytes = getPdfBytes("f01.pdf") 27 | 28 | assertThat(pdfBytes).isNotNull() 29 | 30 | val pdfiumCore = PdfiumCore() 31 | val pdfDocument = pdfiumCore.newDocument(ByteArrayPdfiumSource(pdfBytes!!)) 32 | 33 | assertThat(pdfDocument).isNotNull() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/FindResultKt.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import io.legere.pdfiumandroid.FindResult 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.withContext 6 | import java.io.Closeable 7 | 8 | @Suppress("unused") 9 | class FindResultKt( 10 | private val findResult: FindResult, 11 | private val dispatcher: CoroutineDispatcher, 12 | ) : Closeable { 13 | suspend fun findNext(): Boolean = 14 | withContext(dispatcher) { 15 | findResult.findNext() 16 | } 17 | 18 | suspend fun findPrev(): Boolean = 19 | withContext(dispatcher) { 20 | findResult.findPrev() 21 | } 22 | 23 | suspend fun getSchResultIndex(): Int = 24 | withContext(dispatcher) { 25 | findResult.getSchResultIndex() 26 | } 27 | 28 | suspend fun getSchCount(): Int = 29 | withContext(dispatcher) { 30 | findResult.getSchCount() 31 | } 32 | 33 | suspend fun closeFind() { 34 | withContext(dispatcher) { 35 | findResult.closeFind() 36 | } 37 | } 38 | 39 | override fun close() { 40 | findResult.closeFind() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_catalog.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef PUBLIC_FPDF_CATALOG_H_ 6 | #define PUBLIC_FPDF_CATALOG_H_ 7 | 8 | // NOLINTNEXTLINE(build/include) 9 | #include "fpdfview.h" 10 | 11 | #ifdef __cplusplus 12 | extern "C" { 13 | #endif // __cplusplus 14 | 15 | // Experimental API. 16 | // 17 | // Determine if |document| represents a tagged PDF. 18 | // 19 | // For the definition of tagged PDF, See (see 10.7 "Tagged PDF" in PDF 20 | // Reference 1.7). 21 | // 22 | // document - handle to a document. 23 | // 24 | // Returns |true| iff |document| is a tagged PDF. 25 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV 26 | FPDFCatalog_IsTagged(FPDF_DOCUMENT document); 27 | 28 | // Experimental API. 29 | // Sets the language of |document| to |language|. 30 | // 31 | // document - handle to a document. 32 | // language - the language to set to. 33 | // 34 | // Returns TRUE on success. 35 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV 36 | FPDFCatalog_SetLanguage(FPDF_DOCUMENT document, FPDF_BYTESTRING language); 37 | 38 | #ifdef __cplusplus 39 | } // extern "C" 40 | #endif // __cplusplus 41 | 42 | #endif // PUBLIC_FPDF_CATALOG_H_ 43 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/util/PdfiumNativeSourceBridge.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.util 2 | 3 | import io.legere.pdfiumandroid.Logger 4 | import io.legere.pdfiumandroid.PdfiumSource 5 | 6 | internal class PdfiumNativeSourceBridge( 7 | private val source: PdfiumSource, 8 | ) { 9 | private var buffer: ByteArray? = null 10 | 11 | @Suppress("TooGenericExceptionCaught") 12 | fun read( 13 | position: Long, 14 | size: Long, 15 | ): Int = 16 | try { 17 | require(size <= Int.MAX_VALUE) { "size is too large" } 18 | val trimmedSize = size.toInt() 19 | 20 | var buffer = buffer 21 | if (buffer == null || buffer.size < size) { 22 | buffer = ByteArray(trimmedSize).also { this.buffer = it } 23 | } 24 | 25 | val bytesRead = source.read(position, buffer, trimmedSize) 26 | // Pdfium expects 0 for error while Java/Kotlin usually return a negative value 27 | if (bytesRead <= 0) 0 else bytesRead 28 | } catch (t: Throwable) { 29 | // This is to prevent the exception to go to the native code level 30 | Logger.e("PdfiumNativeSourceBridge", t, "read failed") 31 | 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/FindResultKtF.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | import arrow.core.Either 4 | import io.legere.pdfiumandroid.FindResult 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import java.io.Closeable 7 | 8 | @Suppress("unused") 9 | class FindResultKtF( 10 | private val findResult: FindResult, 11 | private val dispatcher: CoroutineDispatcher, 12 | ) : Closeable { 13 | suspend fun findNext(): Either = 14 | wrapEither(dispatcher) { 15 | findResult.findNext() 16 | } 17 | 18 | suspend fun findPrev(): Either = 19 | wrapEither(dispatcher) { 20 | findResult.findPrev() 21 | } 22 | 23 | suspend fun getSchResultIndex(): Either = 24 | wrapEither(dispatcher) { 25 | findResult.getSchResultIndex() 26 | } 27 | 28 | suspend fun getSchCount(): Either = 29 | wrapEither(dispatcher) { 30 | findResult.getSchCount() 31 | } 32 | 33 | suspend fun closeFind() { 34 | wrapEither(dispatcher) { 35 | findResult.closeFind() 36 | } 37 | } 38 | 39 | override fun close() { 40 | findResult.closeFind() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_flatten.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 | 7 | #ifndef PUBLIC_FPDF_FLATTEN_H_ 8 | #define PUBLIC_FPDF_FLATTEN_H_ 9 | 10 | // NOLINTNEXTLINE(build/include) 11 | #include "fpdfview.h" 12 | 13 | // Flatten operation failed. 14 | #define FLATTEN_FAIL 0 15 | // Flatten operation succeed. 16 | #define FLATTEN_SUCCESS 1 17 | // Nothing to be flattened. 18 | #define FLATTEN_NOTHINGTODO 2 19 | 20 | // Flatten for normal display. 21 | #define FLAT_NORMALDISPLAY 0 22 | // Flatten for print. 23 | #define FLAT_PRINT 1 24 | 25 | #ifdef __cplusplus 26 | extern "C" { 27 | #endif // __cplusplus 28 | 29 | // Flatten annotations and form fields into the page contents. 30 | // 31 | // page - handle to the page. 32 | // nFlag - One of the |FLAT_*| values denoting the page usage. 33 | // 34 | // Returns one of the |FLATTEN_*| values. 35 | // 36 | // Currently, all failures return |FLATTEN_FAIL| with no indication of the 37 | // cause. 38 | FPDF_EXPORT int FPDF_CALLCONV FPDFPage_Flatten(FPDF_PAGE page, int nFlag); 39 | 40 | #ifdef __cplusplus 41 | } // extern "C" 42 | #endif // __cplusplus 43 | 44 | #endif // PUBLIC_FPDF_FLATTEN_H_ 45 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/suspend/PdfiumCoreKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.google.common.truth.Truth.assertThat 5 | import io.legere.pdfiumandroid.base.BasePDFTest 6 | import io.legere.pdfiumandroid.base.ByteArrayPdfiumSource 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class PdfiumCoreKtTest : BasePDFTest() { 14 | @Test 15 | fun newDocument() = 16 | runTest { 17 | val pdfBytes = getPdfBytes("f01.pdf") 18 | 19 | assertThat(pdfBytes).isNotNull() 20 | 21 | val pdfiumCore = PdfiumCoreKt(Dispatchers.Unconfined) 22 | val pdfDocument = pdfiumCore.newDocument(pdfBytes) 23 | 24 | assertThat(pdfDocument).isNotNull() 25 | } 26 | 27 | @Test 28 | fun newDocumentWitCustomSource() = 29 | runTest { 30 | val pdfBytes = getPdfBytes("f01.pdf") 31 | 32 | assertThat(pdfBytes).isNotNull() 33 | 34 | val pdfiumCore = PdfiumCoreKt(Dispatchers.Unconfined) 35 | val pdfDocument = pdfiumCore.newDocument(ByteArrayPdfiumSource(pdfBytes!!)) 36 | 37 | assertThat(pdfDocument).isNotNull() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/PdfPageLinkKt.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import android.graphics.RectF 4 | import io.legere.pdfiumandroid.PdfPageLink 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.withContext 7 | import java.io.Closeable 8 | 9 | class PdfPageLinkKt( 10 | val pageLink: PdfPageLink, 11 | private val dispatcher: CoroutineDispatcher, 12 | ) : Closeable { 13 | suspend fun countWebLinks(): Int = 14 | withContext(dispatcher) { 15 | pageLink.countWebLinks() 16 | } 17 | 18 | suspend fun getURL( 19 | index: Int, 20 | length: Int, 21 | ): String? = 22 | withContext(dispatcher) { 23 | pageLink.getURL(index, length) 24 | } 25 | 26 | suspend fun countRects(index: Int): Int = 27 | withContext(dispatcher) { 28 | pageLink.countRects(index) 29 | } 30 | 31 | suspend fun getRect( 32 | linkIndex: Int, 33 | rectIndex: Int, 34 | ): RectF = 35 | withContext(dispatcher) { 36 | pageLink.getRect(linkIndex, rectIndex) 37 | } 38 | 39 | suspend fun getTextRange(index: Int): Pair = 40 | withContext(dispatcher) { 41 | pageLink.getTextRange(index) 42 | } 43 | 44 | override fun close() { 45 | pageLink.close() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/java/io/legere/pdfiumandroid/arrow/PdfiumCoreKtFTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.google.common.truth.Truth.assertThat 5 | import io.legere.pdfiumandroid.arrow.base.BasePDFTest 6 | import io.legere.pdfiumandroid.arrow.base.ByteArrayPdfiumSource 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.test.runTest 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class PdfiumCoreKtFTest : BasePDFTest() { 14 | @Test 15 | fun newDocument() = 16 | runTest { 17 | val pdfBytes = getPdfBytes("f01.pdf") 18 | 19 | assertThat(pdfBytes).isNotNull() 20 | 21 | val pdfiumCore = PdfiumCoreKtF(Dispatchers.Unconfined) 22 | val pdfDocument = pdfiumCore.newDocument(pdfBytes) 23 | 24 | assertThat(pdfDocument).isNotNull() 25 | } 26 | 27 | @Test 28 | fun newDocumentWithCustomSource() = 29 | runTest { 30 | val pdfBytes = getPdfBytes("f01.pdf") 31 | 32 | assertThat(pdfBytes).isNotNull() 33 | 34 | val pdfiumCore = PdfiumCoreKtF(Dispatchers.Unconfined) 35 | val pdfDocument = pdfiumCore.newDocument(ByteArrayPdfiumSource(pdfBytes!!)) 36 | 37 | assertThat(pdfDocument).isNotNull() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_searchex.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 | 7 | #ifndef PUBLIC_FPDF_SEARCHEX_H_ 8 | #define PUBLIC_FPDF_SEARCHEX_H_ 9 | 10 | // NOLINTNEXTLINE(build/include) 11 | #include "fpdfview.h" 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif // __cplusplus 16 | 17 | // Get the character index in |text_page| internal character list. 18 | // 19 | // text_page - a text page information structure. 20 | // nTextIndex - index of the text returned from FPDFText_GetText(). 21 | // 22 | // Returns the index of the character in internal character list. -1 for error. 23 | FPDF_EXPORT int FPDF_CALLCONV 24 | FPDFText_GetCharIndexFromTextIndex(FPDF_TEXTPAGE text_page, int nTextIndex); 25 | 26 | // Get the text index in |text_page| internal character list. 27 | // 28 | // text_page - a text page information structure. 29 | // nCharIndex - index of the character in internal character list. 30 | // 31 | // Returns the index of the text returned from FPDFText_GetText(). -1 for error. 32 | FPDF_EXPORT int FPDF_CALLCONV 33 | FPDFText_GetTextIndexFromCharIndex(FPDF_TEXTPAGE text_page, int nCharIndex); 34 | 35 | #ifdef __cplusplus 36 | } // extern "C" 37 | #endif // __cplusplus 38 | 39 | #endif // PUBLIC_FPDF_SEARCHEX_H_ 40 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/FindResult.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | import java.io.Closeable 4 | 5 | @Suppress("TooManyFunctions") 6 | class FindResult( 7 | val handle: FindHandle, 8 | ) : Closeable { 9 | private external fun nativeFindNext(findHandle: Long): Boolean 10 | 11 | private external fun nativeFindPrev(findHandle: Long): Boolean 12 | 13 | private external fun nativeGetSchResultIndex(findHandle: Long): Int 14 | 15 | private external fun nativeGetSchCount(findHandle: Long): Int 16 | 17 | private external fun nativeCloseFind(findHandle: Long) 18 | 19 | fun findNext(): Boolean { 20 | synchronized(PdfiumCore.lock) { 21 | return nativeFindNext(handle) 22 | } 23 | } 24 | 25 | fun findPrev(): Boolean { 26 | synchronized(PdfiumCore.lock) { 27 | return nativeFindPrev(handle) 28 | } 29 | } 30 | 31 | fun getSchResultIndex(): Int { 32 | synchronized(PdfiumCore.lock) { 33 | return nativeGetSchResultIndex(handle) 34 | } 35 | } 36 | 37 | fun getSchCount(): Int { 38 | synchronized(PdfiumCore.lock) { 39 | return nativeGetSchCount(handle) 40 | } 41 | } 42 | 43 | fun closeFind() { 44 | synchronized(PdfiumCore.lock) { 45 | nativeCloseFind(handle) 46 | } 47 | } 48 | 49 | override fun close() { 50 | nativeCloseFind(handle) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/Logger.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | import android.util.Log 4 | import androidx.annotation.Keep 5 | 6 | // At the moment we only do debug log with message, or error log with message and throwable 7 | // in the future, we might expand this. 8 | @Keep 9 | interface LoggerInterface { 10 | fun d( 11 | tag: String, 12 | message: String?, 13 | ) 14 | 15 | fun e( 16 | tag: String, 17 | t: Throwable?, 18 | message: String?, 19 | ) 20 | } 21 | 22 | @Suppress("MemberNameEqualsClassName") 23 | object Logger : LoggerInterface { 24 | private var logger: LoggerInterface? = null 25 | 26 | override fun d( 27 | tag: String, 28 | message: String?, 29 | ) { 30 | logger?.d(tag, message) 31 | } 32 | 33 | override fun e( 34 | tag: String, 35 | t: Throwable?, 36 | message: String?, 37 | ) { 38 | logger?.e(tag, t, message) 39 | } 40 | 41 | fun setLogger(logger: LoggerInterface) { 42 | this.logger = logger 43 | } 44 | } 45 | 46 | class DefaultLogger : LoggerInterface { 47 | override fun d( 48 | tag: String, 49 | message: String?, 50 | ) { 51 | message?.let { Log.d(tag, message) } 52 | } 53 | 54 | override fun e( 55 | tag: String, 56 | t: Throwable?, 57 | message: String?, 58 | ) { 59 | Log.e(tag, message, t) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfPageLinkKtF.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | import android.graphics.RectF 4 | import arrow.core.Either 5 | import io.legere.pdfiumandroid.PdfPageLink 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import java.io.Closeable 8 | 9 | class PdfPageLinkKtF( 10 | val pageLink: PdfPageLink, 11 | private val dispatcher: CoroutineDispatcher, 12 | ) : Closeable { 13 | suspend fun countWebLinks(): Either = 14 | wrapEither(dispatcher) { 15 | pageLink.countWebLinks() 16 | } 17 | 18 | suspend fun getURL( 19 | index: Int, 20 | length: Int, 21 | ): Either = 22 | wrapEither(dispatcher) { 23 | pageLink.getURL(index, length) 24 | } 25 | 26 | suspend fun countRects(index: Int): Either = 27 | wrapEither(dispatcher) { 28 | pageLink.countRects(index) 29 | } 30 | 31 | suspend fun getRect( 32 | linkIndex: Int, 33 | rectIndex: Int, 34 | ): Either = 35 | wrapEither(dispatcher) { 36 | pageLink.getRect(linkIndex, rectIndex) 37 | } 38 | 39 | suspend fun getTextRange(index: Int): Either> = 40 | wrapEither(dispatcher) { 41 | pageLink.getTextRange(index) 42 | } 43 | 44 | override fun close() { 45 | pageLink.close() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/legere/pdfiumandroidkt/ui/PdfiumFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroidkt.ui 2 | 3 | import androidx.core.graphics.drawable.toDrawable 4 | import coil.ImageLoader 5 | import coil.decode.DataSource 6 | import coil.fetch.DrawableResult 7 | import coil.fetch.FetchResult 8 | import coil.fetch.Fetcher 9 | import coil.request.Options 10 | import io.legere.pdfiumandroidkt.MainViewModel 11 | import timber.log.Timber 12 | 13 | class PdfiumFetcher( 14 | private val data: PdfiumFetcherData, 15 | private val options: Options, 16 | ) : Fetcher { 17 | override suspend fun fetch(): FetchResult? { 18 | Timber.d("fetch: ${data.page}") 19 | val bitmap = data.viewModel.getPage(data.page, data.width, data.height) 20 | if (bitmap == null) { 21 | Timber.d("fetch: bitmap is null") 22 | return null 23 | } 24 | return DrawableResult( 25 | drawable = bitmap.toDrawable(options.context.resources), 26 | isSampled = false, 27 | dataSource = DataSource.MEMORY, 28 | ) 29 | } 30 | 31 | class Factory : Fetcher.Factory { 32 | override fun create( 33 | data: PdfiumFetcherData, 34 | options: Options, 35 | imageLoader: ImageLoader, 36 | ): Fetcher = PdfiumFetcher(data, options) 37 | } 38 | } 39 | 40 | data class PdfiumFetcherData( 41 | val page: Int, 42 | val width: Int, 43 | val height: Int, 44 | val density: Int, 45 | val viewModel: MainViewModel, 46 | ) 47 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/util/PdfPageCacheBase.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.util 2 | 3 | import com.google.common.cache.CacheBuilder 4 | import com.google.common.cache.CacheLoader 5 | import com.google.common.cache.LoadingCache 6 | import com.google.common.cache.RemovalListener 7 | 8 | private const val CACHE_SIZE = 64L 9 | 10 | /** 11 | * A thread-safe, concurrent LRU cache for PDF page objects. 12 | * This base class holds the core Guava LoadingCache logic. 13 | */ 14 | abstract class PdfPageCacheBase( 15 | maxSize: Long = CACHE_SIZE, 16 | ) : AutoCloseable { 17 | private val removalListener = 18 | RemovalListener { 19 | it.value?.close() 20 | } 21 | 22 | private val pageCache: LoadingCache = 23 | CacheBuilder 24 | .newBuilder() 25 | .maximumSize(maxSize) 26 | .removalListener(removalListener) 27 | .build( 28 | CacheLoader.from { pageIndex -> 29 | openPageAndText(pageIndex) 30 | }, 31 | ) 32 | 33 | /** 34 | * Abstract method to be implemented by subclasses to open a page and its text page. 35 | */ 36 | protected abstract fun openPageAndText(pageIndex: Int): H 37 | 38 | /** 39 | * Gets the page and text page holder from the cache, creating it if necessary. 40 | */ 41 | fun get(pageIndex: Int): H = pageCache.get(pageIndex) 42 | 43 | /** 44 | * Closes all currently cached pages. 45 | */ 46 | override fun close() { 47 | pageCache.invalidateAll() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 6 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle 7 | 8 | name: Gradle Package 9 | 10 | on: 11 | release: 12 | types: [created] 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up JDK 11 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '11' 28 | distribution: 'temurin' 29 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 30 | settings-path: ${{ github.workspace }} # location for the settings.xml file 31 | 32 | - name: Build with Gradle 33 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 34 | with: 35 | arguments: build 36 | 37 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in 38 | # the publishing section of your build.gradle 39 | - name: Publish to GitHub Packages 40 | uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 41 | with: 42 | arguments: publish 43 | env: 44 | USERNAME: ${{ github.actor }} 45 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /check: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | OPTIND=1 # Reset in case getopts has been used previously in the shell. 6 | CLEAN=clean 7 | 8 | while getopts "h?cs" opt; do 9 | case "$opt" in 10 | h|\?) 11 | echo "Usage: $0 [-v] [-n name]" 12 | exit 0 13 | ;; 14 | c) 15 | ;; 16 | s) CLEAN= 17 | ;; 18 | esac 19 | done 20 | 21 | shift $((OPTIND-1)) 22 | 23 | [ "${1:-}" = "--" ] && shift 24 | 25 | nohup java -jar adbserver-desktop.jar > /dev/null 2>&1 & 26 | serverPID=$! 27 | 28 | #./gradlew --continue clean detekt lint testDebugUnitTest reader:connectedDebugAndroidTest readerData:connectedDebugAndroidTest 29 | #./gradlew clean detekt lint testDebugUnitTest reader:connectedDebugAndroidTest readerData:connectedDebugAndroidTest && ./gradlew reader:connectedDebugAndroidTest -Pcucumber 30 | # for i in io.legere.readerdata.test voicedream.reader voicedream.reader.test; do 31 | # adb shell pm uninstall $i 32 | # done || true 33 | #./gradlew clean detekt lint testDebugUnitTest reader:pixel2DebugAndroidTest readerData:pixel2DebugAndroidTest 34 | # for i in io.legere.readerdata.test voicedream.reader voicedream.reader.test; do 35 | # adb shell pm uninstall $i 36 | # done || true 37 | #./gradlew reader:pixel2DebugAndroidTest -Pcucumber 38 | 39 | # ./gradlew clean detekt lint testDebugUnitTest reader:pixel2DebugAndroidTest readerData:pixel2DebugAndroidTest && ./gradlew reader:pixel2DebugAndroidTest -Pcucumber 40 | # 41 | #./gradlew clean detekt lint testDebugUnitTest pixel2DebugAndroidTest && ./gradlew pixel2DebugAndroidTest -Pcucumber 42 | ./gradlew $CLEAN detekt lint && \ 43 | ./gradlew testDebugUnitTest && \ 44 | ./gradlew connectedAndroidTest 45 | 46 | 47 | kill $serverPID 48 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ -z "$JRELEASER_GPG_PUBLIC_KEY" ]; then 4 | echo "❌ ERROR: JRELEASER_GPG_PUBLIC_KEY is not set." 5 | echo "-----------------------------------------------------" 6 | echo "Please set the required environment variables for signing." 7 | echo "You can use the following commands:" 8 | echo "" 9 | echo " export JRELEASER_GPG_PUBLIC_KEY=\$(gpg --export --armor YOUR_KEY_ID)" 10 | echo " export JRELEASER_GPG_SECRET_KEY=\$(gpg --export-secret-keys --armor YOUR_KEY_ID)" 11 | echo " export JRELEASER_GPG_PASSPHRASE='your-secret-gpg-passphrase'" 12 | echo "" 13 | echo "Replace YOUR_KEY_ID and the passphrase with your actual credentials." 14 | echo "-----------------------------------------------------" 15 | exit 1 # Exit with an error code 16 | fi 17 | 18 | # Check for the secret key variable 19 | if [ -z "$JRELEASER_GPG_SECRET_KEY" ]; then 20 | echo "❌ ERROR: JRELEASER_GPG_SECRET_KEY is not set." 21 | echo "--> Please check the instructions above and ensure all three variables are exported." 22 | exit 1 23 | fi 24 | 25 | # Check for the passphrase variable 26 | if [ -z "$JRELEASER_GPG_PASSPHRASE" ]; then 27 | echo "❌ ERROR: JRELEASER_GPG_PASSPHRASE is not set." 28 | echo "--> Please check the instructions above and ensure all three variables are exported." 29 | exit 1 30 | fi 31 | 32 | echo "✅ JReleaser signing environment variables are set. Proceeding with release..." 33 | echo "" 34 | 35 | ./gradlew clean 36 | mkdir -p pdfiumandroid/arrow/build/jreleaser 37 | mkdir -p pdfiumandroid/build/target/staging-deploy 38 | mkdir -p pdfiumandroid/arrow/build/target/staging-deploy 39 | ./gradlew publish 40 | ./gradlew jreleaserFullRelease 41 | ./gradlew :pdfiumandroid:arrow:jreleaserRelease 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /pdfiumandroid/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | # Uncomment this to preserve the line number information for 2 | # debugging stack traces. 3 | -keepattributes SourceFile,LineNumberTable 4 | 5 | # If you keep the line number information, uncomment this to 6 | # hide the original source file name. 7 | #-renamesourcefileattribute SourceFile 8 | 9 | -keep class io.legere.pdfiumandroid.** { *; } 10 | 11 | -keep interface io.legere.pdfiumandroid.** { public *; } 12 | 13 | -keepclasseswithmembernames class io.legere.pdfiumandroid.** { 14 | public ; 15 | } 16 | 17 | -keep class * extends io.legere.pdfiumandroid.LoggerInterface { *; } 18 | -keep class io.legere.pdfiumandroid.suspend.PdfDocumentKt { *; } 19 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfDocumentKt { 20 | public (...); 21 | } 22 | -keep class io.legere.pdfiumandroid.suspend.PdfPageKt { *; } 23 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfPageKt { 24 | public (...); 25 | } 26 | -keep class io.legere.pdfiumandroid.suspend.PdfTextPageKt { *; } 27 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfTextPageKt { 28 | public (...); 29 | } 30 | -keep class io.legere.pdfiumandroid.suspend.PdfiumCoreKt { *; } 31 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfiumCoreKt { 32 | public (...); 33 | } 34 | -keep class io.legere.pdfiumandroid.util.AlreadyClosedBehavior { *; } 35 | -keepclassmembers public class io.legere.pdfiumandroid.util.AlreadyClosedBehavior { 36 | public (...); 37 | } 38 | -keep class io.legere.pdfiumandroid.util.Config { *; } 39 | -keepclassmembers public class io.legere.pdfiumandroid.util.Config { 40 | public (...); 41 | } 42 | -keep class io.legere.pdfiumandroid.util.Size { *; } 43 | -keepclassmembers public class io.legere.pdfiumandroid.util.Size { 44 | public (...); 45 | } 46 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/PdfPageSuspendCacheBase.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import com.google.common.cache.Cache 4 | import com.google.common.cache.CacheBuilder 5 | import com.google.common.cache.RemovalListener 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Deferred 9 | import kotlinx.coroutines.async 10 | import kotlinx.coroutines.launch 11 | 12 | private const val CACHE_SIZE = 64L 13 | 14 | /** 15 | * A thread-safe, concurrent LRU cache for PDF page objects. 16 | * This base class holds the core Guava LoadingCache logic. 17 | */ 18 | abstract class PdfPageSuspendCacheBase( 19 | dispatcher: CoroutineDispatcher, 20 | maxSize: Long = CACHE_SIZE, 21 | ) : AutoCloseable { 22 | private val scope: CoroutineScope = CoroutineScope(dispatcher) 23 | 24 | private val cache: Cache> 25 | 26 | init { 27 | val removalListener = 28 | RemovalListener> { notification -> 29 | notification.value?.let { deferred -> 30 | if (deferred.isCompleted) { 31 | scope.launch { 32 | runCatching { deferred.await().close() } 33 | } 34 | } else { 35 | deferred.cancel() 36 | } 37 | } 38 | } 39 | cache = 40 | CacheBuilder 41 | .newBuilder() 42 | .maximumSize(maxSize) 43 | .removalListener(removalListener) 44 | .build() 45 | } 46 | 47 | protected abstract suspend fun openPageAndText(pageIndex: Int): H 48 | 49 | suspend fun get(pageIndex: Int): H { 50 | val deferred = 51 | cache.asMap().computeIfAbsent(pageIndex) { key -> 52 | scope.async { 53 | openPageAndText(key) 54 | } 55 | } 56 | return deferred.await() 57 | } 58 | 59 | override fun close() { 60 | cache.invalidateAll() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_thumbnail.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef PUBLIC_FPDF_THUMBNAIL_H_ 6 | #define PUBLIC_FPDF_THUMBNAIL_H_ 7 | 8 | #include 9 | 10 | // NOLINTNEXTLINE(build/include) 11 | #include "fpdfview.h" 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | // Experimental API. 18 | // Gets the decoded data from the thumbnail of |page| if it exists. 19 | // This only modifies |buffer| if |buflen| less than or equal to the 20 | // size of the decoded data. Returns the size of the decoded 21 | // data or 0 if thumbnail DNE. Optional, pass null to just retrieve 22 | // the size of the buffer needed. 23 | // 24 | // page - handle to a page. 25 | // buffer - buffer for holding the decoded image data. 26 | // buflen - length of the buffer in bytes. 27 | FPDF_EXPORT unsigned long FPDF_CALLCONV 28 | FPDFPage_GetDecodedThumbnailData(FPDF_PAGE page, 29 | void* buffer, 30 | unsigned long buflen); 31 | 32 | // Experimental API. 33 | // Gets the raw data from the thumbnail of |page| if it exists. 34 | // This only modifies |buffer| if |buflen| is less than or equal to 35 | // the size of the raw data. Returns the size of the raw data or 0 36 | // if thumbnail DNE. Optional, pass null to just retrieve the size 37 | // of the buffer needed. 38 | // 39 | // page - handle to a page. 40 | // buffer - buffer for holding the raw image data. 41 | // buflen - length of the buffer in bytes. 42 | FPDF_EXPORT unsigned long FPDF_CALLCONV 43 | FPDFPage_GetRawThumbnailData(FPDF_PAGE page, 44 | void* buffer, 45 | unsigned long buflen); 46 | 47 | // Experimental API. 48 | // Returns the thumbnail of |page| as a FPDF_BITMAP. Returns a nullptr 49 | // if unable to access the thumbnail's stream. 50 | // 51 | // page - handle to a page. 52 | FPDF_EXPORT FPDF_BITMAP FPDF_CALLCONV 53 | FPDFPage_GetThumbnailAsBitmap(FPDF_PAGE page); 54 | 55 | #ifdef __cplusplus 56 | } 57 | #endif 58 | 59 | #endif // PUBLIC_FPDF_THUMBNAIL_H_ 60 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | 25 | VERSION_NAME=1.0.35 26 | VERSION_CODE=6 27 | GROUP=io.legere 28 | 29 | POM_DESCRIPTION=Pdfium Android binding with Bitmap rendering ( >= API 24 ) 30 | POM_URL=https://github.com/johngray1965/PdfiumAndroidKt 31 | POM_SCM_URL=git@github.com:johngray1965/PdfiumAndroidKt.git 32 | POM_SCM_CONNECTION=scm:git@github.com:johngray1965/PdfiumAndroidKt.git 33 | POM_SCM_DEV_CONNECTION=scm:git@github.com:johngray1965/PdfiumAndroidKt.git 34 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 35 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 36 | POM_LICENCE_DIST=repo 37 | POM_DEVELOPER_ID=johngray1965 38 | POM_DEVELOPER_NAME=John Gray 39 | POM_PACKAGING=aar 40 | 41 | #RELEASE_REPOSITORY_URL=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 42 | #SNAPSHOT_REPOSITORY_URL=https://s01.oss.sonatype.org/content/repositories/snapshots 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original work Copyright 2015 Bekket McClane 2 | Modified work Copyright 2016 Bartosz Schiller 3 | Modified work Copyright 2023 John Gray 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | // Copyright 2014 PDFium Authors. All rights reserved. 18 | // 19 | // Redistribution and use in source and binary forms, with or without 20 | // modification, are permitted provided that the following conditions are 21 | // met: 22 | // 23 | // * Redistributions of source code must retain the above copyright 24 | // notice, this list of conditions and the following disclaimer. 25 | // * Redistributions in binary form must reproduce the above 26 | // copyright notice, this list of conditions and the following disclaimer 27 | // in the documentation and/or other materials provided with the 28 | // distribution. 29 | // * Neither the name of Google Inc. nor the names of its 30 | // contributors may be used to endorse or promote products derived from 31 | // this software without specific prior written permission. 32 | // 33 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 34 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 35 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 36 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 37 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 38 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 39 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 40 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 41 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 42 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 43 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /app/src/main/java/io/legere/pdfiumandroidkt/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroidkt.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val darkColorScheme = 19 | darkColorScheme( 20 | primary = Purple80, 21 | secondary = PurpleGrey80, 22 | tertiary = Pink80, 23 | ) 24 | 25 | private val lightColorScheme = 26 | lightColorScheme( 27 | primary = Purple40, 28 | secondary = PurpleGrey40, 29 | tertiary = Pink40, 30 | ) 31 | 32 | @Composable 33 | @Suppress("FunctionNaming", "ktlint:standard:function-naming") 34 | fun PdfiumAndroidKtTheme( 35 | darkTheme: Boolean = isSystemInDarkTheme(), 36 | // Dynamic color is available on Android 12+ 37 | dynamicColor: Boolean = true, 38 | content: @Composable () -> Unit, 39 | ) { 40 | val colorScheme = 41 | when { 42 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 43 | val context = LocalContext.current 44 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 45 | } 46 | 47 | darkTheme -> darkColorScheme 48 | else -> lightColorScheme 49 | } 50 | val view = LocalView.current 51 | if (!view.isInEditMode) { 52 | SideEffect { 53 | val window = (view.context as Activity).window 54 | window.statusBarColor = colorScheme.primary.toArgb() 55 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 56 | } 57 | } 58 | 59 | MaterialTheme( 60 | colorScheme = colorScheme, 61 | typography = Typography, 62 | content = content, 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keep class io.legere.pdfiumandroid.** { *; } 23 | 24 | -keep interface io.legere.pdfiumandroid.** { public *; } 25 | 26 | -keepclasseswithmembernames class io.legere.pdfiumandroid.** { 27 | public ; 28 | } 29 | 30 | -keep class * extends io.legere.pdfiumandroid.LoggerInterface { *; } 31 | -keep class io.legere.pdfiumandroid.suspend.PdfDocumentKt { *; } 32 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfDocumentKt { 33 | public (...); 34 | } 35 | -keep class io.legere.pdfiumandroid.suspend.PdfPageKt { *; } 36 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfPageKt { 37 | public (...); 38 | } 39 | -keep class io.legere.pdfiumandroid.suspend.PdfTextPageKt { *; } 40 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfTextPageKt { 41 | public (...); 42 | } 43 | -keep class io.legere.pdfiumandroid.suspend.PdfiumCoreKt { *; } 44 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfiumCoreKt { 45 | public (...); 46 | } 47 | -keep class io.legere.pdfiumandroid.util.AlreadyClosedBehavior { *; } 48 | -keepclassmembers public class io.legere.pdfiumandroid.util.AlreadyClosedBehavior { 49 | public (...); 50 | } 51 | -keep class io.legere.pdfiumandroid.util.Config { *; } 52 | -keepclassmembers public class io.legere.pdfiumandroid.util.Config { 53 | public (...); 54 | } 55 | -keep class io.legere.pdfiumandroid.util.Size { *; } 56 | -keepclassmembers public class io.legere.pdfiumandroid.util.Size { 57 | public (...); 58 | } 59 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfPageCache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid 4 | 5 | import io.legere.pdfiumandroid.util.PdfPageCacheBase 6 | 7 | /** 8 | * A cache for pages of a PDF document. 9 | * 10 | * This class is thread-safe. 11 | * 12 | * To use this class, you need to provide a `pdfDocument` and a `pageHolderFactory`. 13 | * The `pageHolderFactory` is a lambda that takes a `PdfPage` and a `PdfTextPage` and returns an instance of `H`. 14 | * 15 | * The generic type `H` is a holder for the page and text page. It can be a simple `PageHolder` or a custom class. 16 | * 17 | * Example usage with `PageHolder`: 18 | * ``` 19 | * val pageCache = PdfPageCache(pdfDocument) { page, textPage -> 20 | * PageHolder(page, textPage) 21 | * } 22 | * ``` 23 | * 24 | * Example usage with a custom holder: 25 | * ``` 26 | * data class CustomPageHolder( 27 | * val page: PdfPage, 28 | * val textPage: PdfTextPage, 29 | * val someOtherData: String 30 | * ) : AutoCloseable { 31 | * override fun close() { 32 | * try { 33 | * textPage.close() 34 | * } finally { 35 | * page.close() 36 | * } 37 | * } 38 | * } 39 | * 40 | * val pageCache = PdfPageCache(pdfDocument) { page, textPage -> 41 | * CustomPageHolder(page, textPage, "some other data") 42 | * } 43 | * ``` 44 | * 45 | * @param H The type of the holder for the page and text page. Must be [AutoCloseable]. 46 | * @property pdfDocument The [PdfDocument] to cache pages from. 47 | * @property pageHolderFactory A factory for creating a holder for a page and text page. 48 | */ 49 | class PdfPageCache( 50 | private val pdfDocument: PdfDocument, 51 | private val pageHolderFactory: (PdfPage, PdfTextPage) -> H, 52 | ) : PdfPageCacheBase() { 53 | override fun openPageAndText(pageIndex: Int): H { 54 | val page = pdfDocument.openPage(pageIndex) 55 | val textPage = page.openTextPage() 56 | return pageHolderFactory(page, textPage) 57 | } 58 | } 59 | 60 | data class PageHolder( 61 | val page: TPage, 62 | val textPage: TTextPage, 63 | ) : AutoCloseable { 64 | override fun close() { 65 | try { 66 | textPage.close() 67 | } finally { 68 | page.close() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pdfiumandroid/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | -keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class io.legere.pdfiumandroid.** { *; } 24 | 25 | -keep interface io.legere.pdfiumandroid.** { public *; } 26 | -keep class * extends io.legere.pdfiumandroid.LoggerInterface { *; } 27 | -dontwarn java.lang.invoke.StringConcatFactory 28 | -keepclasseswithmembernames class io.legere.pdfiumandroid.** { 29 | public ; 30 | } 31 | 32 | -keep class io.legere.pdfiumandroid.suspend.PdfDocumentKt { *; } 33 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfDocumentKt { 34 | public (...); 35 | } 36 | -keep class io.legere.pdfiumandroid.suspend.PdfPageKt { *; } 37 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfPageKt { 38 | public (...); 39 | } 40 | -keep class io.legere.pdfiumandroid.suspend.PdfTextPageKt { *; } 41 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfTextPageKt { 42 | public (...); 43 | } 44 | -keep class io.legere.pdfiumandroid.suspend.PdfiumCoreKt { *; } 45 | -keepclassmembers public class io.legere.pdfiumandroid.suspend.PdfiumCoreKt { 46 | public (...); 47 | } 48 | -keep class io.legere.pdfiumandroid.util.AlreadyClosedBehavior { *; } 49 | -keepclassmembers public class io.legere.pdfiumandroid.util.AlreadyClosedBehavior { 50 | public (...); 51 | } 52 | -keep class io.legere.pdfiumandroid.util.Config { *; } 53 | -keepclassmembers public class io.legere.pdfiumandroid.util.Config { 54 | public (...); 55 | } 56 | -keep class io.legere.pdfiumandroid.util.Size { *; } 57 | -keepclassmembers public class io.legere.pdfiumandroid.util.Size { 58 | public (...); 59 | } 60 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/cpp/fpdf_scopers.h: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef PUBLIC_CPP_FPDF_SCOPERS_H_ 6 | #define PUBLIC_CPP_FPDF_SCOPERS_H_ 7 | 8 | #include 9 | #include 10 | 11 | #include "fpdf_deleters.h" 12 | 13 | // Versions of FPDF types that clean up the object at scope exit. 14 | 15 | using ScopedFPDFAnnotation = 16 | std::unique_ptr::type, 17 | FPDFAnnotationDeleter>; 18 | 19 | using ScopedFPDFAvail = 20 | std::unique_ptr::type, FPDFAvailDeleter>; 21 | 22 | using ScopedFPDFBitmap = 23 | std::unique_ptr::type, FPDFBitmapDeleter>; 24 | 25 | using ScopedFPDFClipPath = 26 | std::unique_ptr::type, 27 | FPDFClipPathDeleter>; 28 | 29 | using ScopedFPDFDocument = 30 | std::unique_ptr::type, 31 | FPDFDocumentDeleter>; 32 | 33 | using ScopedFPDFFont = 34 | std::unique_ptr::type, FPDFFontDeleter>; 35 | 36 | using ScopedFPDFFormHandle = 37 | std::unique_ptr::type, 38 | FPDFFormHandleDeleter>; 39 | 40 | using ScopedFPDFJavaScriptAction = 41 | std::unique_ptr::type, 42 | FPDFJavaScriptActionDeleter>; 43 | 44 | using ScopedFPDFPage = 45 | std::unique_ptr::type, FPDFPageDeleter>; 46 | 47 | using ScopedFPDFPageLink = 48 | std::unique_ptr::type, 49 | FPDFPageLinkDeleter>; 50 | 51 | using ScopedFPDFPageObject = 52 | std::unique_ptr::type, 53 | FPDFPageObjectDeleter>; 54 | 55 | using ScopedFPDFStructTree = 56 | std::unique_ptr::type, 57 | FPDFStructTreeDeleter>; 58 | 59 | using ScopedFPDFTextFind = 60 | std::unique_ptr::type, 61 | FPDFTextFindDeleter>; 62 | 63 | using ScopedFPDFTextPage = 64 | std::unique_ptr::type, 65 | FPDFTextPageDeleter>; 66 | 67 | #endif // PUBLIC_CPP_FPDF_SCOPERS_H_ 68 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.12...4.1.0) 7 | # Declares and names the project. 8 | 9 | project("pdfiumandroid") 10 | 11 | # Creates and names a library, sets it as either STATIC 12 | # or SHARED, and provides the relative paths to its source code. 13 | # You can define multiple libraries, and CMake builds them for you. 14 | # Gradle automatically packages shared libraries with your APK. 15 | 16 | add_library( # Sets the name of the library. 17 | pdfiumandroid 18 | 19 | # Sets the library as a shared library. 20 | SHARED 21 | 22 | # Provides a relative path to your source file(s). 23 | pdfiumandroid.cpp) 24 | 25 | # Searches for a specified prebuilt library and stores the path as a 26 | # variable. Because CMake includes system libraries in the search path by 27 | # default, you only need to specify the name of the public NDK library 28 | # you want to add. CMake verifies that the library exists before 29 | # completing its build. 30 | 31 | find_library( # Sets the name of the path variable. 32 | log-lib 33 | 34 | # Specifies the name of the NDK library that 35 | # you want CMake to locate. 36 | log) 37 | 38 | find_library( # Sets the name of the path variable. 39 | android-lib 40 | 41 | # Specifies the name of the NDK library that 42 | # you want CMake to locate. 43 | android) 44 | 45 | find_library( # Sets the name of the path variable. 46 | jnigraphics-lib 47 | 48 | # Specifies the name of the NDK library that 49 | # you want CMake to locate. 50 | jnigraphics) 51 | 52 | # Specifies libraries CMake should link to your target library. You 53 | # can link multiple libraries, such as libraries you define in this 54 | # build script, prebuilt third-party libraries, or system libraries. 55 | 56 | target_link_libraries( # Specifies the target library. 57 | pdfiumandroid 58 | 59 | libpdfium 60 | 61 | # Links the target library to the log library 62 | # included in the NDK. 63 | ${log-lib} 64 | ${android-lib} 65 | ${jnigraphics-lib} 66 | ) 67 | 68 | 69 | add_library( libpdfium SHARED IMPORTED ) 70 | 71 | set_target_properties( libpdfium PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libpdfium.so) 72 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/cpp/fpdf_deleters.h: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef PUBLIC_CPP_FPDF_DELETERS_H_ 6 | #define PUBLIC_CPP_FPDF_DELETERS_H_ 7 | 8 | #include "../fpdf_annot.h" 9 | #include "../fpdf_dataavail.h" 10 | #include "../fpdf_edit.h" 11 | #include "../fpdf_formfill.h" 12 | #include "../fpdf_javascript.h" 13 | #include "../fpdf_structtree.h" 14 | #include "../fpdf_text.h" 15 | #include "../fpdf_transformpage.h" 16 | #include "../fpdfview.h" 17 | 18 | // Custom deleters for using FPDF_* types with std::unique_ptr<>. 19 | 20 | struct FPDFAnnotationDeleter { 21 | inline void operator()(FPDF_ANNOTATION annot) { FPDFPage_CloseAnnot(annot); } 22 | }; 23 | 24 | struct FPDFAvailDeleter { 25 | inline void operator()(FPDF_AVAIL avail) { FPDFAvail_Destroy(avail); } 26 | }; 27 | 28 | struct FPDFBitmapDeleter { 29 | inline void operator()(FPDF_BITMAP bitmap) { FPDFBitmap_Destroy(bitmap); } 30 | }; 31 | 32 | struct FPDFClipPathDeleter { 33 | inline void operator()(FPDF_CLIPPATH clip_path) { 34 | FPDF_DestroyClipPath(clip_path); 35 | } 36 | }; 37 | 38 | struct FPDFDocumentDeleter { 39 | inline void operator()(FPDF_DOCUMENT doc) { FPDF_CloseDocument(doc); } 40 | }; 41 | 42 | struct FPDFFontDeleter { 43 | inline void operator()(FPDF_FONT font) { FPDFFont_Close(font); } 44 | }; 45 | 46 | struct FPDFFormHandleDeleter { 47 | inline void operator()(FPDF_FORMHANDLE form) { 48 | FPDFDOC_ExitFormFillEnvironment(form); 49 | } 50 | }; 51 | 52 | struct FPDFJavaScriptActionDeleter { 53 | inline void operator()(FPDF_JAVASCRIPT_ACTION javascript) { 54 | FPDFDoc_CloseJavaScriptAction(javascript); 55 | } 56 | }; 57 | 58 | struct FPDFPageDeleter { 59 | inline void operator()(FPDF_PAGE page) { FPDF_ClosePage(page); } 60 | }; 61 | 62 | struct FPDFPageLinkDeleter { 63 | inline void operator()(FPDF_PAGELINK pagelink) { 64 | FPDFLink_CloseWebLinks(pagelink); 65 | } 66 | }; 67 | 68 | struct FPDFPageObjectDeleter { 69 | inline void operator()(FPDF_PAGEOBJECT object) { 70 | FPDFPageObj_Destroy(object); 71 | } 72 | }; 73 | 74 | struct FPDFStructTreeDeleter { 75 | inline void operator()(FPDF_STRUCTTREE tree) { FPDF_StructTree_Close(tree); } 76 | }; 77 | 78 | struct FPDFTextFindDeleter { 79 | inline void operator()(FPDF_SCHHANDLE handle) { FPDFText_FindClose(handle); } 80 | }; 81 | 82 | struct FPDFTextPageDeleter { 83 | inline void operator()(FPDF_TEXTPAGE text) { FPDFText_ClosePage(text); } 84 | }; 85 | 86 | #endif // PUBLIC_CPP_FPDF_DELETERS_H_ 87 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/PdfPageLinkTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | import android.graphics.RectF 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.google.common.truth.Truth.assertThat 6 | import io.legere.pdfiumandroid.base.BasePDFTest 7 | import org.junit.After 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class PdfPageLinkTest : BasePDFTest() { 14 | private lateinit var pdfDocument: PdfDocument 15 | private lateinit var pdfPage: PdfPage 16 | private lateinit var pdfTextPage: PdfTextPage 17 | private var pdfBytes: ByteArray? = null 18 | 19 | @Before 20 | fun setUp() { 21 | pdfBytes = getPdfBytes("pdf-test.pdf") 22 | 23 | assertThat(pdfBytes).isNotNull() 24 | 25 | pdfDocument = PdfiumCore().newDocument(pdfBytes) 26 | 27 | pdfPage = pdfDocument.openPage(0) 28 | pdfTextPage = pdfPage.openTextPage() 29 | } 30 | 31 | @After 32 | fun tearDown() { 33 | pdfTextPage.close() 34 | pdfPage.close() 35 | pdfDocument.close() 36 | } 37 | 38 | @Test 39 | fun testLink() { 40 | val links = pdfTextPage.loadWebLink() 41 | assertThat(links).isNotNull() 42 | links.close() 43 | } 44 | 45 | @Test 46 | fun testCountWebLinks() { 47 | val links = pdfTextPage.loadWebLink() 48 | assertThat(links).isNotNull() 49 | assertThat(links.countWebLinks()).isEqualTo(1) 50 | links.close() 51 | } 52 | 53 | @Test 54 | fun testGetTextRange() { 55 | val links = pdfTextPage.loadWebLink() 56 | assertThat(links).isNotNull() 57 | assertThat(links.getTextRange(0)).isEqualTo(Pair(351, 31)) 58 | links.close() 59 | } 60 | 61 | @Test 62 | fun testGetUrl() { 63 | val links = pdfTextPage.loadWebLink() 64 | assertThat(links).isNotNull() 65 | val (_, count) = links.getTextRange(0) 66 | assertThat(links.getURL(0, count)).isEqualTo("http://www.education.gov.yk.ca/") 67 | links.close() 68 | } 69 | 70 | @Test 71 | fun testCountRects() { 72 | val links = pdfTextPage.loadWebLink() 73 | assertThat(links).isNotNull() 74 | val count = links.countRects(0) 75 | assertThat(count).isEqualTo(1) 76 | links.close() 77 | } 78 | 79 | @Test 80 | fun testGetRect() { 81 | val links = pdfTextPage.loadWebLink() 82 | assertThat(links).isNotNull() 83 | val count = links.getRect(0, 0) 84 | assertThat(count).isEqualTo(RectF(221.46f, 480.624f, 389.66394f, 469.152f)) 85 | links.close() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/PdfiumCoreKt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.suspend 4 | 5 | import android.os.ParcelFileDescriptor 6 | import androidx.annotation.Keep 7 | import io.legere.pdfiumandroid.PdfiumCore 8 | import io.legere.pdfiumandroid.PdfiumSource 9 | import io.legere.pdfiumandroid.util.Config 10 | import kotlinx.coroutines.CoroutineDispatcher 11 | import kotlinx.coroutines.withContext 12 | 13 | /** 14 | * PdfiumCoreKt is the main entry-point for access to the PDFium API. 15 | * @property dispatcher the [CoroutineDispatcher] to use for suspending calls 16 | * @constructor create a [PdfiumCoreKt] from a [PdfiumCore] 17 | */ 18 | @Keep 19 | class PdfiumCoreKt( 20 | private val dispatcher: CoroutineDispatcher, 21 | config: Config = Config(), 22 | ) { 23 | private val coreInternal = PdfiumCore(config = config) 24 | 25 | /** 26 | * suspend version of [PdfiumCore.newDocument] 27 | */ 28 | suspend fun newDocument(fd: ParcelFileDescriptor): PdfDocumentKt = 29 | withContext(dispatcher) { 30 | PdfDocumentKt(coreInternal.newDocument(fd), dispatcher) 31 | } 32 | 33 | /** 34 | * suspend version of [PdfiumCore.newDocument] 35 | */ 36 | suspend fun newDocument( 37 | fd: ParcelFileDescriptor, 38 | password: String?, 39 | ): PdfDocumentKt = 40 | withContext(dispatcher) { 41 | PdfDocumentKt(coreInternal.newDocument(fd, password), dispatcher) 42 | } 43 | 44 | /** 45 | * suspend version of [PdfiumCore.newDocument] 46 | */ 47 | suspend fun newDocument(data: ByteArray?): PdfDocumentKt = 48 | withContext(dispatcher) { 49 | PdfDocumentKt(coreInternal.newDocument(data), dispatcher) 50 | } 51 | 52 | /** 53 | * suspend version of [PdfiumCore.newDocument] 54 | */ 55 | suspend fun newDocument( 56 | data: ByteArray?, 57 | password: String?, 58 | ): PdfDocumentKt = 59 | withContext(dispatcher) { 60 | PdfDocumentKt(coreInternal.newDocument(data, password), dispatcher) 61 | } 62 | 63 | /** 64 | * suspend version of [PdfiumCore.newDocument] 65 | */ 66 | suspend fun newDocument(data: PdfiumSource): PdfDocumentKt = 67 | withContext(dispatcher) { 68 | PdfDocumentKt(coreInternal.newDocument(data), dispatcher) 69 | } 70 | 71 | /** 72 | * suspend version of [PdfiumCore.newDocument] 73 | */ 74 | suspend fun newDocument( 75 | data: PdfiumSource, 76 | password: String?, 77 | ): PdfDocumentKt = 78 | withContext(dispatcher) { 79 | PdfDocumentKt(coreInternal.newDocument(data, password), dispatcher) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfiumCoreKtF.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.arrow 4 | 5 | import android.os.ParcelFileDescriptor 6 | import arrow.core.Either 7 | import io.legere.pdfiumandroid.PdfiumCore 8 | import io.legere.pdfiumandroid.PdfiumSource 9 | import io.legere.pdfiumandroid.util.Config 10 | import kotlinx.coroutines.CoroutineDispatcher 11 | 12 | /** 13 | * PdfiumCoreKtF is the main entry-point for access to the PDFium API. 14 | * @property dispatcher the [CoroutineDispatcher] to use for suspending calls 15 | * @constructor create a [PdfiumCoreKtF] from a [PdfiumCore] 16 | */ 17 | class PdfiumCoreKtF( 18 | private val dispatcher: CoroutineDispatcher, 19 | config: Config = Config(), 20 | ) { 21 | private val coreInternal = PdfiumCore(config = config) 22 | 23 | /** 24 | * suspend version of [PdfiumCore.newDocument] 25 | */ 26 | suspend fun newDocument(fd: ParcelFileDescriptor): Either = 27 | wrapEither(dispatcher) { 28 | PdfDocumentKtF(coreInternal.newDocument(fd), dispatcher) 29 | } 30 | 31 | /** 32 | * suspend version of [PdfiumCore.newDocument] 33 | */ 34 | suspend fun newDocument( 35 | fd: ParcelFileDescriptor, 36 | password: String?, 37 | ): Either = 38 | wrapEither(dispatcher) { 39 | PdfDocumentKtF(coreInternal.newDocument(fd, password), dispatcher) 40 | } 41 | 42 | /** 43 | * suspend version of [PdfiumCore.newDocument] 44 | */ 45 | suspend fun newDocument(data: ByteArray?): Either = 46 | wrapEither(dispatcher) { 47 | PdfDocumentKtF(coreInternal.newDocument(data), dispatcher) 48 | } 49 | 50 | /** 51 | * suspend version of [PdfiumCore.newDocument] 52 | */ 53 | suspend fun newDocument( 54 | data: ByteArray?, 55 | password: String?, 56 | ): Either = 57 | wrapEither(dispatcher) { 58 | PdfDocumentKtF(coreInternal.newDocument(data, password), dispatcher) 59 | } 60 | 61 | /** 62 | * suspend version of [PdfiumCore.newDocument] 63 | */ 64 | suspend fun newDocument(data: PdfiumSource): Either = 65 | wrapEither(dispatcher) { 66 | PdfDocumentKtF(coreInternal.newDocument(data), dispatcher) 67 | } 68 | 69 | /** 70 | * suspend version of [PdfiumCore.newDocument] 71 | */ 72 | suspend fun newDocument( 73 | data: PdfiumSource, 74 | password: String?, 75 | ): Either = 76 | wrapEither(dispatcher) { 77 | PdfDocumentKtF(coreInternal.newDocument(data, password), dispatcher) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/utils/Errors.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef ANDROID_ERRORS_H 18 | #define ANDROID_ERRORS_H 19 | 20 | #include 21 | #include 22 | 23 | namespace android { 24 | 25 | // use this type to return error codes 26 | #ifdef HAVE_MS_C_RUNTIME 27 | typedef int status_t; 28 | #else 29 | typedef int32_t status_t; 30 | #endif 31 | 32 | /* the MS C runtime lacks a few error codes */ 33 | 34 | /* 35 | * Error codes. 36 | * All error codes are negative values. 37 | */ 38 | 39 | // Win32 #defines NO_ERROR as well. It has the same value, so there's no 40 | // real conflict, though it's a bit awkward. 41 | #ifdef _WIN32 42 | # undef NO_ERROR 43 | #endif 44 | 45 | enum { 46 | OK = 0, // Everything's swell. 47 | NO_ERROR = 0, // No errors. 48 | 49 | UNKNOWN_ERROR = (-2147483647-1), // INT32_MIN value 50 | 51 | NO_MEMORY = -ENOMEM, 52 | INVALID_OPERATION = -ENOSYS, 53 | BAD_VALUE = -EINVAL, 54 | BAD_TYPE = (UNKNOWN_ERROR + 1), 55 | NAME_NOT_FOUND = -ENOENT, 56 | PERMISSION_DENIED = -EPERM, 57 | NO_INIT = -ENODEV, 58 | ALREADY_EXISTS = -EEXIST, 59 | DEAD_OBJECT = -EPIPE, 60 | FAILED_TRANSACTION = (UNKNOWN_ERROR + 2), 61 | JPARKS_BROKE_IT = -EPIPE, 62 | #if !defined(HAVE_MS_C_RUNTIME) 63 | BAD_INDEX = -EOVERFLOW, 64 | NOT_ENOUGH_DATA = -ENODATA, 65 | WOULD_BLOCK = -EWOULDBLOCK, 66 | TIMED_OUT = -ETIMEDOUT, 67 | UNKNOWN_TRANSACTION = -EBADMSG, 68 | #else 69 | BAD_INDEX = -E2BIG, 70 | NOT_ENOUGH_DATA = (UNKNOWN_ERROR + 3), 71 | WOULD_BLOCK = (UNKNOWN_ERROR + 4), 72 | TIMED_OUT = (UNKNOWN_ERROR + 5), 73 | UNKNOWN_TRANSACTION = (UNKNOWN_ERROR + 6), 74 | #endif 75 | FDS_NOT_ALLOWED = (UNKNOWN_ERROR + 7), 76 | }; 77 | 78 | // Restore define; enumeration is in "android" namespace, so the value defined 79 | // there won't work for Win32 code in a different namespace. 80 | #ifdef _WIN32 81 | # define NO_ERROR 0L 82 | #endif 83 | 84 | }; // namespace android 85 | 86 | // --------------------------------------------------------------------------- 87 | 88 | #endif // ANDROID_ERRORS_H 89 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/PdfPageKtCache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.suspend 4 | 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.Dispatchers 7 | 8 | /** 9 | * A cache for pages of a PDF document, designed to be used with coroutines. 10 | * 11 | * This class is thread-safe. 12 | * 13 | * To use this class, you need to provide a `pdfDocument`, a `dispatcher` and a `pageHolderFactory`. 14 | * The `pageHolderFactory` is a lambda that takes a `PdfPageKt` and a `PdfTextPageKt` and returns an instance of `H`. 15 | * 16 | * The generic type `H` is a holder for the page and text page. It can be a simple [PageHolderKt] or a custom class. 17 | * 18 | * Example usage with [PageHolderKt]: 19 | * ``` 20 | * val pageCache = PdfPageKtCache(pdfDocument) { page, textPage -> 21 | * PageHolderKt(page, textPage) 22 | * } 23 | * ``` 24 | * 25 | * Example usage with a custom holder: 26 | * ``` 27 | * data class CustomPageHolder( 28 | * val page: PdfPageKt, 29 | * val textPage: PdfTextPageKt, 30 | * val someOtherData: String 31 | * ) : AutoCloseable { 32 | * override fun close() { 33 | * try { 34 | * textPage.close() 35 | * } finally { 36 | * page.close() 37 | * } 38 | * } 39 | * } 40 | * 41 | * val pageCache = PdfPageKtCache(pdfDocument) { page, textPage -> 42 | * CustomPageHolder(page, textPage, "some other data") 43 | * } 44 | * ``` 45 | * 46 | * @param H The type of the holder for the page and text page. Must be [AutoCloseable]. 47 | * @property pdfDocument The [PdfDocumentKt] to cache pages from. 48 | * @param dispatcher The [CoroutineDispatcher] to use for opening pages. Defaults to [Dispatchers.IO]. 49 | * @property pageHolderFactory A factory for creating a holder for a page and text page. 50 | */ 51 | class PdfPageKtCache( 52 | private val pdfDocument: PdfDocumentKt, 53 | dispatcher: CoroutineDispatcher = Dispatchers.IO, 54 | private val pageHolderFactory: suspend (PdfPageKt, PdfTextPageKt) -> H, 55 | ) : PdfPageSuspendCacheBase(dispatcher) { 56 | override suspend fun openPageAndText(pageIndex: Int): H { 57 | val page = pdfDocument.openPage(pageIndex) 58 | val textPage = page.openTextPage() 59 | return pageHolderFactory(page, textPage) 60 | } 61 | } 62 | 63 | /** 64 | * A holder for a [PdfPageKt] and a [PdfTextPageKt]. 65 | * 66 | * @param TPage The type of the page. 67 | * @param TTextPage The type of the text page. 68 | * @property page The page. 69 | * @property textPage The text page. 70 | */ 71 | data class PageHolderKt( 72 | val page: TPage, 73 | val textPage: TTextPage, 74 | ) : AutoCloseable { 75 | override fun close() { 76 | try { 77 | textPage.close() 78 | } finally { 79 | page.close() 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_javascript.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | #ifndef PUBLIC_FPDF_JAVASCRIPT_H_ 6 | #define PUBLIC_FPDF_JAVASCRIPT_H_ 7 | 8 | // NOLINTNEXTLINE(build/include) 9 | #include "fpdfview.h" 10 | 11 | #ifdef __cplusplus 12 | extern "C" { 13 | #endif // __cplusplus 14 | 15 | // Experimental API. 16 | // Get the number of JavaScript actions in |document|. 17 | // 18 | // document - handle to a document. 19 | // 20 | // Returns the number of JavaScript actions in |document| or -1 on error. 21 | FPDF_EXPORT int FPDF_CALLCONV 22 | FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document); 23 | 24 | // Experimental API. 25 | // Get the JavaScript action at |index| in |document|. 26 | // 27 | // document - handle to a document. 28 | // index - the index of the requested JavaScript action. 29 | // 30 | // Returns the handle to the JavaScript action, or NULL on failure. 31 | // Caller owns the returned handle and must close it with 32 | // FPDFDoc_CloseJavaScriptAction(). 33 | FPDF_EXPORT FPDF_JAVASCRIPT_ACTION FPDF_CALLCONV 34 | FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index); 35 | 36 | // Experimental API. 37 | // Close a loaded FPDF_JAVASCRIPT_ACTION object. 38 | 39 | // javascript - Handle to a JavaScript action. 40 | FPDF_EXPORT void FPDF_CALLCONV 41 | FPDFDoc_CloseJavaScriptAction(FPDF_JAVASCRIPT_ACTION javascript); 42 | 43 | // Experimental API. 44 | // Get the name from the |javascript| handle. |buffer| is only modified if 45 | // |buflen| is longer than the length of the name. On errors, |buffer| is 46 | // unmodified and the returned length is 0. 47 | // 48 | // javascript - handle to an JavaScript action. 49 | // buffer - buffer for holding the name, encoded in UTF-16LE. 50 | // buflen - length of the buffer in bytes. 51 | // 52 | // Returns the length of the JavaScript action name in bytes. 53 | FPDF_EXPORT unsigned long FPDF_CALLCONV 54 | FPDFJavaScriptAction_GetName(FPDF_JAVASCRIPT_ACTION javascript, 55 | FPDF_WCHAR* buffer, 56 | unsigned long buflen); 57 | 58 | // Experimental API. 59 | // Get the script from the |javascript| handle. |buffer| is only modified if 60 | // |buflen| is longer than the length of the script. On errors, |buffer| is 61 | // unmodified and the returned length is 0. 62 | // 63 | // javascript - handle to an JavaScript action. 64 | // buffer - buffer for holding the name, encoded in UTF-16LE. 65 | // buflen - length of the buffer in bytes. 66 | // 67 | // Returns the length of the JavaScript action name in bytes. 68 | FPDF_EXPORT unsigned long FPDF_CALLCONV 69 | FPDFJavaScriptAction_GetScript(FPDF_JAVASCRIPT_ACTION javascript, 70 | FPDF_WCHAR* buffer, 71 | unsigned long buflen); 72 | 73 | #ifdef __cplusplus 74 | } // extern "C" 75 | #endif // __cplusplus 76 | 77 | #endif // PUBLIC_FPDF_JAVASCRIPT_H_ 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PdfiumAndroidKt 2 | 3 | [![Android CI](https://github.com/johngray1965/PdfiumAndroidKt/actions/workflows/android.yml/badge.svg)](https://github.com/johngray1965/PdfiumAndroidKt/actions/workflows/android.yml) 4 | 5 | A Pdfium Android library using the latest stable version Pdfium. Written in Kotlin with coroutines to easily talk to the native code off the main thread. 6 | 7 | Largely rewritten, but based on https://github.com/barteksc/PdfiumAndroid 8 | 9 | This version give you three versions of the API, one that using suspend functions, one that use arrow (and suspend functions) and one plain API with nothing fancy (and that should work from Java). 10 | 11 | This is a object-oriented version. PfdiumCore gives you options to open up the PDF, all the methods on document are from PdfDocumment. Likewise there's an PdfPage and PdfTextPage. You get the Page objects from the PdfDocument, all the things that operation on the pages. 12 | 13 | For using suspend functions, use PdfiumCoreKt, and it'll return PdfDocummentK, PdfDocummentK give PdfPageKt and PdfTextPageKt. All the Kt classes have suspend functions, and will do there work on the dispatcher that's passed into PdfiumCoreKt when its created. 14 | 15 | For using arrow functions, use PdfiumCoreKtF, and it'll return PdfDocummentKF, PdfDocummentKF give PdfPageKtF and PdfTextPageKtF. All the KtF classes have arrow functions, and will do there work on the dispatcher that's passed into PdfiumCoreKtF when its created. 16 | 17 | PdfDocument, PdfPage, PdfTextPage and the Kt and KtF versions all uses Closable. 18 | 19 | To use it, add the following in your app's build.gradle: 20 | ``` 21 | implementation("io.legere:pdfiumandroid:1.0.35") 22 | ``` 23 | 24 | The arrow support is in a separate module. To use it, add the following in your app's build.gradle: 25 | ``` 26 | implementation("io.legere:pdfium-android-kt-arrow:1.0.35") 27 | ``` 28 | For more information on arrow-kt, see the https://arrow-kt.io/ 29 | 30 | ## A few notes on performance. 31 | 32 | Opening documents and pages (as well the text pages) are relatively expensive operations. You probably want to avoid, for instance, opening the page on demand. Open the page and keep it open until you're done with it. 33 | 34 | The bitmap rendering API supports RGB_565 format, but its slow. The underlying pdfium APIs work with ARGB_888. The RGB_565 support has to allocate a buffer for ARGB_888, get the data, covert the data, release the buffer. The ARGB_888 support writes directly to the bitmap without any buffer allocation or conversion. 35 | 36 | Rendering directly to a Surface is fast, and doesn't require the memory overhead of bitmaps. 37 | 38 | ## What this project does 39 | 40 | We provide Android bindings for Pdfium. Pdfium is a library that Google produces. 41 | 42 | We don't do user interfaces. We are open to helping integrate this with your UI on contract basis. 43 | 44 | We are open to Pull Requests (PRs), but only if they keep the to scope of providing Android binding for Pdfium. And we don't accept PRs that customize the Pdfium libraries. -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/PdfDocumentTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.google.common.truth.Truth.assertThat 5 | import io.legere.pdfiumandroid.base.BasePDFTest 6 | import org.junit.After 7 | import org.junit.Before 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(AndroidJUnit4::class) 12 | class PdfDocumentTest : BasePDFTest() { 13 | private lateinit var pdfDocument: PdfDocument 14 | private var pdfBytes: ByteArray? = null 15 | 16 | @Before 17 | fun setUp() { 18 | pdfBytes = getPdfBytes("f01.pdf") 19 | 20 | assertThat(pdfBytes).isNotNull() 21 | 22 | pdfDocument = PdfiumCore().newDocument(pdfBytes) 23 | } 24 | 25 | @After 26 | fun tearDown() { 27 | pdfDocument.close() 28 | } 29 | 30 | @Test 31 | fun getPageCount() { 32 | val pageCount = pdfDocument.getPageCount() 33 | 34 | assertThat(pageCount).isEqualTo(4) 35 | } 36 | 37 | @Test 38 | fun openPage() { 39 | val page = pdfDocument.openPage(0) 40 | 41 | assertThat(page).isNotNull() 42 | } 43 | 44 | @Test 45 | fun openPages() { 46 | val page = pdfDocument.openPages(0, 3) 47 | 48 | assertThat(page.size).isEqualTo(4) 49 | } 50 | 51 | @Test 52 | fun getDocumentMeta() { 53 | val meta = pdfDocument.getDocumentMeta() 54 | 55 | assertThat(meta).isNotNull() 56 | } 57 | 58 | @Test 59 | fun getTableOfContents() { 60 | // I don't think this test document has a table of contents 61 | val toc = pdfDocument.getTableOfContents() 62 | 63 | assertThat(toc).isNotNull() 64 | assertThat(toc.size).isEqualTo(0) 65 | } 66 | 67 | @Test 68 | fun openTextPage() { 69 | val page = pdfDocument.openPage(0) 70 | val textPage = page.openTextPage() 71 | assertThat(textPage).isNotNull() 72 | } 73 | 74 | // @Test 75 | // fun openTextPages() { 76 | // val textPages = pdfDocument.openTextPages(0, 3) 77 | // assertThat(textPages.size).isEqualTo(4) 78 | // } 79 | 80 | @Test 81 | fun saveAsCopy() { 82 | pdfDocument.saveAsCopy( 83 | object : PdfWriteCallback { 84 | override fun WriteBlock(data: ByteArray?): Int { 85 | // assertThat(data?.size).isEqualTo(pdfBytes?.size) 86 | // assertThat(data).isEqualTo(pdfBytes) 87 | return data?.size ?: 0 88 | } 89 | }, 90 | ) 91 | } 92 | 93 | @Test(expected = IllegalStateException::class) 94 | fun closeDocument() { 95 | var shouldBeClosed: PdfDocument? 96 | PdfiumCore().newDocument(pdfBytes).use { pdfDocument -> 97 | assertThat(pdfDocument).isNotNull() 98 | shouldBeClosed = pdfDocument 99 | } 100 | 101 | // Now it should be closed 102 | shouldBeClosed?.openPage(0) // This should throw an exception 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_save.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 | 7 | #ifndef PUBLIC_FPDF_SAVE_H_ 8 | #define PUBLIC_FPDF_SAVE_H_ 9 | 10 | // clang-format off 11 | // NOLINTNEXTLINE(build/include) 12 | #include "fpdfview.h" 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | // Structure for custom file write 19 | typedef struct FPDF_FILEWRITE_ { 20 | // 21 | // Version number of the interface. Currently must be 1. 22 | // 23 | int version; 24 | 25 | // Method: WriteBlock 26 | // Output a block of data in your custom way. 27 | // Interface Version: 28 | // 1 29 | // Implementation Required: 30 | // Yes 31 | // Comments: 32 | // Called by function FPDF_SaveDocument 33 | // Parameters: 34 | // pThis - Pointer to the structure itself 35 | // pData - Pointer to a buffer to output 36 | // size - The size of the buffer. 37 | // Return value: 38 | // Should be non-zero if successful, zero for error. 39 | int (*WriteBlock)(struct FPDF_FILEWRITE_* pThis, 40 | const void* pData, 41 | unsigned long size); 42 | } FPDF_FILEWRITE; 43 | 44 | // Flags for FPDF_SaveAsCopy() 45 | #define FPDF_INCREMENTAL 1 46 | #define FPDF_NO_INCREMENTAL 2 47 | #define FPDF_REMOVE_SECURITY 3 48 | 49 | // Function: FPDF_SaveAsCopy 50 | // Saves the copy of specified document in custom way. 51 | // Parameters: 52 | // document - Handle to document, as returned by 53 | // FPDF_LoadDocument() or FPDF_CreateNewDocument(). 54 | // pFileWrite - A pointer to a custom file write structure. 55 | // flags - Flags above that affect how the PDF gets saved. 56 | // Pass in 0 when there are no flags. 57 | // Return value: 58 | // TRUE for succeed, FALSE for failed. 59 | // 60 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDF_SaveAsCopy(FPDF_DOCUMENT document, 61 | FPDF_FILEWRITE* pFileWrite, 62 | FPDF_DWORD flags); 63 | 64 | // Function: FPDF_SaveWithVersion 65 | // Same as FPDF_SaveAsCopy(), except the file version of the 66 | // saved document can be specified by the caller. 67 | // Parameters: 68 | // document - Handle to document. 69 | // pFileWrite - A pointer to a custom file write structure. 70 | // flags - The creating flags. 71 | // fileVersion - The PDF file version. File version: 14 for 1.4, 72 | // 15 for 1.5, ... 73 | // Return value: 74 | // TRUE if succeed, FALSE if failed. 75 | // 76 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV 77 | FPDF_SaveWithVersion(FPDF_DOCUMENT document, 78 | FPDF_FILEWRITE* pFileWrite, 79 | FPDF_DWORD flags, 80 | int fileVersion); 81 | 82 | #ifdef __cplusplus 83 | } 84 | #endif 85 | 86 | #endif // PUBLIC_FPDF_SAVE_H_ 87 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/suspend/PdfPageLinkKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import android.graphics.RectF 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.google.common.truth.Truth.assertThat 6 | import io.legere.pdfiumandroid.base.BasePDFTest 7 | import junit.framework.TestCase 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.runBlocking 10 | import kotlinx.coroutines.test.TestResult 11 | import kotlinx.coroutines.test.runTest 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Test 15 | import org.junit.runner.RunWith 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class PdfPageLinkKtTest : BasePDFTest() { 19 | private lateinit var pdfDocument: PdfDocumentKt 20 | private lateinit var pdfPage: PdfPageKt 21 | private lateinit var pdfTextPage: PdfTextPageKt 22 | 23 | private var pdfBytes: ByteArray? = null 24 | 25 | @Before 26 | fun setUp() = 27 | runBlocking { 28 | pdfBytes = getPdfBytes("pdf-test.pdf") 29 | 30 | TestCase.assertNotNull(pdfBytes) 31 | 32 | pdfDocument = PdfiumCoreKt(Dispatchers.Unconfined).newDocument(pdfBytes) 33 | pdfPage = pdfDocument.openPage(0) 34 | pdfTextPage = pdfPage.openTextPage() 35 | } 36 | 37 | @After 38 | fun tearDown() { 39 | pdfTextPage.close() 40 | pdfPage.close() 41 | pdfDocument.close() 42 | } 43 | 44 | @Test 45 | fun testLink(): TestResult = 46 | runTest { 47 | val links = pdfTextPage.loadWebLink() 48 | assertThat(links).isNotNull() 49 | links.close() 50 | } 51 | 52 | @Test 53 | fun testCountWebLinks(): TestResult = 54 | runTest { 55 | val links = pdfTextPage.loadWebLink() 56 | assertThat(links).isNotNull() 57 | assertThat(links.countWebLinks()).isEqualTo(1) 58 | links.close() 59 | } 60 | 61 | @Test 62 | fun testGetTextRange(): TestResult = 63 | runTest { 64 | val links = pdfTextPage.loadWebLink() 65 | assertThat(links).isNotNull() 66 | assertThat(links.getTextRange(0)).isEqualTo(Pair(351, 31)) 67 | links.close() 68 | } 69 | 70 | @Test 71 | fun testGetUrl(): TestResult = 72 | runTest { 73 | val links = pdfTextPage.loadWebLink() 74 | assertThat(links).isNotNull() 75 | val (_, count) = links.getTextRange(0) 76 | assertThat(links.getURL(0, count)).isEqualTo("http://www.education.gov.yk.ca/") 77 | links.close() 78 | } 79 | 80 | @Test 81 | fun testCountRects(): TestResult = 82 | runTest { 83 | val links = pdfTextPage.loadWebLink() 84 | assertThat(links).isNotNull() 85 | val count = links.countRects(0) 86 | assertThat(count).isEqualTo(1) 87 | links.close() 88 | } 89 | 90 | @Test 91 | fun testGetRect(): TestResult = 92 | runTest { 93 | val links = pdfTextPage.loadWebLink() 94 | assertThat(links).isNotNull() 95 | val count = links.getRect(0, 0) 96 | assertThat(count).isEqualTo(RectF(221.46f, 480.624f, 389.66394f, 469.152f)) 97 | links.close() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | id("com.android.application") 5 | id("org.jetbrains.kotlin.android") 6 | id("com.google.dagger.hilt.android") 7 | id("com.google.devtools.ksp") 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.detekt) 10 | alias(libs.plugins.kover) 11 | alias(libs.plugins.ktlint) 12 | } 13 | kotlin { 14 | compilerOptions { 15 | jvmTarget.set(JvmTarget.JVM_17) 16 | } 17 | } 18 | 19 | android { 20 | namespace = "io.legere.pdfiumandroidkt" 21 | compileSdk = 36 22 | 23 | defaultConfig { 24 | applicationId = "io.legere.pdfiumandroidkt" 25 | minSdk = 24 26 | versionCode = 1 27 | versionName = "1.0" 28 | 29 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 30 | vectorDrawables.useSupportLibrary = true 31 | } 32 | 33 | buildTypes { 34 | release { 35 | isMinifyEnabled = false 36 | proguardFiles( 37 | getDefaultProguardFile("proguard-android-optimize.txt"), 38 | "proguard-rules.pro", 39 | ) 40 | } 41 | // maybeCreate("qa") 42 | // getByName("qa") { 43 | // matchingFallbacks += listOf("release") 44 | // isMinifyEnabled = true 45 | // signingConfig = signingConfigs.getByName("debug") 46 | // matchingFallbacks += listOf("release") 47 | // } 48 | } 49 | compileOptions { 50 | sourceCompatibility(JavaVersion.VERSION_17) 51 | targetCompatibility(JavaVersion.VERSION_17) 52 | } 53 | 54 | buildFeatures { 55 | compose = true 56 | } 57 | 58 | packaging { 59 | resources { 60 | excludes += 61 | setOf( 62 | "/META-INF/{AL2.0,LGPL2.1}", 63 | ) 64 | } 65 | } 66 | 67 | lint { 68 | abortOnError = true 69 | disable += "NotificationPermission" 70 | } 71 | } 72 | 73 | dependencies { 74 | 75 | implementation(project(":pdfiumandroid")) 76 | implementation(libs.androidx.core.ktx) 77 | implementation(libs.androidx.appcompat) 78 | implementation(libs.material) 79 | implementation(libs.androidx.lifecycle.runtime.ktx) 80 | implementation(libs.androidx.activity.compose) 81 | implementation(platform(libs.compose.bom)) 82 | implementation(libs.compose.ui) 83 | implementation(libs.compose.ui.graphics) 84 | implementation(libs.compose.ui.tooling.preview) 85 | implementation(libs.compose.material3) 86 | 87 | implementation(libs.coil.compose) 88 | implementation(libs.compose.glide) 89 | 90 | implementation(libs.timber) 91 | 92 | implementation(libs.hilt.android) 93 | ksp(libs.hilt.compiler) 94 | 95 | testImplementation(libs.junit) 96 | androidTestImplementation(libs.androidx.junit) 97 | androidTestImplementation(libs.androidx.espresso.core) 98 | androidTestImplementation(platform(libs.compose.bom)) 99 | androidTestImplementation(libs.compose.ui.test.junit4) 100 | debugImplementation(libs.compose.ui.tooling) 101 | debugImplementation(libs.compose.ui.test.manifest) 102 | } 103 | 104 | // allprojects { 105 | // tasks.withType(KotlinCompile).configureEach { 106 | // kotlinOptions { 107 | // jvmTarget = "1.8" 108 | // } 109 | // } 110 | // } 111 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/PdfPageLink.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid 2 | 3 | import android.graphics.RectF 4 | import java.io.Closeable 5 | import java.nio.charset.StandardCharsets 6 | 7 | @Suppress("TooManyFunctions") 8 | class PdfPageLink( 9 | private val pageLinkPtr: Long, 10 | ) : Closeable { 11 | fun countWebLinks(): Int { 12 | synchronized(PdfiumCore.lock) { 13 | return nativeCountWebLinks(pageLinkPtr) 14 | } 15 | } 16 | 17 | @Suppress("TooGenericExceptionCaught", "ReturnCount") 18 | fun getURL( 19 | index: Int, 20 | length: Int, 21 | ): String? { 22 | synchronized(PdfiumCore.lock) { 23 | try { 24 | val bytes = ByteArray(length * 2) 25 | val r = 26 | nativeGetURL( 27 | pageLinkPtr, 28 | index, 29 | length, 30 | bytes, 31 | ) 32 | 33 | if (r <= 0) { 34 | return "" 35 | } 36 | return String(bytes, StandardCharsets.UTF_16LE) 37 | } catch (e: NullPointerException) { 38 | Logger.e(TAG, e, "mContext may be null") 39 | } catch (e: Exception) { 40 | Logger.e(TAG, e, "Exception throw from native") 41 | } 42 | return null 43 | } 44 | } 45 | 46 | fun countRects(index: Int): Int { 47 | synchronized(PdfiumCore.lock) { 48 | return nativeCountRects(pageLinkPtr, index) 49 | } 50 | } 51 | 52 | @Suppress("MagicNumber") 53 | fun getRect( 54 | linkIndex: Int, 55 | rectIndex: Int, 56 | ): RectF { 57 | synchronized(PdfiumCore.lock) { 58 | return nativeGetRect(pageLinkPtr, linkIndex, rectIndex).let { 59 | RectF(it[0], it[1], it[2], it[3]) 60 | } 61 | } 62 | } 63 | 64 | fun getTextRange(index: Int): Pair { 65 | synchronized(PdfiumCore.lock) { 66 | return nativeGetTextRange(pageLinkPtr, index).let { 67 | Pair(it[0], it[1]) 68 | } 69 | } 70 | } 71 | 72 | override fun close() { 73 | nativeClosePageLink(pageLinkPtr) 74 | } 75 | 76 | companion object { 77 | private val TAG = PdfPageLink::class.java.name 78 | 79 | @JvmStatic 80 | private external fun nativeClosePageLink(pageLinkPtr: Long) 81 | 82 | @JvmStatic 83 | private external fun nativeCountWebLinks(pageLinkPtr: Long): Int 84 | 85 | @JvmStatic 86 | private external fun nativeGetURL( 87 | pageLinkPtr: Long, 88 | index: Int, 89 | count: Int, 90 | result: ByteArray, 91 | ): Int 92 | 93 | @JvmStatic 94 | private external fun nativeCountRects( 95 | pageLinkPtr: Long, 96 | index: Int, 97 | ): Int 98 | 99 | @JvmStatic 100 | private external fun nativeGetRect( 101 | pageLinkPtr: Long, 102 | linkIndex: Int, 103 | rectIndex: Int, 104 | ): FloatArray 105 | 106 | @JvmStatic 107 | // needs to return a start and an end 108 | private external fun nativeGetTextRange(pageLinkPtr: Long, index: Int): IntArray 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/suspend/PdfDocumentKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.google.common.truth.Truth 5 | import io.legere.pdfiumandroid.PdfWriteCallback 6 | import io.legere.pdfiumandroid.base.BasePDFTest 7 | import junit.framework.TestCase 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.runBlocking 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.After 12 | import org.junit.Before 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | 16 | @RunWith(AndroidJUnit4::class) 17 | class PdfDocumentKtTest : BasePDFTest() { 18 | private lateinit var pdfDocument: PdfDocumentKt 19 | private var pdfBytes: ByteArray? = null 20 | 21 | @Before 22 | fun setUp() = 23 | runBlocking { 24 | pdfBytes = getPdfBytes("f01.pdf") 25 | 26 | TestCase.assertNotNull(pdfBytes) 27 | 28 | pdfDocument = PdfiumCoreKt(Dispatchers.Unconfined).newDocument(pdfBytes) 29 | } 30 | 31 | @After 32 | fun tearDown() = 33 | runTest { 34 | pdfDocument.close() 35 | } 36 | 37 | @Test 38 | fun getPageCount() = 39 | runTest { 40 | val pageCount = pdfDocument.getPageCount() 41 | 42 | assert(pageCount == 4) { "Page count should be 4" } 43 | } 44 | 45 | @Test 46 | fun openPage() = 47 | runTest { 48 | val page = pdfDocument.openPage(0) 49 | 50 | TestCase.assertNotNull(page) 51 | } 52 | 53 | @Test 54 | fun openPages() = 55 | runTest { 56 | val page = pdfDocument.openPages(0, 3) 57 | 58 | assert(page.size == 4) { "Page count should be 4" } 59 | } 60 | 61 | @Test 62 | fun getDocumentMeta() = 63 | runTest { 64 | val meta = pdfDocument.getDocumentMeta() 65 | 66 | TestCase.assertNotNull(meta) 67 | } 68 | 69 | @Test 70 | fun getTableOfContents() = 71 | runTest { 72 | // I don't think this test document has a table of contents 73 | val toc = pdfDocument.getTableOfContents() 74 | 75 | TestCase.assertNotNull(toc) 76 | Truth.assertThat(toc.size).isEqualTo(0) 77 | } 78 | 79 | @Test 80 | fun openTextPage() = 81 | runTest { 82 | val page = pdfDocument.openPage(0) 83 | val textPage = page.openTextPage() 84 | TestCase.assertNotNull(textPage) 85 | } 86 | 87 | @Test 88 | fun openTextPages() = 89 | runTest { 90 | val textPages = pdfDocument.openTextPages(0, 3) 91 | Truth.assertThat(textPages.size).isEqualTo(4) 92 | } 93 | 94 | @Test 95 | fun saveAsCopy() = 96 | runTest { 97 | pdfDocument.saveAsCopy( 98 | object : PdfWriteCallback { 99 | override fun WriteBlock(data: ByteArray?): Int { 100 | // Truth.assertThat(data?.size).isEqualTo(pdfBytes?.size) 101 | // Truth.assertThat(data).isEqualTo(pdfBytes) 102 | return data?.size ?: 0 103 | } 104 | }, 105 | ) 106 | } 107 | 108 | @Test(expected = IllegalStateException::class) 109 | fun close() = 110 | runTest { 111 | var documentAfterClose: PdfDocumentKt? 112 | PdfiumCoreKt(Dispatchers.Unconfined).newDocument(pdfBytes).use { 113 | documentAfterClose = it 114 | } 115 | documentAfterClose?.openPage(0) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/io/legere/pdfiumandroidkt/ui/PdfViewer.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionNaming", "ktlint:standard:function-naming") 2 | 3 | package io.legere.pdfiumandroidkt.ui 4 | 5 | import android.graphics.Matrix 6 | import android.graphics.Rect 7 | import android.graphics.RectF 8 | import android.view.Surface 9 | import androidx.compose.foundation.AndroidEmbeddedExternalSurface 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableIntStateOf 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.toArgb 21 | import io.legere.pdfiumandroid.suspend.PdfPageKtCache 22 | import io.legere.pdfiumandroidkt.PdfHolder 23 | import kotlinx.coroutines.Dispatchers 24 | import kotlinx.coroutines.withContext 25 | import kotlin.math.min 26 | 27 | @Composable 28 | fun PdfViewer( 29 | pageCache: PdfPageKtCache, 30 | pageNum: Int, 31 | ) { 32 | var surface: Surface? by remember { mutableStateOf(null) } 33 | var viewWidth by remember { mutableIntStateOf(0) } 34 | var viewHeight by remember { mutableIntStateOf(0) } 35 | 36 | AndroidEmbeddedExternalSurface( 37 | modifier = Modifier.fillMaxSize(), 38 | ) { 39 | onSurface { s, w, h -> 40 | 41 | s.lockCanvas(Rect(0, 0, w, h)).apply { 42 | drawColor(Color.White.toArgb()) 43 | s.unlockCanvasAndPost(this) 44 | } 45 | surface = s 46 | viewWidth = w 47 | viewHeight = h 48 | } 49 | } 50 | 51 | LaunchedEffect(surface, pageNum, viewWidth, viewHeight) { 52 | val s = surface 53 | if (s != null && viewWidth > 0 && viewHeight > 0) { 54 | withContext(Dispatchers.IO) { 55 | drawPdf( 56 | s, 57 | viewWidth, 58 | viewHeight, 59 | pageCache, 60 | pageNum, 61 | ) 62 | } 63 | } 64 | } 65 | } 66 | 67 | @Suppress("LongParameterList") 68 | suspend fun drawPdf( 69 | surface: Surface, 70 | surfaceWidth: Int, 71 | surfaceHeight: Int, 72 | pageCache: PdfPageKtCache, 73 | currentPage: Int, 74 | ) { 75 | if (!surface.isValid || surfaceWidth == 0 || surfaceHeight == 0) { 76 | // Skip rendering if the Surface is not valid or has zero dimensions 77 | return 78 | } 79 | 80 | val pageHolder = pageCache.get(currentPage) 81 | val pageWidth = pageHolder.pageWidth 82 | val pageHeight = pageHolder.pageHeight 83 | // Calculate scaling factor to fit page within Surface bounds 84 | val scaleFactor = 85 | min( 86 | surfaceWidth.toFloat() / pageWidth, 87 | surfaceHeight.toFloat() / pageHeight, 88 | ) 89 | 90 | val height = (pageHeight * scaleFactor).toInt() 91 | val width = (pageWidth * scaleFactor).toInt() 92 | 93 | val startX = (surfaceWidth - width) / 2 94 | val startY = (surfaceHeight - height) / 2 95 | 96 | val matrix = Matrix() 97 | matrix.postScale(scaleFactor, scaleFactor) 98 | matrix.postTranslate(startX.toFloat(), startY.toFloat()) 99 | val clipRect = 100 | RectF(0f, 0f, surfaceWidth.toFloat(), surfaceHeight.toFloat()) 101 | pageHolder.page.renderPage( 102 | surface = surface, 103 | matrix, 104 | clipRect, 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfPageKtFCache.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.arrow 4 | 5 | import arrow.core.Either 6 | import io.legere.pdfiumandroid.suspend.PdfPageSuspendCacheBase 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | 10 | /** 11 | * A cache for pages of a PDF document, designed to be used with Arrow and coroutines. 12 | * 13 | * This class is thread-safe. 14 | * 15 | * To use this class, you need to provide a `pdfDocument` and a `pageHolderFactory`. 16 | * The `pageHolderFactory` is a lambda that takes a `PdfPageKtF` and a `PdfTextPageKtF` and returns an instance of `H`. 17 | * 18 | * The generic type `H` is a holder for the page and text page. It can be a simple [PageHolderKtF] or a custom class. 19 | * 20 | * Example usage with [PageHolderKtF]: 21 | * ``` 22 | * val pageCache = PdfPageKtFCache(pdfDocument) { page, textPage -> 23 | * PageHolderKtF(page, textPage) 24 | * } 25 | * ``` 26 | * 27 | * Example usage with a custom holder: 28 | * ``` 29 | * data class CustomPageHolder( 30 | * val page: PdfPageKtF, 31 | * val textPage: PdfTextPageKtF, 32 | * val someOtherData: String 33 | * ) : AutoCloseable { 34 | * override fun close() { 35 | * try { 36 | * textPage.close() 37 | * } finally { 38 | * page.close() 39 | * } 40 | * } 41 | * } 42 | * 43 | * val pageCache = PdfPageKtFCache(pdfDocument) { page, textPage -> 44 | * CustomPageHolder(page, textPage, "some other data") 45 | * } 46 | * ``` 47 | * 48 | * @param H The type of the holder for the page and text page. Must be [AutoCloseable]. 49 | * @property pdfDocument The [PdfDocumentKtF] to cache pages from. 50 | * @param dispatcher The [CoroutineDispatcher] to use for opening pages. Defaults to [Dispatchers.IO]. 51 | * @property pageHolderFactory A factory for creating a holder for a page and text page. 52 | */ 53 | class PdfPageKtFCache( 54 | private val pdfDocument: PdfDocumentKtF, 55 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO, 56 | private val pageHolderFactory: suspend (PdfPageKtF, PdfTextPageKtF) -> H, 57 | ) : AutoCloseable { 58 | private val suspendCache = 59 | object : PdfPageSuspendCacheBase(dispatcher) { 60 | @Suppress("MaxLineLength") 61 | override suspend fun openPageAndText(pageIndex: Int): H { 62 | val page = pdfDocument.document.openPage(pageIndex) 63 | val textPage = page.openTextPage() 64 | return pageHolderFactory( 65 | PdfPageKtF(page, dispatcher), 66 | PdfTextPageKtF(textPage, dispatcher), 67 | ) 68 | } 69 | } 70 | 71 | /** 72 | * Get a page from the cache. 73 | * 74 | * @param pageIndex The index of the page to get. 75 | * @return An [Either] with the page holder or an error. 76 | */ 77 | suspend fun getF(pageIndex: Int): Either = 78 | Either.catch { 79 | suspendCache.get(pageIndex) 80 | } 81 | 82 | override fun close() { 83 | suspendCache.close() 84 | } 85 | } 86 | 87 | /** 88 | * A holder for a [PdfPageKtF] and a [PdfTextPageKtF]. 89 | * 90 | * @param TPage The type of the page. 91 | * @param TTextPage The type of the text page. 92 | * @property page The page. 93 | * @property textPage The text page. 94 | */ 95 | data class PageHolderKtF( 96 | val page: TPage, 97 | val textPage: TTextPage, 98 | ) : AutoCloseable { 99 | override fun close() { 100 | try { 101 | textPage.close() 102 | } finally { 103 | page.close() 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pdfiumandroid/src/test/java/io/legere/pdfiumandroid/util/PdfiumNativeSourceBridgeTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.util 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.legere.pdfiumandroid.PdfiumSource 5 | import junit.framework.TestCase.fail 6 | import org.junit.Test 7 | 8 | class PdfiumNativeSourceBridgeTest { 9 | @Test 10 | @Suppress("SwallowedException") 11 | fun paramsDispatchedCorrectly() { 12 | var lastPosition = -1L 13 | var lastBufferSize = -1 14 | var lastSize = -1 15 | val bridge = 16 | PdfiumNativeSourceBridge( 17 | source = 18 | mockCustomSource { position, buffer, size -> 19 | lastPosition = position 20 | lastBufferSize = buffer.size 21 | lastSize = size 22 | size 23 | }, 24 | ) 25 | 26 | try { 27 | assertThat(bridge.read(1, 2)).isEqualTo(2) 28 | assertThat(lastPosition).isEqualTo(1) 29 | assertThat(lastBufferSize).isEqualTo(2) 30 | assertThat(lastSize).isEqualTo(2) 31 | } catch (t: Exception) { 32 | fail("Should not throw exception") 33 | } 34 | } 35 | 36 | @Test 37 | @Suppress("SwallowedException") 38 | fun sizeExceedsIntRange() { 39 | val bridge = PdfiumNativeSourceBridge(mockCustomSource()) 40 | 41 | try { 42 | assertThat(bridge.read(0, Long.MAX_VALUE)).isEqualTo(0) 43 | } catch (t: Exception) { 44 | fail("Should not throw exception") 45 | } 46 | } 47 | 48 | @Test 49 | @Suppress("SwallowedException") 50 | fun sourceThrowsException() { 51 | val bridge = PdfiumNativeSourceBridge(mockCustomSource { _, _, _ -> error("Exception!") }) 52 | 53 | try { 54 | assertThat(bridge.read(0, 1)).isEqualTo(0) 55 | } catch (t: Exception) { 56 | fail("Should not throw exception") 57 | } 58 | } 59 | 60 | @Test 61 | @Suppress("SwallowedException") 62 | fun sourceReturnsNegativeValue() { 63 | val bridge = PdfiumNativeSourceBridge(mockCustomSource { _, _, _ -> -1 }) 64 | 65 | try { 66 | assertThat(bridge.read(0, 1)).isEqualTo(0) 67 | } catch (t: Exception) { 68 | fail("Should not throw exception") 69 | } 70 | } 71 | 72 | @Test 73 | @Suppress("SwallowedException") 74 | fun bufferRescales() { 75 | var lastBufferSize = -1 76 | val bridge = 77 | PdfiumNativeSourceBridge( 78 | source = 79 | mockCustomSource { _, buffer, _ -> 80 | lastBufferSize = buffer.size 81 | buffer.size 82 | }, 83 | ) 84 | 85 | try { 86 | assertThat(bridge.read(0, 1)).isEqualTo(1) 87 | assertThat(lastBufferSize).isEqualTo(1) 88 | 89 | assertThat(bridge.read(0, 2)).isEqualTo(2) 90 | assertThat(lastBufferSize).isEqualTo(2) 91 | } catch (t: Exception) { 92 | fail("Should not throw exception") 93 | } 94 | } 95 | 96 | // todo: replace with MockK or Mockito when available 97 | private fun mockCustomSource( 98 | length: Long = 0L, 99 | read: (Long, ByteArray, Int) -> Int = { _, _, size -> size }, 100 | ): PdfiumSource = 101 | object : PdfiumSource { 102 | override val length: Long 103 | get() = length 104 | 105 | override fun read( 106 | position: Long, 107 | buffer: ByteArray, 108 | size: Int, 109 | ): Int = read(position, buffer, size) 110 | 111 | override fun close() { 112 | // nothing to close 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/java/io/legere/pdfiumandroid/arrow/PdfPageLinkKtFTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | import android.graphics.RectF 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import arrow.core.raise.either 6 | import com.google.common.truth.Truth 7 | import io.legere.pdfiumandroid.arrow.base.BasePDFTest 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.runBlocking 10 | import kotlinx.coroutines.test.TestResult 11 | import kotlinx.coroutines.test.runTest 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Test 15 | import org.junit.runner.RunWith 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class PdfPageLinkKtFTest : BasePDFTest() { 19 | private lateinit var pdfDocument: PdfDocumentKtF 20 | private lateinit var pdfPage: PdfPageKtF 21 | private lateinit var pdfTextPage: PdfTextPageKtF 22 | private var pdfBytes: ByteArray? = null 23 | 24 | @Before 25 | fun setUp() = 26 | runBlocking { 27 | pdfBytes = getPdfBytes("pdf-test.pdf") 28 | 29 | Truth.assertThat(pdfBytes).isNotNull() 30 | 31 | pdfDocument = PdfiumCoreKtF(Dispatchers.Unconfined).newDocument(pdfBytes).getOrNull()!! 32 | pdfPage = pdfDocument.openPage(0).getOrNull()!! 33 | pdfTextPage = pdfPage.openTextPage().getOrNull()!! 34 | } 35 | 36 | @After 37 | fun tearDown() { 38 | pdfTextPage.close() 39 | pdfPage.close() 40 | pdfDocument.close() 41 | } 42 | 43 | @Test 44 | fun testLink(): TestResult = 45 | runTest { 46 | either { 47 | pdfTextPage.loadWebLink().bind().use { links -> 48 | Truth.assertThat(links).isNotNull() 49 | } 50 | } 51 | } 52 | 53 | @Test 54 | fun testCountWebLinks(): TestResult = 55 | runTest { 56 | either { 57 | pdfTextPage.loadWebLink().bind().use { links -> 58 | Truth.assertThat(links).isNotNull() 59 | Truth.assertThat(links.countWebLinks().bind()).isEqualTo(1) 60 | } 61 | } 62 | } 63 | 64 | @Test 65 | fun testGetTextRange(): TestResult = 66 | runTest { 67 | either { 68 | pdfTextPage.loadWebLink().bind().use { links -> 69 | Truth.assertThat(links).isNotNull() 70 | Truth.assertThat(links.getTextRange(0).bind()).isEqualTo(Pair(351, 31)) 71 | } 72 | } 73 | } 74 | 75 | @Test 76 | fun testGetUrl(): TestResult = 77 | runTest { 78 | either { 79 | pdfTextPage.loadWebLink().bind().use { links -> 80 | Truth.assertThat(links).isNotNull() 81 | val (_, count) = links.getTextRange(0).bind() 82 | Truth 83 | .assertThat(links.getURL(0, count).bind()) 84 | .isEqualTo("http://www.education.gov.yk.ca/") 85 | } 86 | } 87 | } 88 | 89 | @Test 90 | fun testCountRects(): TestResult = 91 | runTest { 92 | either { 93 | pdfTextPage.loadWebLink().bind().use { links -> 94 | Truth.assertThat(links).isNotNull() 95 | val count = links.countRects(0).bind() 96 | Truth.assertThat(count).isEqualTo(1) 97 | } 98 | } 99 | } 100 | 101 | @Test 102 | fun testGetRect(): TestResult = 103 | runTest { 104 | either { 105 | pdfTextPage.loadWebLink().bind().use { links -> 106 | Truth.assertThat(links).isNotNull() 107 | val count = links.getRect(0, 0).bind() 108 | Truth 109 | .assertThat(count) 110 | .isEqualTo(RectF(221.46f, 480.624f, 389.66394f, 469.152f)) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/androidTest/java/io/legere/pdfiumandroid/arrow/PdfDocumentKtFTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.arrow 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import arrow.core.raise.either 5 | import com.google.common.truth.Truth.assertThat 6 | import io.legere.pdfiumandroid.PdfWriteCallback 7 | import io.legere.pdfiumandroid.arrow.base.BasePDFTest 8 | import junit.framework.TestCase 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.runBlocking 11 | import kotlinx.coroutines.test.runTest 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Test 15 | import org.junit.runner.RunWith 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class PdfDocumentKtFTest : BasePDFTest() { 19 | private lateinit var pdfDocument: PdfDocumentKtF 20 | private var pdfBytes: ByteArray? = null 21 | 22 | @Before 23 | fun setUp() = 24 | runBlocking { 25 | pdfBytes = getPdfBytes("f01.pdf") 26 | 27 | assertThat(pdfBytes).isNotNull() 28 | 29 | pdfDocument = PdfiumCoreKtF(Dispatchers.Unconfined).newDocument(pdfBytes).getOrNull()!! 30 | } 31 | 32 | @After 33 | fun tearDown() = 34 | runTest { 35 | pdfDocument.close() 36 | } 37 | 38 | @Test 39 | fun getPageCount() = 40 | runTest { 41 | either { 42 | val pageCount = pdfDocument.getPageCount().bind() 43 | 44 | assertThat(pageCount).isEqualTo(4) 45 | } 46 | } 47 | 48 | @Test 49 | fun openPage() = 50 | runTest { 51 | either { 52 | val page = pdfDocument.openPage(0).bind() 53 | 54 | assertThat(page).isNotNull() 55 | } 56 | } 57 | 58 | @Test 59 | fun openPages() = 60 | runTest { 61 | either { 62 | val page = pdfDocument.openPages(0, 3).bind() 63 | 64 | assertThat(page.size).isEqualTo(4) 65 | } 66 | } 67 | 68 | @Test 69 | fun getDocumentMeta() = 70 | runTest { 71 | either { 72 | val meta = pdfDocument.getDocumentMeta().bind() 73 | 74 | assertThat(meta).isNotNull() 75 | } 76 | } 77 | 78 | @Test 79 | fun getTableOfContents() = 80 | runTest { 81 | either { 82 | // I don't think this test document has a table of contents 83 | val toc = pdfDocument.getTableOfContents().bind() 84 | 85 | TestCase.assertNotNull(toc) 86 | assertThat(toc.size).isEqualTo(0) 87 | } 88 | } 89 | 90 | @Test 91 | fun openTextPage() = 92 | runTest { 93 | either { 94 | val page = pdfDocument.openPage(0).bind() 95 | val textPage = page.openTextPage().bind() 96 | assertThat(textPage).isNotNull() 97 | } 98 | } 99 | 100 | @Test 101 | fun openTextPages() = 102 | runTest { 103 | either { 104 | val textPages = pdfDocument.openTextPages(0, 3).bind() 105 | assertThat(textPages.size).isEqualTo(4) 106 | } 107 | } 108 | 109 | @Test 110 | fun saveAsCopy() = 111 | runTest { 112 | pdfDocument.saveAsCopy( 113 | object : PdfWriteCallback { 114 | override fun WriteBlock(data: ByteArray?): Int { 115 | // Truth.assertThat(data?.size).isEqualTo(pdfBytes?.size) 116 | // Truth.assertThat(data).isEqualTo(pdfBytes) 117 | return data?.size ?: 0 118 | } 119 | }, 120 | ) 121 | } 122 | 123 | fun close() = 124 | runTest { 125 | either { 126 | var documentAfterClose: PdfDocumentKtF? 127 | PdfiumCoreKtF(Dispatchers.Unconfined).newDocument(pdfBytes).bind().use { 128 | documentAfterClose = it 129 | } 130 | documentAfterClose?.openPage(0) 131 | }.mapLeft { 132 | assertThat(it).isInstanceOf(PdfiumKtFErrors.AlreadyClosed::class.java) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio,android,c++,kotlin 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,android,c++,kotlin 3 | ### Android ### 4 | # Gradle files 5 | .gradle/ 6 | build/ 7 | # Local configuration file (sdk path, etc) 8 | local.properties 9 | # Log/OS Files 10 | *.log 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | # IntelliJ 18 | *.iml 19 | .idea/ 20 | misc.xml 21 | deploymentTargetDropDown.xml 22 | render.experimental.xml 23 | # Keystore files 24 | *.jks 25 | *.keystore 26 | # Google Services (e.g. APIs or Firebase) 27 | google-services.json 28 | # Android Profiling 29 | *.hprof 30 | ### Android Patch ### 31 | gen-external-apklibs 32 | # Replacement of .externalNativeBuild directories introduced 33 | # with Android Studio 3.5. 34 | 35 | ### C++ ### 36 | # Prerequisites 37 | *.d 38 | 39 | # Compiled Object files 40 | *.slo 41 | *.lo 42 | *.o 43 | *.obj 44 | 45 | # Precompiled Headers 46 | *.gch 47 | *.pch 48 | 49 | # Compiled Dynamic libraries 50 | *.dylib 51 | *.dll 52 | 53 | # Fortran module files 54 | *.mod 55 | *.smod 56 | 57 | # Compiled Static libraries 58 | *.lai 59 | *.la 60 | *.a 61 | *.lib 62 | 63 | # Executables 64 | *.exe 65 | *.out 66 | *.app 67 | 68 | ### Kotlin ### 69 | # Compiled class file 70 | *.class 71 | 72 | # Log file 73 | 74 | # BlueJ files 75 | *.ctxt 76 | 77 | # Mobile Tools for Java (J2ME) 78 | .mtj.tmp/ 79 | 80 | # Package Files # 81 | *.jar 82 | *.war 83 | *.nar 84 | *.ear 85 | *.zip 86 | *.tar.gz 87 | *.rar 88 | 89 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 90 | hs_err_pid* 91 | replay_pid* 92 | 93 | ### AndroidStudio ### 94 | # Covers files to be ignored for android development using Android Studio. 95 | 96 | # Built application files 97 | *.ap_ 98 | *.aab 99 | 100 | # Files for the ART/Dalvik VM 101 | *.dex 102 | 103 | # Java class files 104 | 105 | # Generated files 106 | bin/ 107 | gen/ 108 | out/ 109 | 110 | # Gradle files 111 | .gradle 112 | 113 | # Signing files 114 | .signing/ 115 | 116 | # Local configuration file (sdk path, etc) 117 | 118 | # Proguard folder generated by Eclipse 119 | proguard/ 120 | 121 | # Log Files 122 | 123 | # Android Studio 124 | /*/build/ 125 | /*/local.properties 126 | /*/out 127 | /*/*/build 128 | /*/*/production 129 | .navigation/ 130 | *.ipr 131 | *~ 132 | *.swp 133 | 134 | # Keystore files 135 | 136 | # Google Services (e.g. APIs or Firebase) 137 | # google-services.json 138 | 139 | # Android Patch 140 | 141 | # External native build folder generated in Android Studio 2.2 and later 142 | .externalNativeBuild 143 | 144 | # NDK 145 | obj/ 146 | 147 | # IntelliJ IDEA 148 | *.iws 149 | /out/ 150 | 151 | # User-specific configurations 152 | .idea/caches/ 153 | .idea/libraries/ 154 | .idea/shelf/ 155 | .idea/workspace.xml 156 | .idea/tasks.xml 157 | .idea/.name 158 | .idea/compiler.xml 159 | .idea/copyright/profiles_settings.xml 160 | .idea/encodings.xml 161 | .idea/misc.xml 162 | .idea/modules.xml 163 | .idea/scopes/scope_settings.xml 164 | .idea/dictionaries 165 | .idea/vcs.xml 166 | .idea/jsLibraryMappings.xml 167 | .idea/datasources.xml 168 | .idea/dataSources.ids 169 | .idea/sqlDataSources.xml 170 | .idea/dynamic.xml 171 | .idea/uiDesigner.xml 172 | .idea/assetWizardSettings.xml 173 | .idea/gradle.xml 174 | .idea/jarRepositories.xml 175 | .idea/navEditor.xml 176 | 177 | # Legacy Eclipse project files 178 | .classpath 179 | .project 180 | .cproject 181 | .settings/ 182 | 183 | # Mobile Tools for Java (J2ME) 184 | 185 | # Package Files # 186 | 187 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 188 | 189 | ## Plugin-specific files: 190 | 191 | # mpeltonen/sbt-idea plugin 192 | .idea_modules/ 193 | 194 | # JIRA plugin 195 | atlassian-ide-plugin.xml 196 | 197 | # Mongo Explorer plugin 198 | .idea/mongoSettings.xml 199 | 200 | # Crashlytics plugin (for Android Studio and IntelliJ) 201 | com_crashlytics_export_strings.xml 202 | crashlytics.properties 203 | crashlytics-build.properties 204 | fabric.properties 205 | 206 | ### AndroidStudio Patch ### 207 | 208 | !/gradle/wrapper/gradle-wrapper.jar 209 | 210 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio,android,c++,kotlin 211 | 212 | /*/.cxx 213 | /x/** 214 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_ext.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 | 7 | #ifndef PUBLIC_FPDF_EXT_H_ 8 | #define PUBLIC_FPDF_EXT_H_ 9 | 10 | #include 11 | 12 | // NOLINTNEXTLINE(build/include) 13 | #include "fpdfview.h" 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif // __cplusplus 18 | 19 | // Unsupported XFA form. 20 | #define FPDF_UNSP_DOC_XFAFORM 1 21 | // Unsupported portable collection. 22 | #define FPDF_UNSP_DOC_PORTABLECOLLECTION 2 23 | // Unsupported attachment. 24 | #define FPDF_UNSP_DOC_ATTACHMENT 3 25 | // Unsupported security. 26 | #define FPDF_UNSP_DOC_SECURITY 4 27 | // Unsupported shared review. 28 | #define FPDF_UNSP_DOC_SHAREDREVIEW 5 29 | // Unsupported shared form, acrobat. 30 | #define FPDF_UNSP_DOC_SHAREDFORM_ACROBAT 6 31 | // Unsupported shared form, filesystem. 32 | #define FPDF_UNSP_DOC_SHAREDFORM_FILESYSTEM 7 33 | // Unsupported shared form, email. 34 | #define FPDF_UNSP_DOC_SHAREDFORM_EMAIL 8 35 | // Unsupported 3D annotation. 36 | #define FPDF_UNSP_ANNOT_3DANNOT 11 37 | // Unsupported movie annotation. 38 | #define FPDF_UNSP_ANNOT_MOVIE 12 39 | // Unsupported sound annotation. 40 | #define FPDF_UNSP_ANNOT_SOUND 13 41 | // Unsupported screen media annotation. 42 | #define FPDF_UNSP_ANNOT_SCREEN_MEDIA 14 43 | // Unsupported screen rich media annotation. 44 | #define FPDF_UNSP_ANNOT_SCREEN_RICHMEDIA 15 45 | // Unsupported attachment annotation. 46 | #define FPDF_UNSP_ANNOT_ATTACHMENT 16 47 | // Unsupported signature annotation. 48 | #define FPDF_UNSP_ANNOT_SIG 17 49 | 50 | // Interface for unsupported feature notifications. 51 | typedef struct _UNSUPPORT_INFO { 52 | // Version number of the interface. Must be 1. 53 | int version; 54 | 55 | // Unsupported object notification function. 56 | // Interface Version: 1 57 | // Implementation Required: Yes 58 | // 59 | // pThis - pointer to the interface structure. 60 | // nType - the type of unsupported object. One of the |FPDF_UNSP_*| entries. 61 | void (*FSDK_UnSupport_Handler)(struct _UNSUPPORT_INFO* pThis, int nType); 62 | } UNSUPPORT_INFO; 63 | 64 | // Setup an unsupported object handler. 65 | // 66 | // unsp_info - Pointer to an UNSUPPORT_INFO structure. 67 | // 68 | // Returns TRUE on success. 69 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV 70 | FSDK_SetUnSpObjProcessHandler(UNSUPPORT_INFO* unsp_info); 71 | 72 | // Set replacement function for calls to time(). 73 | // 74 | // This API is intended to be used only for testing, thus may cause PDFium to 75 | // behave poorly in production environments. 76 | // 77 | // func - Function pointer to alternate implementation of time(), or 78 | // NULL to restore to actual time() call itself. 79 | FPDF_EXPORT void FPDF_CALLCONV FSDK_SetTimeFunction(time_t (*func)()); 80 | 81 | // Set replacement function for calls to localtime(). 82 | // 83 | // This API is intended to be used only for testing, thus may cause PDFium to 84 | // behave poorly in production environments. 85 | // 86 | // func - Function pointer to alternate implementation of localtime(), or 87 | // NULL to restore to actual localtime() call itself. 88 | FPDF_EXPORT void FPDF_CALLCONV 89 | FSDK_SetLocaltimeFunction(struct tm* (*func)(const time_t*)); 90 | 91 | // Unknown page mode. 92 | #define PAGEMODE_UNKNOWN -1 93 | // Document outline, and thumbnails hidden. 94 | #define PAGEMODE_USENONE 0 95 | // Document outline visible. 96 | #define PAGEMODE_USEOUTLINES 1 97 | // Thumbnail images visible. 98 | #define PAGEMODE_USETHUMBS 2 99 | // Full-screen mode, no menu bar, window controls, or other decorations visible. 100 | #define PAGEMODE_FULLSCREEN 3 101 | // Optional content group panel visible. 102 | #define PAGEMODE_USEOC 4 103 | // Attachments panel visible. 104 | #define PAGEMODE_USEATTACHMENTS 5 105 | 106 | // Get the document's PageMode. 107 | // 108 | // doc - Handle to document. 109 | // 110 | // Returns one of the |PAGEMODE_*| flags defined above. 111 | // 112 | // The page mode defines how the document should be initially displayed. 113 | FPDF_EXPORT int FPDF_CALLCONV FPDFDoc_GetPageMode(FPDF_DOCUMENT document); 114 | 115 | #ifdef __cplusplus 116 | } // extern "C" 117 | #endif // __cplusplus 118 | 119 | #endif // PUBLIC_FPDF_EXT_H_ 120 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/utils/Mutex.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef _LIBS_UTILS_MUTEX_H 18 | #define _LIBS_UTILS_MUTEX_H 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #if defined(HAVE_PTHREADS) 25 | # include 26 | #endif 27 | 28 | #include "Errors.h" 29 | 30 | // --------------------------------------------------------------------------- 31 | namespace android { 32 | // --------------------------------------------------------------------------- 33 | 34 | class Condition; 35 | 36 | /* 37 | * Simple mutex class. The implementation is system-dependent. 38 | * 39 | * The mutex must be unlocked by the thread that locked it. They are not 40 | * recursive, i.e. the same thread can't lock it multiple times. 41 | */ 42 | class Mutex { 43 | public: 44 | enum { 45 | PRIVATE = 0, 46 | SHARED = 1 47 | }; 48 | 49 | Mutex(); 50 | Mutex(const char* name); 51 | Mutex(int type, const char* name = NULL); 52 | ~Mutex(); 53 | 54 | // lock or unlock the mutex 55 | status_t lock(); 56 | void unlock(); 57 | 58 | // lock if possible; returns 0 on success, error otherwise 59 | status_t tryLock(); 60 | 61 | // Manages the mutex automatically. It'll be locked when Autolock is 62 | // constructed and released when Autolock goes out of scope. 63 | class Autolock { 64 | public: 65 | inline Autolock(Mutex& mutex) : mLock(mutex) { mLock.lock(); } 66 | inline Autolock(Mutex* mutex) : mLock(*mutex) { mLock.lock(); } 67 | inline ~Autolock() { mLock.unlock(); } 68 | private: 69 | Mutex& mLock; 70 | }; 71 | 72 | private: 73 | friend class Condition; 74 | 75 | // A mutex cannot be copied 76 | Mutex(const Mutex&); 77 | Mutex& operator = (const Mutex&); 78 | 79 | #if defined(HAVE_PTHREADS) 80 | pthread_mutex_t mMutex; 81 | #else 82 | void _init(); 83 | void* mState; 84 | #endif 85 | }; 86 | 87 | // --------------------------------------------------------------------------- 88 | 89 | #if defined(HAVE_PTHREADS) 90 | 91 | inline Mutex::Mutex() { 92 | pthread_mutex_init(&mMutex, NULL); 93 | } 94 | inline Mutex::Mutex(__attribute__((unused)) const char* name) { 95 | pthread_mutex_init(&mMutex, NULL); 96 | } 97 | inline Mutex::Mutex(int type, __attribute__((unused)) const char* name) { 98 | if (type == SHARED) { 99 | pthread_mutexattr_t attr; 100 | pthread_mutexattr_init(&attr); 101 | pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); 102 | pthread_mutex_init(&mMutex, &attr); 103 | pthread_mutexattr_destroy(&attr); 104 | } else { 105 | pthread_mutex_init(&mMutex, NULL); 106 | } 107 | } 108 | inline Mutex::~Mutex() { 109 | pthread_mutex_destroy(&mMutex); 110 | } 111 | inline status_t Mutex::lock() { 112 | return -pthread_mutex_lock(&mMutex); 113 | } 114 | inline void Mutex::unlock() { 115 | pthread_mutex_unlock(&mMutex); 116 | } 117 | inline status_t Mutex::tryLock() { 118 | return -pthread_mutex_trylock(&mMutex); 119 | } 120 | 121 | #endif // HAVE_PTHREADS 122 | 123 | // --------------------------------------------------------------------------- 124 | 125 | /* 126 | * Automatic mutex. Declare one of these at the top of a function. 127 | * When the function returns, it will go out of scope, and release the 128 | * mutex. 129 | */ 130 | 131 | typedef Mutex::Autolock AutoMutex; 132 | 133 | // --------------------------------------------------------------------------- 134 | }; // namespace android 135 | // --------------------------------------------------------------------------- 136 | 137 | #endif // _LIBS_UTILS_MUTEX_H 138 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin-stdlib = "2.2.21" 3 | kotlin_version = "2.2.21" 4 | ksp_version = "2.3.0" 5 | hilt_version = "2.57.2" 6 | gradleplugin = "8.13.1" 7 | detekt_version = "1.23.8" 8 | kover_version = "0.9.3" 9 | ktlint_version = "14.0.1" 10 | activity-compose = "1.11.0" 11 | compose-bom = "2025.11.00" 12 | androidx-junit = "1.3.0" 13 | appcompat = "1.7.1" 14 | coil-compose = "2.7.0" 15 | glide = "1.0.0-beta08" 16 | core-ktx = "1.17.0" 17 | core-testing = "2.2.0" 18 | espresso-core = "3.7.0" 19 | hilt-android = "2.57.2" 20 | junit = "4.13.2" 21 | kotlinx-coroutines-android = "1.10.2" 22 | lifecycle-runtime-ktx = "2.9.4" 23 | material = "1.13.0" 24 | timber = "5.0.1" 25 | truth = "1.4.5" 26 | arrow = "2.2.0" 27 | annotation-jvm = "1.9.1" 28 | jreleaser = "1.20.0" 29 | runner = "1.7.0" 30 | guava = "33.5.0-android" 31 | 32 | [libraries] 33 | compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } 34 | compose-ui = { module = "androidx.compose.ui:ui" } 35 | compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } 36 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 37 | compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } 38 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 39 | compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } 40 | compose-material3 = { module = "androidx.compose.material3:material3" } 41 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } 42 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } 43 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } 44 | androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "core-testing" } 45 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" } 46 | androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } 47 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } 48 | coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" } 49 | compose-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } 50 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" } 51 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" } 52 | junit = { module = "junit:junit", version.ref = "junit" } 53 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-stdlib" } 54 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" } 55 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-android" } 56 | material = { module = "com.google.android.material:material", version.ref = "material" } 57 | timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } 58 | truth = { module = "com.google.truth:truth", version.ref = "truth" } 59 | arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } 60 | arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } 61 | androidx-annotation-jvm = { group = "androidx.annotation", name = "annotation-jvm", version.ref = "annotation-jvm" } 62 | androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } 63 | guava = { module = "com.google.guava:guava", version.ref = "guava" } 64 | 65 | [bundles] 66 | 67 | [plugins] 68 | android-application = { id = "com.android.application", version.ref = "gradleplugin" } 69 | android-library = { id = "com.android.library", version.ref = "gradleplugin" } 70 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version" } 71 | hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt_version" } 72 | kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp_version" } 73 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin_version" } 74 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt_version" } 75 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover_version" } 76 | ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint_version" } 77 | jreleaser = { id = "org.jreleaser", version.ref = "jreleaser" } -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/PdfTextPageKt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.suspend 4 | 5 | import android.graphics.RectF 6 | import androidx.annotation.Keep 7 | import io.legere.pdfiumandroid.FindFlags 8 | import io.legere.pdfiumandroid.Logger 9 | import io.legere.pdfiumandroid.PdfTextPage 10 | import io.legere.pdfiumandroid.WordRangeRect 11 | import kotlinx.coroutines.CoroutineDispatcher 12 | import kotlinx.coroutines.withContext 13 | import java.io.Closeable 14 | 15 | /** 16 | * PdfTextPageKt represents a single text page of a PDF file. 17 | * @property page the [PdfTextPage] to wrap 18 | * @property dispatcher the [CoroutineDispatcher] to use for suspending calls 19 | */ 20 | @Suppress("TooManyFunctions") 21 | @Keep 22 | class PdfTextPageKt( 23 | val page: PdfTextPage, 24 | private val dispatcher: CoroutineDispatcher, 25 | ) : Closeable { 26 | /** 27 | * suspend version of [PdfTextPage.textPageCountChars] 28 | */ 29 | suspend fun textPageCountChars(): Int = 30 | withContext(dispatcher) { 31 | page.textPageCountChars() 32 | } 33 | 34 | /** 35 | * suspend version of [PdfTextPage.textPageGetText] 36 | */ 37 | suspend fun textPageGetText( 38 | startIndex: Int, 39 | length: Int, 40 | ): String? = 41 | withContext(dispatcher) { 42 | page.textPageGetText(startIndex, length) 43 | } 44 | 45 | /** 46 | * suspend version of [PdfTextPage.textPageGetUnicode] 47 | */ 48 | suspend fun textPageGetUnicode(index: Int): Char = 49 | withContext(dispatcher) { 50 | page.textPageGetUnicode(index) 51 | } 52 | 53 | /** 54 | * suspend version of [PdfTextPage.textPageGetCharBox] 55 | */ 56 | suspend fun textPageGetCharBox(index: Int): RectF? = 57 | withContext(dispatcher) { 58 | page.textPageGetCharBox(index) 59 | } 60 | 61 | /** 62 | * suspend version of [PdfTextPage.textPageGetCharIndexAtPos] 63 | */ 64 | suspend fun textPageGetCharIndexAtPos( 65 | x: Double, 66 | y: Double, 67 | xTolerance: Double, 68 | yTolerance: Double, 69 | ): Int = 70 | withContext(dispatcher) { 71 | page.textPageGetCharIndexAtPos(x, y, xTolerance, yTolerance) 72 | } 73 | 74 | /** 75 | * suspend version of [PdfTextPage.textPageCountRects] 76 | */ 77 | suspend fun textPageCountRects( 78 | startIndex: Int, 79 | count: Int, 80 | ): Int = 81 | withContext(dispatcher) { 82 | page.textPageCountRects(startIndex, count) 83 | } 84 | 85 | /** 86 | * suspend version of [PdfTextPage.textPageGetRect] 87 | */ 88 | suspend fun textPageGetRect(rectIndex: Int): RectF? = 89 | withContext(dispatcher) { 90 | page.textPageGetRect(rectIndex) 91 | } 92 | 93 | /** 94 | * suspend version of [PdfTextPage.textPageGetRectsForRanges] 95 | */ 96 | suspend fun textPageGetRectsForRanges(wordRanges: IntArray): List? = 97 | withContext(dispatcher) { 98 | page.textPageGetRectsForRanges(wordRanges) 99 | } 100 | 101 | /** 102 | * suspend version of [PdfTextPage.textPageGetBoundedText] 103 | */ 104 | suspend fun textPageGetBoundedText( 105 | rect: RectF, 106 | length: Int, 107 | ): String? = 108 | withContext(dispatcher) { 109 | page.textPageGetBoundedText(rect, length) 110 | } 111 | 112 | /** 113 | * suspend version of [PdfTextPage.getFontSize] 114 | */ 115 | suspend fun getFontSize(charIndex: Int): Double = 116 | withContext(dispatcher) { 117 | page.getFontSize(charIndex) 118 | } 119 | 120 | suspend fun findStart( 121 | findWhat: String, 122 | flags: Set, 123 | startIndex: Int, 124 | ): FindResultKt? = 125 | withContext(dispatcher) { 126 | val findResult = page.findStart(findWhat, flags, startIndex) 127 | if (findResult == null) { 128 | null 129 | } else { 130 | FindResultKt(findResult, dispatcher) 131 | } 132 | } 133 | 134 | suspend fun loadWebLink(): PdfPageLinkKt = 135 | withContext(dispatcher) { 136 | PdfPageLinkKt(page.loadWebLink(), dispatcher) 137 | } 138 | 139 | /** 140 | * Close the page and free all resources. 141 | */ 142 | override fun close() { 143 | page.close() 144 | } 145 | 146 | fun safeClose(): Boolean = 147 | try { 148 | page.close() 149 | true 150 | } catch (e: IllegalStateException) { 151 | Logger.e("PdfTextPageKt", e, "PdfTextPageKt.safeClose") 152 | false 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_ppo.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 | 7 | #ifndef PUBLIC_FPDF_PPO_H_ 8 | #define PUBLIC_FPDF_PPO_H_ 9 | 10 | // NOLINTNEXTLINE(build/include) 11 | #include "fpdfview.h" 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | // Experimental API. 18 | // Import pages to a FPDF_DOCUMENT. 19 | // 20 | // dest_doc - The destination document for the pages. 21 | // src_doc - The document to be imported. 22 | // page_indices - An array of page indices to be imported. The first page is 23 | // zero. If |page_indices| is NULL, all pages from |src_doc| 24 | // are imported. 25 | // length - The length of the |page_indices| array. 26 | // index - The page index at which to insert the first imported page 27 | // into |dest_doc|. The first page is zero. 28 | // 29 | // Returns TRUE on success. Returns FALSE if any pages in |page_indices| is 30 | // invalid. 31 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV 32 | FPDF_ImportPagesByIndex(FPDF_DOCUMENT dest_doc, 33 | FPDF_DOCUMENT src_doc, 34 | const int* page_indices, 35 | unsigned long length, 36 | int index); 37 | 38 | // Import pages to a FPDF_DOCUMENT. 39 | // 40 | // dest_doc - The destination document for the pages. 41 | // src_doc - The document to be imported. 42 | // pagerange - A page range string, Such as "1,3,5-7". The first page is one. 43 | // If |pagerange| is NULL, all pages from |src_doc| are imported. 44 | // index - The page index at which to insert the first imported page into 45 | // |dest_doc|. The first page is zero. 46 | // 47 | // Returns TRUE on success. Returns FALSE if any pages in |pagerange| is 48 | // invalid or if |pagerange| cannot be read. 49 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDF_ImportPages(FPDF_DOCUMENT dest_doc, 50 | FPDF_DOCUMENT src_doc, 51 | FPDF_BYTESTRING pagerange, 52 | int index); 53 | 54 | // Experimental API. 55 | // Create a new document from |src_doc|. The pages of |src_doc| will be 56 | // combined to provide |num_pages_on_x_axis x num_pages_on_y_axis| pages per 57 | // |output_doc| page. 58 | // 59 | // src_doc - The document to be imported. 60 | // output_width - The output page width in PDF "user space" units. 61 | // output_height - The output page height in PDF "user space" units. 62 | // num_pages_on_x_axis - The number of pages on X Axis. 63 | // num_pages_on_y_axis - The number of pages on Y Axis. 64 | // 65 | // Return value: 66 | // A handle to the created document, or NULL on failure. 67 | // 68 | // Comments: 69 | // number of pages per page = num_pages_on_x_axis * num_pages_on_y_axis 70 | // 71 | FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV 72 | FPDF_ImportNPagesToOne(FPDF_DOCUMENT src_doc, 73 | float output_width, 74 | float output_height, 75 | size_t num_pages_on_x_axis, 76 | size_t num_pages_on_y_axis); 77 | 78 | // Experimental API. 79 | // Create a template to generate form xobjects from |src_doc|'s page at 80 | // |src_page_index|, for use in |dest_doc|. 81 | // 82 | // Returns a handle on success, or NULL on failure. Caller owns the newly 83 | // created object. 84 | FPDF_EXPORT FPDF_XOBJECT FPDF_CALLCONV 85 | FPDF_NewXObjectFromPage(FPDF_DOCUMENT dest_doc, 86 | FPDF_DOCUMENT src_doc, 87 | int src_page_index); 88 | 89 | // Experimental API. 90 | // Close an FPDF_XOBJECT handle created by FPDF_NewXObjectFromPage(). 91 | // FPDF_PAGEOBJECTs created from the FPDF_XOBJECT handle are not affected. 92 | FPDF_EXPORT void FPDF_CALLCONV FPDF_CloseXObject(FPDF_XOBJECT xobject); 93 | 94 | // Experimental API. 95 | // Create a new form object from an FPDF_XOBJECT object. 96 | // 97 | // Returns a new form object on success, or NULL on failure. Caller owns the 98 | // newly created object. 99 | FPDF_EXPORT FPDF_PAGEOBJECT FPDF_CALLCONV 100 | FPDF_NewFormObjectFromXObject(FPDF_XOBJECT xobject); 101 | 102 | // Copy the viewer preferences from |src_doc| into |dest_doc|. 103 | // 104 | // dest_doc - Document to write the viewer preferences into. 105 | // src_doc - Document to read the viewer preferences from. 106 | // 107 | // Returns TRUE on success. 108 | FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV 109 | FPDF_CopyViewerPreferences(FPDF_DOCUMENT dest_doc, FPDF_DOCUMENT src_doc); 110 | 111 | #ifdef __cplusplus 112 | } // extern "C" 113 | #endif // __cplusplus 114 | 115 | #endif // PUBLIC_FPDF_PPO_H_ 116 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfTextPageKtF.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.arrow 4 | 5 | import android.graphics.RectF 6 | import arrow.core.Either 7 | import io.legere.pdfiumandroid.FindFlags 8 | import io.legere.pdfiumandroid.PdfTextPage 9 | import io.legere.pdfiumandroid.WordRangeRect 10 | import kotlinx.coroutines.CoroutineDispatcher 11 | import java.io.Closeable 12 | 13 | /** 14 | * PdfTextPageKtF represents a single text page of a PDF file. 15 | * @property page the [PdfTextPage] to wrap 16 | * @property dispatcher the [CoroutineDispatcher] to use for suspending calls 17 | */ 18 | @Suppress("TooManyFunctions") 19 | class PdfTextPageKtF( 20 | val page: PdfTextPage, 21 | private val dispatcher: CoroutineDispatcher, 22 | ) : Closeable { 23 | /** 24 | * suspend version of [PdfTextPage.textPageCountChars] 25 | */ 26 | suspend fun textPageCountChars(): Either = 27 | wrapEither(dispatcher) { 28 | page.textPageCountChars() 29 | } 30 | 31 | /** 32 | * suspend version of [PdfTextPage.textPageGetText] 33 | */ 34 | suspend fun textPageGetText( 35 | startIndex: Int, 36 | length: Int, 37 | ): Either = 38 | wrapEither(dispatcher) { 39 | page.textPageGetText(startIndex, length) 40 | } 41 | 42 | /** 43 | * suspend version of [PdfTextPage.textPageGetUnicode] 44 | */ 45 | suspend fun textPageGetUnicode(index: Int): Either = 46 | wrapEither(dispatcher) { 47 | page.textPageGetUnicode(index) 48 | } 49 | 50 | /** 51 | * suspend version of [PdfTextPage.textPageGetCharBox] 52 | */ 53 | suspend fun textPageGetCharBox(index: Int): Either = 54 | wrapEither(dispatcher) { 55 | page.textPageGetCharBox(index) 56 | } 57 | 58 | /** 59 | * suspend version of [PdfTextPage.textPageGetCharIndexAtPos] 60 | */ 61 | suspend fun textPageGetCharIndexAtPos( 62 | x: Double, 63 | y: Double, 64 | xTolerance: Double, 65 | yTolerance: Double, 66 | ): Either = 67 | wrapEither(dispatcher) { 68 | page.textPageGetCharIndexAtPos(x, y, xTolerance, yTolerance) 69 | } 70 | 71 | /** 72 | * suspend version of [PdfTextPage.textPageCountRects] 73 | */ 74 | suspend fun textPageCountRects( 75 | startIndex: Int, 76 | count: Int, 77 | ): Either = 78 | wrapEither(dispatcher) { 79 | page.textPageCountRects(startIndex, count) 80 | } 81 | 82 | /** 83 | * suspend version of [PdfTextPage.textPageGetRect] 84 | */ 85 | suspend fun textPageGetRect(rectIndex: Int): Either = 86 | wrapEither(dispatcher) { 87 | page.textPageGetRect(rectIndex) 88 | } 89 | 90 | /** 91 | * suspend version of [PdfTextPage.textPageGetRectsForRanges] 92 | */ 93 | suspend fun textPageGetRectsForRanges(wordRanges: IntArray): Either?> = 94 | wrapEither(dispatcher) { 95 | page.textPageGetRectsForRanges(wordRanges) 96 | } 97 | 98 | /** 99 | * suspend version of [PdfTextPage.textPageGetBoundedText] 100 | */ 101 | suspend fun textPageGetBoundedText( 102 | rect: RectF, 103 | length: Int, 104 | ): Either = 105 | wrapEither(dispatcher) { 106 | page.textPageGetBoundedText(rect, length) 107 | } 108 | 109 | /** 110 | * suspend version of [PdfTextPage.getFontSize] 111 | */ 112 | suspend fun getFontSize(charIndex: Int): Either = 113 | wrapEither(dispatcher) { 114 | page.getFontSize(charIndex) 115 | } 116 | 117 | suspend fun findStart( 118 | findWhat: String, 119 | flags: Set, 120 | startIndex: Int, 121 | ): Either = 122 | wrapEither(dispatcher) { 123 | val findResult = page.findStart(findWhat, flags, startIndex) 124 | if (findResult == null) { 125 | error("findResult is null") 126 | } else { 127 | FindResultKtF(findResult, dispatcher) 128 | } 129 | } 130 | 131 | suspend fun loadWebLink(): Either = 132 | wrapEither(dispatcher) { 133 | PdfPageLinkKtF(page.loadWebLink(), dispatcher) 134 | } 135 | 136 | /** 137 | * Close the page and free all resources. 138 | */ 139 | override fun close() { 140 | page.close() 141 | } 142 | 143 | fun safeClose(): Either = 144 | Either 145 | .catch { 146 | page.close() 147 | true 148 | }.mapLeft { exceptionToPdfiumKtFError(it) } 149 | } 150 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/src/main/java/io/legere/pdfiumandroid/arrow/PdfDocumentKtF.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused", "CanBeVal") 2 | 3 | package io.legere.pdfiumandroid.arrow 4 | 5 | import android.graphics.Matrix 6 | import android.graphics.RectF 7 | import android.view.Surface 8 | import arrow.core.Either 9 | import io.legere.pdfiumandroid.PdfDocument 10 | import io.legere.pdfiumandroid.PdfWriteCallback 11 | import kotlinx.coroutines.CoroutineDispatcher 12 | import kotlinx.coroutines.withContext 13 | import java.io.Closeable 14 | 15 | /** 16 | * PdfDocumentKtF represents a PDF file and allows you to load pages from it. 17 | * @property document the [PdfDocument] to wrap 18 | * @property dispatcher the [CoroutineDispatcher] to use for suspending calls 19 | * @constructor create a [PdfDocumentKtF] from a [PdfDocument] 20 | */ 21 | @Suppress("TooManyFunctions", "CanBeVal") 22 | class PdfDocumentKtF( 23 | val document: PdfDocument, 24 | private val dispatcher: CoroutineDispatcher, 25 | ) : Closeable { 26 | /** 27 | * suspend version of [PdfDocument.getPageCount] 28 | */ 29 | suspend fun getPageCount(): Either = 30 | wrapEither(dispatcher) { 31 | document.getPageCount() 32 | } 33 | 34 | /** 35 | * suspend version of [PdfDocument.getPageCharCounts] 36 | */ 37 | suspend fun getPageCharCounts(): Either = 38 | wrapEither(dispatcher) { 39 | document.getPageCharCounts() 40 | } 41 | 42 | /** 43 | * suspend version of [PdfDocument.openPage] 44 | */ 45 | suspend fun openPage(pageIndex: Int): Either = 46 | wrapEither(dispatcher) { 47 | PdfPageKtF(document.openPage(pageIndex), dispatcher) 48 | } 49 | 50 | /** 51 | * suspend version of [PdfDocument.openPages] 52 | */ 53 | suspend fun openPages( 54 | fromIndex: Int, 55 | toIndex: Int, 56 | ): Either> = 57 | wrapEither(dispatcher) { 58 | document.openPages(fromIndex, toIndex).map { PdfPageKtF(it, dispatcher) } 59 | } 60 | 61 | /** 62 | * suspend version of [PdfDocument.renderPages] 63 | */ 64 | @Suppress("LongParameterList", "ComplexMethod", "ComplexCondition") 65 | suspend fun renderPages( 66 | surface: Surface?, 67 | pages: List, 68 | matrices: List, 69 | clipRects: List, 70 | renderAnnot: Boolean = false, 71 | textMask: Boolean = false, 72 | canvasColor: Int = 0xFF848484.toInt(), 73 | pageBackgroundColor: Int = 0xFFFFFFFF.toInt(), 74 | renderCoroutinesDispatcher: CoroutineDispatcher, 75 | ): Boolean { 76 | return withContext(renderCoroutinesDispatcher) { 77 | return@withContext surface 78 | ?.let { 79 | document.renderPages( 80 | surface, 81 | pages.map { page -> page.page }, 82 | matrices, 83 | clipRects, 84 | renderAnnot, 85 | textMask, 86 | canvasColor, 87 | pageBackgroundColor, 88 | ) 89 | } ?: false 90 | } 91 | } 92 | 93 | /** 94 | * suspend version of [PdfDocument.deletePage] 95 | */ 96 | suspend fun deletePage(pageIndex: Int): Unit = 97 | withContext(dispatcher) { 98 | document.deletePage(pageIndex) 99 | } 100 | 101 | /** 102 | * suspend version of [PdfDocument.getDocumentMeta] 103 | */ 104 | suspend fun getDocumentMeta(): Either = 105 | wrapEither(dispatcher) { 106 | document.getDocumentMeta() 107 | } 108 | 109 | /** 110 | * suspend version of [PdfDocument.getTableOfContents] 111 | */ 112 | suspend fun getTableOfContents(): Either> = 113 | wrapEither(dispatcher) { 114 | document.getTableOfContents() 115 | } 116 | 117 | /** 118 | * suspend version of [PdfDocument.openTextPages] 119 | */ 120 | suspend fun openTextPages( 121 | fromIndex: Int, 122 | toIndex: Int, 123 | ): Either> = 124 | wrapEither(dispatcher) { 125 | document.openTextPages(fromIndex, toIndex).map { PdfTextPageKtF(it, dispatcher) } 126 | } 127 | 128 | /** 129 | * suspend version of [PdfDocument.saveAsCopy] 130 | */ 131 | suspend fun saveAsCopy(callback: PdfWriteCallback): Either = 132 | wrapEither(dispatcher) { 133 | document.saveAsCopy(callback) 134 | } 135 | 136 | /** 137 | * Close the document 138 | * @throws IllegalArgumentException if document is closed 139 | */ 140 | override fun close() { 141 | document.close() 142 | } 143 | 144 | fun safeClose(): Either = 145 | Either 146 | .catch { 147 | document.close() 148 | true 149 | }.mapLeft { exceptionToPdfiumKtFError(it) } 150 | } 151 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/java/io/legere/pdfiumandroid/suspend/PdfDocumentKt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legere.pdfiumandroid.suspend 4 | 5 | import android.graphics.Matrix 6 | import android.graphics.RectF 7 | import android.view.Surface 8 | import androidx.annotation.Keep 9 | import io.legere.pdfiumandroid.Logger 10 | import io.legere.pdfiumandroid.PdfDocument 11 | import io.legere.pdfiumandroid.PdfWriteCallback 12 | import io.legere.pdfiumandroid.PdfiumCore 13 | import kotlinx.coroutines.CoroutineDispatcher 14 | import kotlinx.coroutines.sync.withLock 15 | import kotlinx.coroutines.withContext 16 | import java.io.Closeable 17 | 18 | /** 19 | * PdfDocumentKt represents a PDF file and allows you to load pages from it. 20 | * @property document the [PdfDocument] to wrap 21 | * @property dispatcher the [CoroutineDispatcher] to use for suspending calls 22 | * @constructor create a [PdfDocumentKt] from a [PdfDocument] 23 | */ 24 | @Suppress("TooManyFunctions") 25 | @Keep 26 | class PdfDocumentKt( 27 | val document: PdfDocument, 28 | private val dispatcher: CoroutineDispatcher, 29 | ) : Closeable { 30 | /** 31 | * suspend version of [PdfDocument.getPageCount] 32 | */ 33 | suspend fun getPageCount(): Int = 34 | withContext(dispatcher) { 35 | document.getPageCount() 36 | } 37 | 38 | /** 39 | * suspend version of [PdfDocument.getPageCharCounts] 40 | */ 41 | suspend fun getPageCharCounts(): IntArray = 42 | withContext(dispatcher) { 43 | document.getPageCharCounts() 44 | } 45 | 46 | /** 47 | * suspend version of [PdfDocument.openPage] 48 | */ 49 | suspend fun openPage(pageIndex: Int): PdfPageKt = 50 | withContext(dispatcher) { 51 | PdfPageKt(document.openPage(pageIndex), dispatcher) 52 | } 53 | 54 | /** 55 | * suspend version of [PdfDocument.deletePage] 56 | */ 57 | suspend fun deletePage(pageIndex: Int): Unit = 58 | withContext(dispatcher) { 59 | document.deletePage(pageIndex) 60 | } 61 | 62 | /** 63 | * suspend version of [PdfDocument.openPages] 64 | */ 65 | suspend fun openPages( 66 | fromIndex: Int, 67 | toIndex: Int, 68 | ): List = 69 | withContext(dispatcher) { 70 | document.openPages(fromIndex, toIndex).map { PdfPageKt(it, dispatcher) } 71 | } 72 | 73 | /** 74 | * suspend version of [PdfDocument.renderPages] 75 | */ 76 | @Suppress("LongParameterList", "ComplexMethod", "ComplexCondition") 77 | suspend fun renderPages( 78 | surface: Surface, 79 | pages: List, 80 | matrices: List, 81 | clipRects: List, 82 | renderAnnot: Boolean = false, 83 | textMask: Boolean = false, 84 | canvasColor: Int = 0xFF848484.toInt(), 85 | pageBackgroundColor: Int = 0xFFFFFFFF.toInt(), 86 | renderCoroutinesDispatcher: CoroutineDispatcher, 87 | ): Boolean { 88 | PdfiumCore.surfaceMutex.withLock { 89 | return withContext(renderCoroutinesDispatcher) { 90 | return@withContext document.renderPages( 91 | surface, 92 | pages.map { it.page }, 93 | matrices, 94 | clipRects, 95 | renderAnnot, 96 | textMask, 97 | canvasColor, 98 | pageBackgroundColor, 99 | ) 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * suspend version of [PdfDocument.getDocumentMeta] 106 | */ 107 | suspend fun getDocumentMeta(): PdfDocument.Meta = 108 | withContext(dispatcher) { 109 | document.getDocumentMeta() 110 | } 111 | 112 | /** 113 | * suspend version of [PdfDocument.getTableOfContents] 114 | */ 115 | suspend fun getTableOfContents(): List = 116 | withContext(dispatcher) { 117 | document.getTableOfContents() 118 | } 119 | 120 | /** 121 | * suspend version of [PdfDocument.openTextPage] 122 | */ 123 | @Deprecated("use PdfPageKt.openTextPage", ReplaceWith("page.openTextPage()")) 124 | @Suppress("DEPRECATION") 125 | suspend fun openTextPage(page: PdfPageKt): PdfTextPageKt = 126 | withContext(dispatcher) { 127 | PdfTextPageKt(document.openTextPage(page.page), dispatcher) 128 | } 129 | 130 | /** 131 | * suspend version of [PdfDocument.openTextPages] 132 | */ 133 | suspend fun openTextPages( 134 | fromIndex: Int, 135 | toIndex: Int, 136 | ): List = 137 | withContext(dispatcher) { 138 | document.openTextPages(fromIndex, toIndex).map { PdfTextPageKt(it, dispatcher) } 139 | } 140 | 141 | /** 142 | * suspend version of [PdfDocument.saveAsCopy] 143 | */ 144 | suspend fun saveAsCopy(callback: PdfWriteCallback): Boolean = 145 | withContext(dispatcher) { 146 | document.saveAsCopy(callback) 147 | } 148 | 149 | /** 150 | * Close the document 151 | * @throws IllegalArgumentException if document is closed 152 | */ 153 | override fun close() { 154 | document.close() 155 | } 156 | 157 | fun safeClose(): Boolean = 158 | try { 159 | document.close() 160 | true 161 | } catch (e: IllegalStateException) { 162 | Logger.e("PdfDocumentKt", e, "PdfDocumentKt.safeClose") 163 | false 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pdfiumandroid/arrow/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jreleaser.model.Active 3 | import org.jreleaser.model.Signing 4 | 5 | 6 | plugins { 7 | alias(libs.plugins.android.library) 8 | alias(libs.plugins.kotlin.android) 9 | alias(libs.plugins.detekt) 10 | alias(libs.plugins.kover) 11 | alias(libs.plugins.ktlint) 12 | alias(libs.plugins.jreleaser) 13 | `maven-publish` 14 | signing 15 | } 16 | kotlin { 17 | compilerOptions { 18 | jvmTarget.set(JvmTarget.JVM_17) 19 | } 20 | } 21 | 22 | android { 23 | namespace = "io.legere.pdfiumandroid.arrow" 24 | compileSdk = 36 25 | 26 | defaultConfig { 27 | minSdk = 24 28 | 29 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 30 | consumerProguardFiles("consumer-rules.pro") 31 | } 32 | 33 | buildTypes { 34 | release { 35 | isMinifyEnabled = false 36 | proguardFiles( 37 | getDefaultProguardFile("proguard-android-optimize.txt"), 38 | "proguard-rules.pro", 39 | ) 40 | } 41 | } 42 | compileOptions { 43 | sourceCompatibility(JavaVersion.VERSION_17) 44 | targetCompatibility(JavaVersion.VERSION_17) 45 | } 46 | publishing { 47 | singleVariant("release") { 48 | // if you don't want sources/javadoc, remove these lines 49 | withSourcesJar() 50 | withJavadocJar() 51 | } 52 | } 53 | } 54 | 55 | dependencies { 56 | implementation(project(":pdfiumandroid")) 57 | compileOnly(libs.arrow.core) 58 | compileOnly(libs.kotlinx.coroutines.android) 59 | compileOnly(libs.androidx.annotation.jvm) 60 | compileOnly(libs.kotlin.stdlib) 61 | implementation(libs.guava) 62 | 63 | testImplementation(libs.androidx.runner) 64 | testImplementation(libs.junit) 65 | androidTestImplementation(libs.androidx.junit) 66 | androidTestImplementation(libs.androidx.espresso.core) 67 | androidTestImplementation(libs.truth) 68 | androidTestImplementation(libs.kotlinx.coroutines.test) 69 | androidTestImplementation(libs.androidx.core.testing) 70 | androidTestImplementation(libs.arrow.fx.coroutines) 71 | } 72 | 73 | fun getRepositoryUsername(): String = 74 | if (rootProject.hasProperty("JRELEASER_MAVENCENTRAL_USERNAME")) { 75 | rootProject.properties["JRELEASER_MAVENCENTRAL_USERNAME"] as String 76 | } else { 77 | "" 78 | } 79 | 80 | publishing { 81 | publications { 82 | create("maven") { 83 | groupId = "io.legere" 84 | artifactId = "pdfium-android-kt-arrow" 85 | version = project.property("VERSION_NAME") as String 86 | 87 | pom { 88 | name.set("pdfiumandroid.arrow") 89 | description.set("Arrow support for PdfiumAndroid") 90 | url.set(rootProject.properties["POM_URL"] as String) 91 | licenses { 92 | license { 93 | name.set(rootProject.properties["POM_LICENCE_NAME"] as String) 94 | url.set(rootProject.properties["POM_LICENCE_URL"] as String) 95 | distribution.set(rootProject.properties["POM_LICENCE_DIST"] as String) 96 | } 97 | } 98 | developers { 99 | developer { 100 | id.set(rootProject.properties["POM_DEVELOPER_ID"] as String) 101 | name.set(rootProject.properties["POM_DEVELOPER_NAME"] as String) 102 | } 103 | } 104 | scm { 105 | connection.set(rootProject.properties["POM_SCM_CONNECTION"] as String) 106 | developerConnection.set(rootProject.properties["POM_SCM_DEV_CONNECTION"] as String) 107 | url.set(rootProject.properties["POM_SCM_URL"] as String) 108 | } 109 | } 110 | afterEvaluate { 111 | from(components["release"]) 112 | } 113 | } 114 | } 115 | repositories { 116 | maven { 117 | url = 118 | uri(layout.buildDirectory.dir("target/staging-deploy")) 119 | } 120 | } 121 | } 122 | 123 | jreleaser { 124 | project { 125 | inceptionYear = "2023" 126 | author("@johngray1965") 127 | description = "Arrow support for PdfiumAndroid" 128 | version = rootProject.properties["VERSION_NAME"] as String 129 | license = "http://www.apache.org/licenses/LICENSE-2.0.txt" 130 | links { 131 | homepage = "https://github.com/johngray1965/PdfiumAndroidKt" 132 | license = "http://www.apache.org/licenses/LICENSE-2.0.txt" 133 | } 134 | } 135 | gitRootSearch = true 136 | signing { 137 | active = Active.ALWAYS 138 | mode = Signing.Mode.MEMORY 139 | armored = true 140 | verify = true 141 | } 142 | release { 143 | github { 144 | repoOwner = "johngray1965" 145 | overwrite = true 146 | } 147 | } 148 | // distributions { 149 | // create("pdfiumandroid.arrow") { 150 | // artifact { 151 | // path = file("build/distributions/{{distributionName}}-{{projectVersion}}.zip") 152 | // } 153 | // } 154 | // } 155 | deploy { 156 | maven { 157 | mavenCentral.create("sonatype") { 158 | active = Active.ALWAYS 159 | verifyPom = false 160 | url = "https://central.sonatype.com/api/v1/publisher" 161 | stagingRepository( 162 | layout.buildDirectory 163 | .dir("target/staging-deploy") 164 | .get() 165 | .toString(), 166 | ) 167 | username = getRepositoryUsername() 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /pdfiumandroid/src/androidTest/java/io/legere/pdfiumandroid/suspend/PdfTextPageKtTest.kt: -------------------------------------------------------------------------------- 1 | package io.legere.pdfiumandroid.suspend 2 | 3 | import android.graphics.RectF 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.google.common.truth.Truth 6 | import io.legere.pdfiumandroid.base.BasePDFTest 7 | import junit.framework.TestCase 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.runBlocking 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.After 12 | import org.junit.Before 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | 16 | @RunWith(AndroidJUnit4::class) 17 | class PdfTextPageKtTest : BasePDFTest() { 18 | private lateinit var pdfDocument: PdfDocumentKt 19 | private var pdfBytes: ByteArray? = null 20 | 21 | @Before 22 | fun setUp() = 23 | runBlocking { 24 | pdfBytes = getPdfBytes("f01.pdf") 25 | 26 | TestCase.assertNotNull(pdfBytes) 27 | 28 | pdfDocument = PdfiumCoreKt(Dispatchers.Unconfined).newDocument(pdfBytes) 29 | } 30 | 31 | @After 32 | fun tearDown() { 33 | pdfDocument.close() 34 | } 35 | 36 | @Test 37 | fun textPageCountChars() = 38 | runTest { 39 | pdfDocument.openPage(0).use { page -> 40 | page.openTextPage().use { textPage -> 41 | val charCount = textPage.textPageCountChars() 42 | 43 | Truth.assertThat(charCount).isEqualTo(3468) 44 | } 45 | } 46 | } 47 | 48 | @Test 49 | fun textPageGetText() = 50 | runTest { 51 | pdfDocument.openPage(0).use { page -> 52 | page.openTextPage().use { textPage -> 53 | val text = textPage.textPageGetText(0, 100) 54 | 55 | Truth.assertThat(text?.length).isEqualTo(100) 56 | } 57 | } 58 | } 59 | 60 | @Test 61 | fun textPageGetUnicode() = 62 | runTest { 63 | pdfDocument.openPage(0).use { page -> 64 | page.openTextPage().use { textPage -> 65 | val char = textPage.textPageGetUnicode(0) 66 | 67 | Truth.assertThat(char).isEqualTo('T') 68 | } 69 | } 70 | } 71 | 72 | @Test 73 | fun textPageGetCharBox() = 74 | runTest { 75 | pdfDocument.openPage(0).use { page -> 76 | page.openTextPage().use { textPage -> 77 | val rect = textPage.textPageGetCharBox(0) 78 | 79 | Truth 80 | .assertThat(rect) 81 | .isEqualTo(RectF(90.314415f, 715.3187f, 103.44171f, 699.1206f)) 82 | } 83 | } 84 | } 85 | 86 | @Test 87 | fun textPageGetCharIndexAtPos() = 88 | runTest { 89 | pdfDocument.openPage(0).use { page -> 90 | page.openTextPage().use { textPage -> 91 | val characterToLookup = 0 92 | val rect = textPage.textPageGetCharBox(characterToLookup) 93 | 94 | val pos = 95 | textPage.textPageGetCharIndexAtPos( 96 | rect?.centerX()?.toDouble() ?: 0.0, 97 | rect?.centerY()?.toDouble() ?: 0.0, 98 | // Shouldn't need much since we're in the middle of the rect 99 | 1.0, 100 | 1.0, 101 | ) 102 | 103 | Truth.assertThat(pos).isEqualTo(characterToLookup) 104 | } 105 | } 106 | } 107 | 108 | @Test 109 | fun textPageCountRects() = 110 | runTest { 111 | pdfDocument.openPage(0).use { page -> 112 | page.openTextPage().use { textPage -> 113 | val rectCount = textPage.textPageCountRects(0, 100) 114 | 115 | Truth.assertThat(rectCount).isEqualTo(4) 116 | } 117 | } 118 | } 119 | 120 | @Test 121 | fun textPageGetRect() = 122 | runTest { 123 | pdfDocument.openPage(0).use { page -> 124 | page.openTextPage().use { textPage -> 125 | val rect = textPage.textPageGetRect(0) 126 | 127 | Truth.assertThat(rect).isEqualTo(RectF(0f, 0f, 0f, 0f)) 128 | } 129 | } 130 | } 131 | 132 | @Test 133 | fun textPageGetBoundedText() = 134 | runTest { 135 | pdfDocument.openPage(0).use { page -> 136 | page.openTextPage().use { textPage -> 137 | val text = textPage.textPageGetBoundedText(RectF(0f, 97f, 100f, 100f), 100) 138 | 139 | Truth.assertThat(text).isEqualTo("Do") 140 | } 141 | } 142 | } 143 | 144 | @Test 145 | fun getFontSize() = 146 | runTest { 147 | pdfDocument.openPage(0).use { page -> 148 | page.openTextPage().use { textPage -> 149 | val fontSize = textPage.getFontSize(0) 150 | 151 | Truth.assertThat(fontSize).isEqualTo(22.559999465942383) 152 | } 153 | } 154 | } 155 | 156 | @Test(expected = IllegalStateException::class) 157 | fun close() = 158 | runTest { 159 | var pageAfterClose: PdfTextPageKt? 160 | pdfDocument.openPage(0).use { page -> 161 | page.openTextPage().use { textPage -> 162 | pageAfterClose = textPage 163 | } 164 | } 165 | pageAfterClose!!.textPageCountChars() 166 | } 167 | 168 | @Test 169 | fun getPage() = 170 | runTest { 171 | pdfDocument.openPage(0).use { page -> 172 | page.openTextPage().use { textPage -> 173 | 174 | Truth.assertThat(textPage.page).isNotNull() 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /pdfiumandroid/src/main/cpp/include/fpdf_fwlevent.h: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The PDFium Authors 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com 6 | 7 | #ifndef PUBLIC_FPDF_FWLEVENT_H_ 8 | #define PUBLIC_FPDF_FWLEVENT_H_ 9 | 10 | // NOLINTNEXTLINE(build/include) 11 | #include "fpdfview.h" 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif // __cplusplus 16 | 17 | // Key flags. 18 | typedef enum { 19 | FWL_EVENTFLAG_ShiftKey = 1 << 0, 20 | FWL_EVENTFLAG_ControlKey = 1 << 1, 21 | FWL_EVENTFLAG_AltKey = 1 << 2, 22 | FWL_EVENTFLAG_MetaKey = 1 << 3, 23 | FWL_EVENTFLAG_KeyPad = 1 << 4, 24 | FWL_EVENTFLAG_AutoRepeat = 1 << 5, 25 | FWL_EVENTFLAG_LeftButtonDown = 1 << 6, 26 | FWL_EVENTFLAG_MiddleButtonDown = 1 << 7, 27 | FWL_EVENTFLAG_RightButtonDown = 1 << 8, 28 | } FWL_EVENTFLAG; 29 | 30 | // Virtual keycodes. 31 | typedef enum { 32 | FWL_VKEY_Back = 0x08, 33 | FWL_VKEY_Tab = 0x09, 34 | FWL_VKEY_NewLine = 0x0A, 35 | FWL_VKEY_Clear = 0x0C, 36 | FWL_VKEY_Return = 0x0D, 37 | FWL_VKEY_Shift = 0x10, 38 | FWL_VKEY_Control = 0x11, 39 | FWL_VKEY_Menu = 0x12, 40 | FWL_VKEY_Pause = 0x13, 41 | FWL_VKEY_Capital = 0x14, 42 | FWL_VKEY_Kana = 0x15, 43 | FWL_VKEY_Hangul = 0x15, 44 | FWL_VKEY_Junja = 0x17, 45 | FWL_VKEY_Final = 0x18, 46 | FWL_VKEY_Hanja = 0x19, 47 | FWL_VKEY_Kanji = 0x19, 48 | FWL_VKEY_Escape = 0x1B, 49 | FWL_VKEY_Convert = 0x1C, 50 | FWL_VKEY_NonConvert = 0x1D, 51 | FWL_VKEY_Accept = 0x1E, 52 | FWL_VKEY_ModeChange = 0x1F, 53 | FWL_VKEY_Space = 0x20, 54 | FWL_VKEY_Prior = 0x21, 55 | FWL_VKEY_Next = 0x22, 56 | FWL_VKEY_End = 0x23, 57 | FWL_VKEY_Home = 0x24, 58 | FWL_VKEY_Left = 0x25, 59 | FWL_VKEY_Up = 0x26, 60 | FWL_VKEY_Right = 0x27, 61 | FWL_VKEY_Down = 0x28, 62 | FWL_VKEY_Select = 0x29, 63 | FWL_VKEY_Print = 0x2A, 64 | FWL_VKEY_Execute = 0x2B, 65 | FWL_VKEY_Snapshot = 0x2C, 66 | FWL_VKEY_Insert = 0x2D, 67 | FWL_VKEY_Delete = 0x2E, 68 | FWL_VKEY_Help = 0x2F, 69 | FWL_VKEY_0 = 0x30, 70 | FWL_VKEY_1 = 0x31, 71 | FWL_VKEY_2 = 0x32, 72 | FWL_VKEY_3 = 0x33, 73 | FWL_VKEY_4 = 0x34, 74 | FWL_VKEY_5 = 0x35, 75 | FWL_VKEY_6 = 0x36, 76 | FWL_VKEY_7 = 0x37, 77 | FWL_VKEY_8 = 0x38, 78 | FWL_VKEY_9 = 0x39, 79 | FWL_VKEY_A = 0x41, 80 | FWL_VKEY_B = 0x42, 81 | FWL_VKEY_C = 0x43, 82 | FWL_VKEY_D = 0x44, 83 | FWL_VKEY_E = 0x45, 84 | FWL_VKEY_F = 0x46, 85 | FWL_VKEY_G = 0x47, 86 | FWL_VKEY_H = 0x48, 87 | FWL_VKEY_I = 0x49, 88 | FWL_VKEY_J = 0x4A, 89 | FWL_VKEY_K = 0x4B, 90 | FWL_VKEY_L = 0x4C, 91 | FWL_VKEY_M = 0x4D, 92 | FWL_VKEY_N = 0x4E, 93 | FWL_VKEY_O = 0x4F, 94 | FWL_VKEY_P = 0x50, 95 | FWL_VKEY_Q = 0x51, 96 | FWL_VKEY_R = 0x52, 97 | FWL_VKEY_S = 0x53, 98 | FWL_VKEY_T = 0x54, 99 | FWL_VKEY_U = 0x55, 100 | FWL_VKEY_V = 0x56, 101 | FWL_VKEY_W = 0x57, 102 | FWL_VKEY_X = 0x58, 103 | FWL_VKEY_Y = 0x59, 104 | FWL_VKEY_Z = 0x5A, 105 | FWL_VKEY_LWin = 0x5B, 106 | FWL_VKEY_Command = 0x5B, 107 | FWL_VKEY_RWin = 0x5C, 108 | FWL_VKEY_Apps = 0x5D, 109 | FWL_VKEY_Sleep = 0x5F, 110 | FWL_VKEY_NumPad0 = 0x60, 111 | FWL_VKEY_NumPad1 = 0x61, 112 | FWL_VKEY_NumPad2 = 0x62, 113 | FWL_VKEY_NumPad3 = 0x63, 114 | FWL_VKEY_NumPad4 = 0x64, 115 | FWL_VKEY_NumPad5 = 0x65, 116 | FWL_VKEY_NumPad6 = 0x66, 117 | FWL_VKEY_NumPad7 = 0x67, 118 | FWL_VKEY_NumPad8 = 0x68, 119 | FWL_VKEY_NumPad9 = 0x69, 120 | FWL_VKEY_Multiply = 0x6A, 121 | FWL_VKEY_Add = 0x6B, 122 | FWL_VKEY_Separator = 0x6C, 123 | FWL_VKEY_Subtract = 0x6D, 124 | FWL_VKEY_Decimal = 0x6E, 125 | FWL_VKEY_Divide = 0x6F, 126 | FWL_VKEY_F1 = 0x70, 127 | FWL_VKEY_F2 = 0x71, 128 | FWL_VKEY_F3 = 0x72, 129 | FWL_VKEY_F4 = 0x73, 130 | FWL_VKEY_F5 = 0x74, 131 | FWL_VKEY_F6 = 0x75, 132 | FWL_VKEY_F7 = 0x76, 133 | FWL_VKEY_F8 = 0x77, 134 | FWL_VKEY_F9 = 0x78, 135 | FWL_VKEY_F10 = 0x79, 136 | FWL_VKEY_F11 = 0x7A, 137 | FWL_VKEY_F12 = 0x7B, 138 | FWL_VKEY_F13 = 0x7C, 139 | FWL_VKEY_F14 = 0x7D, 140 | FWL_VKEY_F15 = 0x7E, 141 | FWL_VKEY_F16 = 0x7F, 142 | FWL_VKEY_F17 = 0x80, 143 | FWL_VKEY_F18 = 0x81, 144 | FWL_VKEY_F19 = 0x82, 145 | FWL_VKEY_F20 = 0x83, 146 | FWL_VKEY_F21 = 0x84, 147 | FWL_VKEY_F22 = 0x85, 148 | FWL_VKEY_F23 = 0x86, 149 | FWL_VKEY_F24 = 0x87, 150 | FWL_VKEY_NunLock = 0x90, 151 | FWL_VKEY_Scroll = 0x91, 152 | FWL_VKEY_LShift = 0xA0, 153 | FWL_VKEY_RShift = 0xA1, 154 | FWL_VKEY_LControl = 0xA2, 155 | FWL_VKEY_RControl = 0xA3, 156 | FWL_VKEY_LMenu = 0xA4, 157 | FWL_VKEY_RMenu = 0xA5, 158 | FWL_VKEY_BROWSER_Back = 0xA6, 159 | FWL_VKEY_BROWSER_Forward = 0xA7, 160 | FWL_VKEY_BROWSER_Refresh = 0xA8, 161 | FWL_VKEY_BROWSER_Stop = 0xA9, 162 | FWL_VKEY_BROWSER_Search = 0xAA, 163 | FWL_VKEY_BROWSER_Favorites = 0xAB, 164 | FWL_VKEY_BROWSER_Home = 0xAC, 165 | FWL_VKEY_VOLUME_Mute = 0xAD, 166 | FWL_VKEY_VOLUME_Down = 0xAE, 167 | FWL_VKEY_VOLUME_Up = 0xAF, 168 | FWL_VKEY_MEDIA_NEXT_Track = 0xB0, 169 | FWL_VKEY_MEDIA_PREV_Track = 0xB1, 170 | FWL_VKEY_MEDIA_Stop = 0xB2, 171 | FWL_VKEY_MEDIA_PLAY_Pause = 0xB3, 172 | FWL_VKEY_MEDIA_LAUNCH_Mail = 0xB4, 173 | FWL_VKEY_MEDIA_LAUNCH_MEDIA_Select = 0xB5, 174 | FWL_VKEY_MEDIA_LAUNCH_APP1 = 0xB6, 175 | FWL_VKEY_MEDIA_LAUNCH_APP2 = 0xB7, 176 | FWL_VKEY_OEM_1 = 0xBA, 177 | FWL_VKEY_OEM_Plus = 0xBB, 178 | FWL_VKEY_OEM_Comma = 0xBC, 179 | FWL_VKEY_OEM_Minus = 0xBD, 180 | FWL_VKEY_OEM_Period = 0xBE, 181 | FWL_VKEY_OEM_2 = 0xBF, 182 | FWL_VKEY_OEM_3 = 0xC0, 183 | FWL_VKEY_OEM_4 = 0xDB, 184 | FWL_VKEY_OEM_5 = 0xDC, 185 | FWL_VKEY_OEM_6 = 0xDD, 186 | FWL_VKEY_OEM_7 = 0xDE, 187 | FWL_VKEY_OEM_8 = 0xDF, 188 | FWL_VKEY_OEM_102 = 0xE2, 189 | FWL_VKEY_ProcessKey = 0xE5, 190 | FWL_VKEY_Packet = 0xE7, 191 | FWL_VKEY_Attn = 0xF6, 192 | FWL_VKEY_Crsel = 0xF7, 193 | FWL_VKEY_Exsel = 0xF8, 194 | FWL_VKEY_Ereof = 0xF9, 195 | FWL_VKEY_Play = 0xFA, 196 | FWL_VKEY_Zoom = 0xFB, 197 | FWL_VKEY_NoName = 0xFC, 198 | FWL_VKEY_PA1 = 0xFD, 199 | FWL_VKEY_OEM_Clear = 0xFE, 200 | FWL_VKEY_Unknown = 0, 201 | } FWL_VKEYCODE; 202 | 203 | #ifdef __cplusplus 204 | } // extern "C" 205 | #endif // __cplusplus 206 | 207 | #endif // PUBLIC_FPDF_FWLEVENT_H_ 208 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | --------------------------------------------------------------------------------